Redis分布式锁:有 SETNX,还需要 Redisson?

188 阅读4分钟

组里有同学说:“分布式锁直接用 SETNX 就够了,没必要上框架”。

image.png

这里先说结论:优先使用封装好的框架 Redisson。

ps. 之前看交易中心的代码,里面就存在同一个调用链中多次使用分布式锁,这就是一个隐患点。

image.png

原因有三:

1、省时省心:只用 SETNX 不够安全;能做到“可用但不严谨”。自研还要补齐一堆细节(SET NX PX + token + Lua 解锁 + 续期/等待/重入/容灾)

2、安全可靠:多年维护、活跃的 issue/PR 与稳定的发布节奏,边角场景被大量用户验证。

3、避免踩坑:避免因个别同学使用姿势不当(开发不规范、技术认知不到位),而造成不必要的问题。

万一万一有一天你的 直属 leader 问你,能有啥问题?你了解 Redisson 细节吗?

image.png

可以从这 3大块 切入:

  1. 分布式锁特性
  2. 锁的特性
  3. Redisson 特性

1、分布式锁特性

如果要设计一个分布式锁,就需要明确分布式锁经常出现哪些问题,以及如何解决。

  • 可用问题:无论何时都要保证锁服务的可用性(这是系统正常执行锁操作的基础)。
  • 死锁问题:客户端一定可以获得锁,即使锁住某个资源的客户端在释放锁之前崩溃或者网络不可达(这是避免死锁的设计原则)。
  • 脑裂问题:集群同步时产生的数据不一致,导致新的进程有可能拿到锁,但之前的进程以为自己还有锁,那么就出现两个进程拿到了同一个锁的问题。

通过 “可用问题、死锁问题、脑裂问题” 来展开回答各分布式锁的实现方案的优缺点和适用场景。

2、锁的特性

同时还需要考虑,锁的四种设计原则:

  • 互斥性:即在分布式系统环境下,对于某一共享资源,需要保证在同一时间只能一个线程或进程对该资源进行操作。
  • 高可用:也就是可靠性,锁服务不能有单点风险,要保证分布式锁系统是集群的,并且某一台机器锁不能提供服务了,其他机器仍然可以提供锁服务。
  • 锁释放:具备锁失效机制,防止死锁。即使出现进程在持有锁的期间崩溃或者解锁失败的情况,也能被动解锁,保证后续其他进程可以获得锁。
  • 可重入:一个节点获取了锁之后,还可以再次获取整个锁资源。

3、Redisson 特性

  • 更安全正确:内置看门狗自动续期、基于唯一 token 的原子加/解锁(Lua 比对),避免 setnx+expire 的非原子、误删他人锁和锁过期并发等坑。
  • 更强能力更省代码:支持 tryLock(等待/租期)、Pub/Sub 阻塞通知、重入/读写/公平锁、信号量/栅栏/MultiLock/RedLock,用现成语义替代自研轮子。
  • 生产级可靠性:原生哨兵/集群/主从支持,连接管理、超时与重试策略完善,故障切换时行为可控,稳定性和性能有成熟实践背书。

Redisson 使用 demo:加锁、解锁。

RLock lock = redisson.getLock("lock:order:123");
try {
  // 不传过期时间 -> 默认30s, 看门狗自动续期
  boolean isLock = lock.tryLock(5, TimeUnit.SECONDS);
  if (!isLock) {
      log.warn("加锁失败。lockInfo:{}", lockInfo);
      throw new LockException("加锁失败");
  }
} finally {
  lock.unlock();
}

一张图来解释加锁

image.png

  1. Redis Cluster 读写操作时,会基于 key(CRC16)计算 slot,得到哪台 master 机子
  2. 执行 Lua 脚本:保证原子操作
  3. 启动看门狗(Watchdog):监控

一张图来解释解锁:执行 lock.unlock(), 就可以释放分布式锁 image.png

  1. 每次都对 myLock 数据结构中的那个加锁次数减1。
  2. 如果发现加锁次数是 0 了, 说明这个客户端已经不再持有锁了, 此时就会用: del myLock 命令, 从 redis 里删除这个 key。
  3. 另外的客户端2 就可以尝试完成加锁了。
  4. 使用 Redis 的 发布订阅功能,通知其他客户端

互斥场景有哪些?

  1. 同个线程,多次 lock.lock() : 不会互斥,因为重入
  2. 不同线程,进行 lock.lock() : 会互斥,互相阻塞
  3. 不同客户端,进行 lock.lock() :会互斥,互相阻塞 image.png

分布式锁存储在 Redis中数据结构是什么?

# Redis 中如下 
172.18.1.23:7004> hgetall myLock 
1) "dfd3aabb-82ce-4c54-966d-5719675a3d62:1" 
2) "1" 

# 实际上就类似 map: 
# "dfd3aabb-82ce-4c54-966d-5719675a3d62:1",代表某客户端 UUID,1 是 threadId 
# 1,代表自增次数 
{ 
  "dfd3aabb-82ce-4c54-966d-5719675a3d62:1": 1 
}

所以能不能重入锁,要看客户端和线程是否一致。