分布式锁的实现之 redis 篇

编程之家收集整理的这篇文章主要介绍了分布式锁的实现之 redis 篇编程之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
@H_403_0@为什么需要分布式锁

引入经典的秒杀情景,100件商品供客户抢。如果是单机版的话,我们使用synchronized 或者 lock 都可以实现线程安全。但是如果多个服务器的话,synchronized 和 lock 就不管用了(废话,怎么可能管用,都不在同一段代码了)。

分布式锁就是被设计出来实现多个服务器的线程安全。

很容易想到的方案是把共享变量(锁)抽取出来放在一个公共的数据库里(Redis、Memchhed)里,所有的服务器通过这个公共的资源实现数据的一致性,防止超卖。

具体实现

分布式锁的实现方式有:Memchched分布式锁、Redis分布式锁、Zookeeper分布式锁,这里我们以Redis分布式锁为例,Redis分布式锁也是现在使用得最多的

1. 思路

  • setnx加锁

    setnx是实现分布式的核心,意思是只有当前key不存在才返回1,当前key存在返回0

    这个key就是我们的“锁”,只有线程获得锁才能继续执行,执行完del这个key相当于解锁操作。这个就是redis实现分布式锁的核心,怎么样,很好理解吧

  • del解锁

2. 第一个问题:锁无法被释放

试想一下,如果你执行完set命令服务器宕机了,来不及del解锁,那么这个锁永远无法被释放,其他线程无法执行。

解决方法是key必须设置一个超时时间,即使没有被显示释放,也在超时后自动释放。

redis为我们提供了这个命令设置超时时间

  • expire key ttl 秒为单位
  • pexpire key ttl 毫秒为单位
  • expireat key timestamp
  • pexpireat key timestamp

因此加锁的操作变成:

setnx lock 1
expire lock 10

但是这两个操作不保证原子性(Redis单条操作保证原子性),如果加完锁还没设置过期时间服务器就宕机了,同样会导致死锁,因此加锁整个操作必须保证原子性。

redis提供了set+过期时间的原子操作

set lock 1 EX 10 NX
// 最终的加锁命令

3. 第二个问题:错误释放锁

第二个问题,如果线程执行时间超过TTL,当前锁被自动销毁

但是等线程执行完了,原来的del方法还会执行,它就会去执行解锁操作,把其他线程占用的锁给del了,这会产生非常严重的问题

String REDIS_Lock="lock";
String value=1;
try{
    redisUtil.setLockDistribute(REDIS_LOCK,1,10);
    ......业务逻辑
        
}finally{
    // 这个操作有可能会误删锁
    redisUtil.del(REDIS_LOCK);
}

解决方案是key的value不再是默认的了

String REDIS_Lock="lock";
String value=UUID.randomUUID().toString()+Thread.currentThread().getname();
try{
    redisUtil.setLockDistribute(REDIS_LOCK,10);
    ......业务逻辑
        
}finally{
    // 先判断后删除
    if(redisUtil.get(REDIS_LOCK).equals(value)){
        redisUtil.del(REDIS_LOCK);
    }
    
}

这样写其实还有个问题,判断和删除无法保证原子性,还是有可能误删。因此解锁我们使用lua脚本来保证原子性:工具类有实现lua脚本的方法

//lua脚本删除key原子操作
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

(解锁操作也可用事务来保证原子性,应付面试,实战还是lua脚本)

4. 第三个问题:超时解锁导致并发

加锁和解锁操作我们都搞定了,但是还有一个问题:如果你的线程执行时间超过ttl过期时间,锁还是被释放了,其他线程可以和次线程并发执行,这是我们并不想看到的。

因此我们要为ttl延时

我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”。

image-20210502230844016

5. 集群环境下可能出现的问题

redis集群环境,多个master,多个slave的情况下:

当主节点挂掉时,从节点会取而代之,但客户端无明显感知。当客户端 A 成功加锁,指令还未同步,此时主节点挂掉,从节点提升为主节点,新的主节点没有锁的数据,当客户端 B 加锁时就会成功。

也就是主结点加了锁就宕机了,从节点还没同步,当该从节点提升为主节点时就会出错。

image-20210502231255950

解决方案我也不清楚....以后碰到再找资料

开源框架Redisson

上面的流程如果手写的话会要人老命,开源框架Redisson帮我们摆平一切,现在用得十分多

直接上代码

// 注入redisson
public Redisson redisson(){
    Config config=new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
    return Redisson.create(config);

}
@Autowired
Redisson son;

String REDIS_Lock="lock";
String value=UUID.randomUUID().toString()+Thread.currentThread().getname();
RLock lock=son.getLock();
try{
    lock.lock();
    ......业务逻辑
        
}finally{
   	lock.unlock();
}

// 这段代码解决上述三个问题,集群环境下redis分布式锁的实现

结语

分布式锁看起来难其实原理还是很简单的,没事多看看官方文档,讲得挺细致的

参考

总结

以上是编程之家为你收集整理的分布式锁的实现之 redis 篇全部内容,希望文章能够帮你解决分布式锁的实现之 redis 篇所遇到的程序开发问题。

如果觉得编程之家网站内容还不错,欢迎将编程之家网站推荐给程序员好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。

小编个人微信号 jb51ccc
喜欢与人分享编程技术与工作经验,欢迎加入编程之家官方交流群!

相关文章

猜你在找的Java相关文章

什么是线程不安全 我对线程安全的理解就是多个线程同时操作一个共享变量时会产生意料之外的情况,这种情况就是线程不安全。注意:只有写操作才可能出现线程不安全,对共享变量只进行读操作线程是绝对安全的。 具体
为什么要使用线程池 创建/销毁线程需要消耗系统资源,线程池可以复用已创建的线程。 控制并发的数量。并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃。(主要原因) 可以对线程做统一管理。 JUC
参考资料: JVM虚拟机入门教程-陈树义 JavaGuide 《深入理解Java虚拟机》 面经: 个人整理 - Java 后端面试题 - JVM 篇 阿里 Java 实习十轮面试面经 1. 讲一下JV
代码如下:/*  练习把一个整数逆序输出  分别把个位,十位,百位,千位等各位的数字取出来*/import java.util.Scanner;class Demo18 { public static void main(String[] args)  { Scanner sc=new Scanner(System.in); System.out.println("请输入:"); int n...
代码如下:/*  练习限制用户登陆的次数*/import java.util.Scanner;class Demo16 { public static void main(String[] args)  { Scanner sc=new Scanner(System.in); String name=""; String pwd=""; for(int i=1;i<=5;i++){ ...
代码如下:/*  从键盘上输入正数和附属,分别统计正数和负数的个数并计算所有数的和。输入0表示结束*/import java.util.Scanner;class Demo15{ public static void main(String[] args)  { Scanner sc=new Scanner(System.in); int num=1,t=0,f=0,sum=0; while...
代码如下:/*  多分支练习彩票游戏,随机生成两个一位数的随机数,提示用户输入猜测的数字  如果完全匹配奖金10000,只匹配数字没有匹配顺序奖金3000,只匹配一个数字奖金1000  例:若生成的随机数为18,如果用户输入18奖金10000;如果用户输入81奖金3000;如果用户输入16奖金1000*/import java.util.Scanner;class Demo13{ public s...
代码如下:/*  练习简易计算器*/import java.util.Scanner;class Demo12 { public static void main(String[] args)  { Scanner sc=new Scanner(System.in); System.out.println("请输入第一个数字:"); int a=sc.nextInt(); System.o...