欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

redisson分布式锁实现原理

程序员文章站 2022-03-10 16:09:43
分布式锁知识总结在单体架构中,我们可以通过sychronized来保证并发安全,但是在分布式架构中,sychronized没用,sychronized只能保证一个进程(JVM)中的线程安全,无法跨进程为什么要用分布式锁?1、redis实现分布式锁 最简单的redis实现就是通过 redis的setnx(set if not exists)命令来实现 RedisTemplate对redis进行了封装,使用redistemplate就能实......

本人小白一个,不能保证博客中内容都准确,如果博客中有错误的地方,望各位多多指教,请指正。欢迎找我一起讨论

 

分布式锁知识总结

为什么要用分布式锁?

       在单体架构中,我们可以通过sychronized来保证并发安全,但是在分布式架构中,sychronized没用,sychronized只能保证一个进程(JVM)中的线程安全,无法跨进程

redisson分布式锁实现原理

分布式锁的执行流程

redisson分布式锁实现原理

 

分布式锁的实现方式

1、redis实现分布式锁

         最简单的redis实现就是通过 redis的setnx(set if not exists)命令来实现 

           RedisTemplate对redis进行了封装,使用redistemplate就能实现 redis的setnx(set if not exists)命令

                    bool   result  =       stringRedisTemplate.opsForValue().setIfAbsent( "key","value"  );  // setIfAbsent方法就相当于  setnx命令

                                         setIfAbsent方法 返回 true  说明key不存在,插入数据成功

                                         setIfAbsent方法 返回 true  说明key已经存在了,插入数据失败

                                           

          大概的代码逻辑就是 

                      bool   result  =       stringRedisTemplate.opsForValue().setIfAbsent( "key","value"  );  // 设置key(相当于竞争锁   )

                      if(!result){

                            return "key已经存在";

                      }

                      执行业务逻辑代码(比如扣库存)

                      stringRedisTemplate.delete("key");  // 删除key (相当于释放锁)

 

想想这里面可能出现的问题:

1、线程在运行业务代码的时候出现异常,那么此时就无法释放锁,就会形成死锁。怎么办?   =====》try  finally  ,在finally中执行delete操作

 

2、线程在执行业务代码的时候,服务器宕机了,此时也无法释放锁,也会形成死锁。怎么办?  =====》 给 锁的key设置一个timeout

                       int timeout = 10;

                      stringRedisTemplate.expire("key",timeout ,TimeUnit.SECONDS);

3、此时加锁与设置timeout之间是有时间损耗的,也就是说如果线程在加完锁正准备设置timeout时,此时服务宕机了,timeout就设置不了,就跟上面情况类似了,也会产生死锁,怎么办?   ================》 保证  加锁与设置timeout 的原子性   ( RedisTemplate 提供了一个重载的setIfAbsent方法来保证原则性,此方法底层就是使用了lua脚本)

                     redis会将lua脚本中所有的命令当作一个原子操作执行

                         int timeout = 10;

                        //  bool   result  =       stringRedisTemplate.opsForValue().setIfAbsent( "key","value"  );

                        //  stringRedisTemplate.expire("key",timeout ,TimeUnit.SECONDS);

                          bool   result  =  stringRedisTemplate.opsForValue().setIfAbsent( "key","value" ,timeout ,TimeUnit.SECONDS);  // 这行代码就相当于把上面两行代码进行了原子性操作

 

4、线程 1 还没执行完业务代码,此时锁timeout了,redis把线程1 加的锁删了 ,此时线程 2 过来了加了锁,然后执行业务代码,注意! 此时线程 1 和线程 2 可能同时操作共享资源 ,然后线程1 此时执行完了删锁,注意! 此时线程 1 删除的锁是线程 2 加的锁,然后此时线程 3过来了加了锁,然后执行业务代码 ,注意! 此时线程 2 和线程 3可能同时操作共享资源,然后线程2 此时执行完了删锁,注意! 此时线程2 删除的锁是线程 3 加的锁,......往复循环 ,导致锁永久失效。

 

怎么办?=====》给自己加的锁  加一个标识,这个标识只有自己知道,此时锁就只能自己释放,别人就释放不了了

redisson分布式锁实现原理

5、线程 1 还没执行完业务代码,此时锁timeout了,redis把线程1 加的锁删了 ,此时线程 2 过来了加了锁,然后执行业务代码,注意! 此时线程 1 和线程 2 可能同时操作共享资源 ,然后线程1 此时执行完了业务代码之后删锁,哦吼,我的锁怎么没了,然后就报错了。怎么办?

======》加大timeout的值

=======》但是有个问题,timeout越大  ====== 》 服务宕机之后,死锁时间越长  =======》其他线程等待的时间越长  ======》用户的脾气越大。 怎么办?

======》开启一个异步线程,开启一个定时任务,每个 1/3 timeout扫描一次,看看当前锁的key 还在不在,当前线程还在不在,是不是死(宕机 )了,然后线程还在,锁的key也还在,那么就给锁续命,重新设置锁的key 的 timeout  ,只要线程没执行完就给锁的key续命,知道线程执行完自动删除锁的key(释放锁)

此时就不得不介绍一个神奇的框架了 !!!!redisson框架

redisson框架     ---- 一个 redis java client

redisson框架 中解决了上面,底层就是用的setnx ,lua脚本保证原子性,锁的key默认timeout 30s ,其他线程自旋 , 用timerTask 定时任务 默认  每隔 1/3 timeout 续命一次。

redisson分布式锁实现原理

redisson分布式锁实现原理:

redisson分布式锁实现原理

 

redisson框架 使用如下:

redisson分布式锁实现原理

redisson分布式锁实现原理

6、如果redis用了集群(一主多从),线程 1 的锁的key刚把锁的key存入redis中,   redis的master 主节点挂了,锁的key还没同步到slave从节点,然后此时slave从节点升为 master主节点,但是此时这个新的master主节点中没有线程 1 的锁的key ,但是此时线程 1 还在执行业务代码,然后才是 线程 2 来了,由于此时新的master主节点上没有锁的key,所以线程2申请锁成功,然后此时线程 1  和 线程 2 就可能同时访问并使用共享资源  。  此时就有问题了呀! 怎么办?

=======》①用zookeeper    有延时 ,要同步半数以上的follower才能加锁成功,所以此时不用担心leader挂了,follower中没有锁的key   ②人工补偿    ③redis自己解决 用redlock

  redlock  实现原理

大概就是搞多个对等的redis节点,节点之间没有依赖(主从依赖),通过 setnx命令 加锁 ,同时发给每个redis节点,发送的这些节点中要有半数以上的redis节点加锁成功,才算认为线程拿到了锁,才继续往下执行业务。(原理跟zookeeper类似  , 牺牲了性能,如果没超过半数,涉及到锁回滚问题)

redisson分布式锁实现原理

 

7、高并发性下请求到了redis,但是redis是单线程工作模式,在redis中就也不是并发执行,而是串行执行,影响性能。怎么办?

====》 用库存举例,   将库存分段存储,分段加锁=======》把每段的锁的key放到redis集群中,把不同锁的key放到不同的redis master主节点上

        一个段的库存不够减怎么办?去减下面段的,合并几个段一起扣减。

 

2、zookeeper实现分布式锁

           redisson分布式锁实现原理

              由于zookeeper上面的临时节点是唯一的,只会创建一个,而锁也只有一把,所以它能代表这个锁,多个客户端去抢这个锁,也就是去zookeeper上面创建临时节点,看谁创建的快,谁第一个创建了这个临时节点谁就获取到了锁。

redisson分布式锁实现原理

此时clientA第一个成功创建了临时节点,就代表clientA已经获取到了锁,其他client再去创建这个临时节点,发现这个节点已经存在了,他们就不能去创建了,就都阻塞了,他们一直监听这个节点的变化,也就是在监听这个锁的变化(通过zookeeper  Watch功能)

redisson分布式锁实现原理

clientA完成了它的业务操作 ,结束了与zookeeper的会话  临时节点被自动删除,就代表释放了锁,此时其他client监听到了临时节点被删除了(锁释放了),就都会去竞争锁,也就是去创建临时节点。

同一时间有多个客户端在竞争锁

======》上面这个实现方式有什么问题呢 ?想想如果上千个client去监听这个临时节点的变化,一旦这个临时节点变化了,然后此时就是上千个client去竞争这个锁(羊群效应 / 惊群效应),这对于zookeeper的压力是非常大的,所以这个方案不可行。那怎么办呢?

======》使用临时顺序节点  + 监听   

     使用临时顺序节点创建出来的节点都是有序的,只有最小的节点能拿到锁,其他的节点监听比它小的前一个节点。

获取锁大概流程:  每个client 分别 创建临时顺序节点,然后谁的节点最小就谁能拿到锁,其他节点就监听比自己小的前一个节点

   比如下图:A B  C D分别创建节点,A最先创建,所以它的节点编号最小,所以它能获取锁,然后B监听A,C监听B,D监听C

redisson分布式锁实现原理

释放锁大概流程:clientA 执行完自己的业务之后结束了与zookeeper的会话,会话一结束,节点会自动删除(释放了锁),同时clientB监听到了clientA的这个节点被删除后,会去判断自己的节点是不是最小的节点,如果是最小的就获取锁,如果不是就继续监听等待,不做任务处理,clientA节点删除只对clientB有影响,对后面的 C  D 等节点没有影响,因为C只监听B,B是没有变化的,所以C不会受到影响,不会变,D只监听C,C没有变,D也不会有影响,也不会变.......就解决的羊群(惊群)效应。

同一时间只有一个客户端在竞争锁

redisson分布式锁实现原理

大概流程:  自己看图   , 直接通过 curator框架就可以实现, curator已经将这些问题解决了,封装好了,直接用就行。

redisson分布式锁实现原理

 

 

3、数据库实现分布式锁

     基于数据库表(主要原理:数据库的主键不能重复,作为分布式锁的实现)

           0、首先先创建一个tb_lock表,用于记录当前哪个线程正在使用数据,表里就一个  业务ID 或者业务名称  字段 (唯一主键),当作锁

           1、当线程要访问数据时,会先将要执行的业务的业务id或者业务名称 ,insert插入tb_lock表中

           2、当插入成功,代表该线程获得了锁,即可执行业务逻辑

           3、当其他线程在执行相同业务的时候也会先将要执行业务的  业务id或者业务名称  插入tb_lock表中,由于主键冲突,此时会导致插入失败,就代表获取锁失败

           4、获取锁成功的线程在执行完业务代码后,在删除tb_lock表中删除对应业务的 业务id或者业务名称 ,代表释放锁

想想这样实现会出现问题吗?

1、一旦数据库挂掉,会导致业务系统不可用。   

                        ====》用两个数据库,一主一从,数据之前双向同步。一旦挂掉快速切换到备库上。

2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。

                        ====》做一个异步定时任务,每隔一定时间把数据库中的超时数据清理一遍。

3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。

                        ====》用while循环,直到insert成功再返回成功。

4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

                        ====》在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

     基于数据库表的排它锁实现(基于MySql的InnoDB引擎)

           1、在执行业务逻辑之前,先通过一个带有for  update 的查询语句 拿到排它锁 

                                                  select * from table where productId = 1 for update

                         行锁 : 当前连接要执行的带有 for update 的SQL语句以后,指定了主键查询,代表当前连接锁定了这条数据(productId = 1)

                                                  select * from table  for update

                         表锁:当前连接执行带有for update的SQL语句以后,没有指定主键查询,那么会将表进行锁定,只有当前连接可以对这张表进行操作

           2、获得排它锁的线程即可获得分布式锁,执行业务逻辑

           3、执行完业务逻辑之后,再通过JDBC的connection.commit() 方法来释放锁

想想这样实现会出现问题吗?

1、一旦数据库挂掉,会导致业务系统不可用。   

                        ====》使用这种方式,此问题不会发生,服务宕机之后数据库会自己把锁释放掉。

2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。

                        ====》做一个异步定时任务,每隔一定时间把数据库中的超时数据清理一遍。

3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。

                        ====》使用这种方式,此问题不会发生,for update语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。

4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

                        ====》在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

本文地址:https://blog.csdn.net/qq_36743888/article/details/112486814