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

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

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

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

原因有三:

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

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

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

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

可以从这 3大块 切入:

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

1、分布式锁特性

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

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

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

2、锁的特性

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

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

3、Redisson 特性

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

Redisson 使用 demo:加锁、解锁。

java 复制代码
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();
}

一张图来解释加锁

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

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

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

互斥场景有哪些?

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

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

shell 复制代码
# 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 
}

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

相关推荐
青云计划11 小时前
知光项目知文发布模块
java·后端·spring·mybatis
Victor35612 小时前
MongoDB(9)什么是MongoDB的副本集(Replica Set)?
后端
Victor35612 小时前
MongoDB(8)什么是聚合(Aggregation)?
后端
yeyeye11113 小时前
Spring Cloud Data Flow 简介
后端·spring·spring cloud
Tony Bai14 小时前
告别 Flaky Tests:Go 官方拟引入 testing/nettest,重塑内存网络测试标准
开发语言·网络·后端·golang·php
+VX:Fegn089514 小时前
计算机毕业设计|基于springboot + vue鲜花商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
程序猿阿伟14 小时前
《GraphQL批处理与全局缓存共享的底层逻辑》
后端·缓存·graphql
小小张说故事14 小时前
SQLAlchemy 技术入门指南
后端·python
识君啊15 小时前
SpringBoot 事务管理解析 - @Transactional 的正确用法与常见坑
java·数据库·spring boot·后端
学到头秃的suhian16 小时前
Redis缓存
数据库·redis·缓存