分布式锁知识总结

分布式锁知识总结

为什么要用分布式锁?

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

分布式锁的执行流程

 

分布式锁的实现方式

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 加的锁,......往复循环 ,导致锁永久失效。

 

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

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框架 使用如下:

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类似  , 牺牲了性能,如果没超过半数,涉及到锁回滚问题)

 

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

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

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

 

2、zookeeper实现分布式锁

           

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

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

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

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

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

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

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

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

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

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

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

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

 

 

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章