跳至主要內容

分布式锁

ltyzzz约 1617 字大约 5 分钟

问题清单

Questions

  1. 传统单机模式下 JVM 锁失效场景有哪些?

  2. 如何防止超卖现象?(如何保证多线程下共享资源安全性)

  3. 为什么要用 Redis 实现分布式锁?

  4. 采用 Redis 做分布式锁需要考虑哪些点?

  5. 对比一下 Redis、MySQL、Zookeeper 实现分布式锁的优缺点

  6. Redis 分布式锁有哪些安全问题?

  7. Redisson有哪些功能?

问题回答

  1. 传统单机模式下 JVM 锁失效场景有哪些?

Answer

  • Spring 中设置多例模式,造成锁失效

  • 添加事务注解,造成锁失效

    • 事务原理为 AOP,在前置方法开启事务,在后置方法提交事务

    • 而加锁和解锁操作分别位于开启事务后和提交事务前

    • 如此操作会造成当 a 释放锁但是未提交事务前,b 得到了锁,读取到的数据还是旧数据,因为 MySQL 默认隔离级别是可重复读

  • 分布式场景下,锁失效

  1. 如何防止超卖现象?(如何保证多线程下共享资源安全性)

Answer

  • 使用 JVMsynchronized / reentrantlock

    • 缺点很多,适用范围小
  • 使用 mysql 单行语句进行更新操作

    • update db_stock set count = count - 1 where productId = xxx and stock >= xxx
      
    • 缺点为:

      • 无法获取商品库存更新前后的数量
      • 锁范围问题,需要为查询条件添加索引,由表级锁降级为行级锁
  • 使用 mysql 乐观锁进行更新

    • 在对应的表中设置 version 字段

    • 先查询,得到当前的 version 字段

      select * from db_stock where productId = xxx 
      
    • 更新时条件中加上 version,需要确认当前数据库中的 version 是否与查到的 version 相等,不相等则自旋重试

      update db_stock set count = xxx and version = version + 1 where productId = xxx and version = xxx
      
    • 缺点为:乐观锁不适用于竞争激烈的场景,会导致 CPU 资源占据过大,性能低。乐观锁还存在 ABA 问题

  • 使用 mysql 悲观锁进行更新

    通过 select ... for update 语句为表数据加行级锁,然后进行修改操作

    • 缺点为:悲观锁性能也较低,也可能造成死锁问题
  1. 为什么要用 Redis 实现分布式锁?

Answer

  1. 性能高效,Redis 基于内存,读写效率高

  2. 实现方便,Redis 提供的 setnx 操作为原子命令,天然适合做分布式锁

  3. 适合分布式场景,因为 Redis 是独立于各个集群的,可以被多个集群共享

  1. 采用 Redis 做分布式锁需要考虑哪些点?

Answer

  • 死锁:当某一线程获取到 Redis 分布式锁之后,突然宕机,那么之后的请求将永远无法得到该锁

    • 需要为该 key 设置过期时间
  • 加锁原子性:可能在设置 key 和设置过期时间之间,Redis 宕机。因此需要保证原子性。

    • Redis 提供了原子命令,可以将 setnxexpire 结合到一个命令中

      set lock xxx ex 20 nx
      
  • 误删操作:

    • 请求 a 执行操作的时间大于过期时间,导致操作还未执行完成,锁就被自动释放了

    • 请求 b 在自动释放后,又获取了锁。等到请求 a 操作结束后,手动释放了锁,而此时相当于将请求 b 的锁释放了

    • 因此为防止误删操作,在每一个线程获取锁的时候,需要设置锁的 value。在释放锁的时候,需要比较 value 是否相等,即判断是不是自己的锁

  • 解锁原子性:判断锁是否属于自己与解锁操作需要保证原子性

    • 采用 lua 脚本实现

      if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
      
  • 自动续期:某请求处理时间超过了锁的过期时间,导致其他线程获取到了锁,此时多线程共同操作共享资源,产生线程安全问题

    • 采用 Timer 定时任务,每隔一段时间为锁续期
  • 可重入性:利用 hset 实现

  1. 对比一下 Redis、MySQL、Zookeeper 实现分布式锁的优缺点

Answer

  1. Redis 实现简单,性能较高,很适合用作分布式锁,但是需要去自行处理宕机解锁、自动续期等操作

  2. MySQL 实现简单,但是性能过低、可靠性差,防止死锁、自动续期等操作均需要自己手动实现

  3. Zookepper 的临时节点能够保证宕机后被自动删除,不需要额外考虑死锁问题,且其提供的序列化节点,满足公平锁。

    但是 Zookeeper 实现锁的性能不高,且需要额外的部署和维护成本

  1. Redis 分布式锁有哪些安全问题?

Answer

主从模式下,客户端 1 在主库上执行加锁操作,但是主库宕机后,从库顶替主库,产生锁丢失问题

引入 RedLock 算法解决主从模式下分布式锁的安全问题

  • RedLock 只有主机没有从机和哨兵节点,官方推荐部署 5 个主机实例

  • 客户端在多个 Redis 实例上请求加锁,超过半数节点成功才视为最终加锁成功

  • 加锁总时间不能超过锁的过期时间

  • 释放锁需要向全部节点发起释放请求

RedLock的安全问题:

  • GC stop the world

    GC 可能发生在任一时刻,如果客户端 1 通过 RedLock 算法获取到锁,但是此时发生了 GCGC 过程中,获取到的锁过期,又被客户端 2 获取到锁。

    等客户端 1 GC 结束后,它仍然认为此刻持有锁。此时,多个客户端可以同时操作共享资源,造成线程安全问题

    但是此问题被反驳:加锁成功之前的 GC 带来的问题可以通过加锁总时间是否大于过期时间从而检测出来,而加锁成功之后的 GC 则所有算法都无法解决

    • 因此:所有分布式锁在此极端情况下,均存在不安全问题
  • 时钟偏移问题

  1. Redisson有哪些功能?

Answer

  1. 可重入锁

  2. 公平锁

  3. 联锁 RedissonMultiLock

  4. 红锁 RedissonRedLock

  5. 读写锁 RReadWriteLock

  6. 信号量 RSemaphore

  7. 倒计时器 RCountDownLatch

  8. 限流器 RRateLimiter