多线程里用Redis搞锁,怎么高并发下还能稳住不乱掉
- 问答
- 2026-01-01 08:19:26
- 3
关于在高并发多线程环境下如何使用Redis实现一个稳定可靠的锁,避免出现混乱,核心思想可以概括为“不仅要能锁得住,还要能解得开,更要防止误伤”,这就像在一个非常拥挤、人人都在抢着通过的门前,你不仅需要一把结实的锁(确保只有一个人能进去),还需要一把绝对不会丢的钥匙(防止拿到锁的人因为意外无法解锁),更需要在拿锁的人突然晕倒(服务崩溃)时,锁能自动打开,避免所有人都被堵死(系统死锁)。
来源参考:《Redis官方文档》关于Distributed Locks的说明,以及社区实践如Redisson客户端的设计思想。
下面我们分点来说具体怎么做:
第一,设置锁的本质:不能只用SET,要用SETNX加过期时间。
最简单的想法是,在Redis里创建一个键(Key)作为锁,比如线程A想操作“用户123的账户”,它就在Redis里设置一个键叫“lock:user123”,如果设置成功,就表示拿到了锁,其他线程再来设置,发现这个键已经存在,就说明锁被别人拿走了,需要等待。
但这里有两个大坑:

- 原子性:如果先检查键是否存在,再设置键,这不是一个原子操作,可能线程A检查发现没有锁,但在它设置之前,线程B也检查发现没有锁,于是两个线程都认为自己拿到了锁,混乱就发生了。设置锁和判断锁必须是一个不可分割的动作,Redis的
SET key value NX命令就是这个原子操作。“NX”意思是“Only set the key if it does not already exist.”,只有键不存在时才会设置成功。 - 死锁:如果线程A拿到锁后,在处理业务逻辑时突然崩溃了,或者所在的服务器宕机了,那么这个锁就会永远留在Redis里,其他所有线程再也无法获得锁,整个系统就“卡死”了。必须给锁设置一个过期时间(TTL),比如10秒钟,这样即使拿锁的线程出问题,锁也会在10秒后自动释放,避免死锁。
设置锁和设置过期时间是两个命令,如果不是原子操作,可能刚执行完SETNX,线程就崩溃了,过期时间还没设上,依然会导致死锁。必须使用一个原子命令同时完成设置值和过期时间,在Redis 2.6.12之后,可以使用 SET key value NX PX milliseconds 命令。SET lock:user123 random_value NX PX 10000,这一步是基石,确保了锁的基本安全。
第二,释放锁的安全:只能释放自己加的锁。
解决了加锁的问题,解锁同样危险,一个常见的错误是:线程A拿到了锁,设置的过期时间是10秒,但A可能因为某些原因(比如进行了一次很慢的数据库查询)执行了15秒,这时,Redis中的锁因为超过了10秒已经自动释放了,线程B趁虚而入,成功拿到了锁,线程A终于执行完了,它开始释放锁,但它释放的竟然是线程B刚刚创建的锁!这样一来,线程C可能又会拿到锁,局面再次失控。
解锁时不能简单地用DEL key命令把锁删掉。必须确保当前线程只能删除自己设置的那个锁,怎么做呢?我们在加锁时,设置的“value”不能是一个固定的值(1”),而应该是一个唯一标识,比如一个随机生成的字符串(UUID)或者当前线程的ID。
解锁时,要先获取这个锁对应的value,判断它是否与当前线程持有的唯一标识相等,只有相等才允许删除。“获取值”和“删除键”又是两个命令,需要保证其原子性,否则可能在获取值之后、删除之前,锁过期了,又被其他线程抢去,再次发生误删。

来源参考:《Redis官方文档》建议使用Lua脚本来保证解锁操作的原子性。
Redis支持Lua脚本,可以确保脚本中的多个命令被连续执行,中间不会被其他命令插入,解锁的正确姿势是执行一个Lua脚本:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
这个脚本的意思是:如果传入的键(KEYS[1],即锁名)的值,等于传入的参数(ARGV[1],即线程的唯一标识),那么就删除这个键;否则,返回0表示删除失败,这样就完美解决了误删别人锁的问题。
第三,应对更复杂的场景:锁的自动续期。
上面提到了线程执行时间可能超过锁过期时间的问题,除了确保解锁安全,我们还可以用一种更积极的方式来解决——锁续期,也就是说,如果线程A发现业务逻辑还没执行完,但锁就快要过期了,它可以主动向Redis发起请求,延长这个锁的过期时间。

这个逻辑如果让业务代码来实现会非常复杂和耦合,业界成熟的Redis客户端(如Java的Redisson)都实现了“看门狗”(Watchdog)机制,简单说就是,在你成功加锁后,客户端会启动一个后台线程,定时(比如在锁过期时间的三分之一时)去检查业务线程是否还在持有锁(即业务没执行完),如果还在持有,就自动重置锁的过期时间,这样,只要业务线程还在正常运行,锁就不会因为超时而释放,除非业务主动释放锁或进程崩溃,这大大提升了锁在长任务中的稳定性。
第四,高并发下的稳定性:避免无休止的竞争。
当锁被释放时,很可能有成百上千个线程同时在等待这个锁,如果锁一释放,所有等待的线程同时向Redis发起加锁请求,会对Redis造成一个巨大的压力冲击,俗称“惊群效应”。
为了缓解这个问题,等待锁的线程不应该立即、不断地重试,更好的做法是引入一个随机的、短暂的等待时间(退避策略),比如等待一段随机时长后再尝试获取锁,这样可以将请求在时间上分散开,避免对Redis的集中轰炸,更高级的客户端还会使用Redis的发布订阅(Pub/Sub)功能,让等待的线程先订阅锁释放的消息,当锁被释放时,Redis会通知所有订阅的线程,它们再起来竞争,这比让线程不停地轮询(不断发请求问“锁释放了吗?”)要高效得多。
在高并发多线程下用Redis稳住锁,关键几步是:
- 加锁原子:用
SET key random_value NX PX timeout一步到位。 - 值需唯一:value用唯一标识,区分不同客户端。
- 解锁安全:用Lua脚本原子地判断并删除,只删自己的锁。
- 考虑续期:对于执行时间不确定的任务,使用“看门狗”机制自动续期。
- 避免惊群:使用退避策略或发布订阅模式,平滑化并发请求。
遵循这些原则,就能在绝大多数高并发场景下,构建一个相对稳定可靠的Redis分布式锁。
本文由芮以莲于2026-01-01发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:http://waw.haoid.cn/wenda/72344.html
