Redis——Redis实现分布式锁

  • 分布式锁
    • 基于数据库实现分布式锁
    • 基于缓存Redis实现分布式锁
      • Redisson
    • 基于Zookeeper实现分布式锁

1. 数据库实现

(1)悲观锁实现:利用select … where … for update 加排他锁,业务处理完再用commit()释放锁

  • 注:要注意行锁转换成表锁的问题
    • InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给method_name添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上
  • 存在问题和解决方式
    • 阻塞问题:无需while()循环,for update语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功
    • 服务宕机:使用这种方式,服务宕机之后数据库会自己把锁释放掉,无需担心

(2)乐观锁实现:CAS,利用唯一性约束insert加锁,delete解锁

  • 加锁:
    • insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)
  • 解锁:
    • delete from methodLock where method_name ='method_name'
  • 对method_name做唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功
  • 存在问题和解决方式
    • 单点问题:这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用 ——> 主备模式解决
    • 可能死锁:这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁 ——> 做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍
    • 非阻塞锁:数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作  ——> while循环,直到insert成功再返回成功
    • 不可重入:这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了 ——> 在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了
  • 不可解决的问题:
    • select for update是否加行锁不确定:MySql会对查询进行优化,即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的
    • 排他锁来进行分布式锁的lock,那么一个排他锁长时间不提交,就会占用数据库连接。一旦类似的连接变得多了,就可能把数据库连接池撑爆

(3)总结

  • 优点
    • 用以实现,简单易懂
  • 缺点
    • 数据库开销,性能影响大
    • 数据库的行级锁并不一定靠谱,尤其是当我们的锁表并不大的时候
    • 会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂

2. Redis实现

  • 涉及命令
    • SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0
    • expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁
    • delete key:删除key
  • 核心思想:key=lock, value=uuid();每次先用SETNX尝试拿锁,如果key不存在说明可以加锁;如果key不存在说明已经加了锁了,再通过uuid()判断是不是自己的可重入锁
  • 由于需要保证原子性,通过lua脚本:
  • LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

(1)Redisson:开源库(Ref:https://blog.csdn.net/shuangyueliao/article/details/89344256

  • Client1加锁:LUA脚本
    • Client1生成uuid,并加锁(hash数据结构):hset myLock 8743c9c0-0795-4907-87fd-6c719a6b4586:1 1
    • 上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加锁
    • 接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒
  • Client2尝试加锁,加锁失败
    • 首先判断exists myLock,锁已经存在
    • 其次判断是否是自己的可重入锁:uuid()不一样,不是的
    • 最后获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的剩余生存时间
    • while()循环,不停CAS尝试加锁
  • watch dog自动延期机制
    • Client1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢
    • Client1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间
  • Client1继续加锁:可重入锁
    • 首先判断exists myLock,锁已经存在
    • 其次判断是否是自己的可重入锁:uuid()一样,可重入锁
    • 对这个hash的value+1,表示可重入次数
  • 释放锁
    • 当解锁时,对value-1,如果发现是0,则del MyLock,释放锁
  • 缺点:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁
    • 过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master
    • 客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁
    • 此时就会导致多个客户端对一个分布式锁完成了加锁,导致各种脏数据的产生

3. Zookeeper实现

  • 大致思想:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题

4. 总结

  • 从理解的难易程度角度(从低到高):数据库 > 缓存 > Zookeeper
  • 从实现的复杂性角度(从低到高):Zookeeper >= 缓存 > 数据库
  • 从性能角度(从高到低):缓存 > Zookeeper >= 数据库
  • 从可靠性角度(从高到低):Zookeeper > 缓存 > 数据库

Ref

http://www.hollischuang.com/archives/1716

https://blog.csdn.net/qq_35620501/article/details/95047642

https://www.cnblogs.com/barrywxx/p/11644803.html

Redisson:https://blog.csdn.net/shuangyueliao/article/details/89344256

猜你喜欢

转载自blog.csdn.net/qq_41157876/article/details/115460197