当前位置:首页 > 问答 > 正文

Redis里那个key到底怎么锁又怎么解,搞清楚这事儿不难但挺关键

关于Redis里怎么锁和怎么解,这事儿说白了就是防止“撞车”,想象一下,一个仓库里有个宝贝,好几个人都想同时进去拿,如果没个规矩,大家一窝蜂冲进去,肯定乱套,甚至把宝贝弄坏,Redis的锁就是这个规矩,保证同一时间只有一个人能进去操作。

这个规矩,在Redis里最常用的实现方式,就是用一个特殊的key来代表一把锁,这个key存在,就表示锁已经被别人拿走了;key不存在,就表示锁是空闲的,你可以去拿。

第一部分:怎么锁?—— 安全地拿到钥匙

锁的操作,核心命令是 SET key value NX PX 30000,我们来拆开看这个命令,这是关键中的关键(来源:Redis官方文档对SET命令参数的说明)。

  • key:这就是锁的名字,比如你想锁住用户123的账户,防止同时扣款,锁的key可以叫 "lock:user:123",取个清晰的名字很重要。
  • value:这是锁的值。这是个非常关键的细节,但容易被忽略。 你不能随便设个值,比如都设成“1”,因为当你解锁的时候,你需要证明“这个锁是我上的”,这个value必须是一个全局唯一的值,比如一个随机的长字符串,或者一个UUID(通用唯一识别码),这样,只有持有这个唯一凭证的人,才有资格解锁。
  • NX:意思是“只有在这个key不存在的时候,才设置它”,这就是锁的核心机制,如果key已经存在(锁被别人占了),那么这次SET操作就会失败,返回nil,告诉你没拿到锁。
  • PX 30000:这是给锁设置一个过期时间,比如30000毫秒(30秒)。这可能是第二重要的点。 为什么要设过期时间?想象一下,如果拿到锁的程序(比如一个Java应用)在执行业务逻辑时突然崩溃了,没来得及释放锁,那么这个锁就会永远存在,其他所有进程就再也拿不到锁了,这就是“死锁”,设置了过期时间,即使程序崩溃,锁也会在30秒后自动消失,避免了死锁的发生。

一个完整的加锁过程是这样的:你的程序在要操作共享资源前,向Redis发送 SET lock:user:123 随机唯一字符串 NX PX 30000 命令,如果Redis返回OK,恭喜你,你成功拿到了锁,可以放心进去操作了,如果返回nil,说明锁已被占用,你需要等待或者直接放弃。

Redis里那个key到底怎么锁又怎么解,搞清楚这事儿不难但挺关键

第二部分:怎么解?—— 安全地归还钥匙

解锁听起来简单,不就是把那个key删掉吗?用 DEL lock:user:123 不就行了?不行,这样会出大问题。

问题在于,你不能删别人加的锁,举个例子:

  1. 进程A拿到了锁,设置的过期时间是30秒。
  2. 进程A开始处理业务,但这个业务比较复杂,花了35秒还没做完。
  3. 因为30秒过期时间到了,Redis自动把进程A的锁给删了。
  4. 这时,一直等在旁边的进程B一看锁没了,立刻成功加锁。
  5. 进程B刚开始操作,进程A的业务终于做完了,然后它执行了 DEL 命令。
  6. 结果就是,进程A错误地删除了进程B刚加的锁!这下子,进程C也能进来插一脚,整个秩序就乱套了。

解锁不能简单粗暴地删除key。正确的解锁姿势是一个原子操作:先比对自己是不是锁的主人,如果是,再删除。(来源:Redis官方建议使用Lua脚本保证原子性)。

Redis里那个key到底怎么锁又怎么解,搞清楚这事儿不难但挺关键

在Redis里,我们可以用Lua脚本来实现这个原子操作,因为Lua脚本在执行时是不会被其他命令打断的,脚本内容大致如下:

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

这个脚本的意思是:

  • KEYS[1] 就是锁的key,"lock:user:123"
  • ARGV[1] 就是你当初加锁时设置的那个唯一随机值
  • 脚本会先检查当前锁的值,是否和你传过来的唯一值相等。
  • 如果相等,说明你就是锁的主人,然后执行删除操作。
  • 如果不相等,说明你早已不是锁的主人了(可能因为超时被其他人抢占了),那就什么都不做。

通过这个方式,解锁操作就变得安全了,绝不会误删别人的锁。

第三部分:实践中还要注意啥?

Redis里那个key到底怎么锁又怎么解,搞清楚这事儿不难但挺关键

搞清楚了基本的锁和解锁,还有一些现实问题要考虑。

  1. 锁的超时时间设多长? 这是个难题,设短了,业务没处理完锁就释放了,会导致多个进程同时进入临界区,数据可能出错,设长了,万一持有锁的进程挂了,其他进程要等很久才能继续,通常的做法是,这个时间要设置得比绝大多数业务执行时间都要长一些,并且最好有个监控机制,对于执行时间过长的任务给出告警。

  2. 加锁失败怎么办? 如果没拿到锁,常见的策略有:

    • 直接返回错误:告诉用户“系统繁忙,请稍后再试”。
    • 轮询重试:隔一段时间(比如100毫秒)再试一次,直到拿到锁或超过重试次数,但要注意,如果很多人重试,会给Redis造成压力。
    • 使用阻塞队列:更高级的做法是,把请求排队,让拿到锁的进程处理完后通知下一个,但这需要更复杂的机制。
  3. 这还不是万无一失的:上面说的这种锁,在Redis单机模式下是没问题的,但如果Redis是集群模式,主从节点之间数据同步有延迟,可能会在极端情况下出现两个客户端同时认为自己持有锁的情况,如果你需要应对这种极端场景,可能需要更复杂的算法,比如Redlock,但那又复杂得多,对于绝大多数应用场景,我们上面讨论的单Redis实例锁已经足够可靠。

Redis锁的核心就两点:SET NX PX 安全地加锁(带上唯一值和过期时间),用比对value再删除的原子脚本安全地解锁,搞清楚这个流程,你就能在需要协调多个进程或服务时,有效地避免数据混乱,保证业务的正确性,这事儿确实不难,但每个细节都挺关键,马虎不得。