Redis 锁


#Redis#


使用场景示例:业务上希望用户的某个操作是串行的。用户可能并发请求,可以通过对该用户加公共的锁保证同一时刻,只处理一个请求。此时可以使用 redis 的锁。(在分布式场景下,可以叫做「基于 redis 的分布式锁」)。

使用 setnx 创建排他锁

setnx 指令语法:

setnx key value

setnx 会返回1或者0,1代表成功,0为失败。

nx 是 not exists 的缩写,意思是,key 不存在时则set。

实际使用时,value的值可以随便设置。

使用 redis-cli 连接同一个 redis 服务,操作记录如下(代表无操作):

说明 redis会话1 redis会话2 结果
对 aaa 加锁成功 setnx aaa bbb -- (integer) 1
对 aaa 加锁失败 -- setnx aaa bbb (integer) 0
加锁,无法重入 setnx aaa bbb -- (integer) 0
获取 aaa 对应的 value get aaa -- "bbb"
解锁 del aaa -- (integer) 1

注意,对于会话1加的锁,会话2也可以通过 del 对其解锁。正常业务流程中,只有加锁成功,才去解锁

引入过期机制

在需要防止并发的业务代码中,先加锁,再进行业务处理,处理完后,解锁。如果因为一些原因没有走到解锁这一步,或者解锁时出现异常导致失败。那么锁一致存在,下一次加锁便无法成功。一个优化方案是引入过期机制,即在 setnx 之后,使用 expire 指令为其设置过期时间。

过期时间的作用:作为解锁失败的兜底。

过期时间的注意事项:过期时间必须大于正常加锁解锁之间的业务处理时间。

示例:

说明 redis会话1 redis会话2 结果
对 aaa 加锁成功 setnx aaa bbb -- (integer) 1
设置超时时间为 120 s expire aaa 120 -- (integer) 1
在未过期时,尝试加锁会失败 -- setnx aaa bbb (integer) 0
过期后,数据消失,锁也不存在了 get aaa -- (nil)
尝试加锁成功 -- setnx aaa bbb (integer) 1

加锁和过期要保证原子性

这需要引入 redis 事务,或 lua 脚本。暂不说明。

使用 set ex nx 创建排他锁

ex 用于设置过期时间,单位是秒,nx 代表仅在 key 不存在时才 set。

例如对 aaa 锁 10 秒:

set aaa bbb ex 10 nx

示例:

说明 redis会话1 redis会话2 结果
对 aaa 加锁成功 set aaa bbb ex 10 nx -- OK
在未过期时,尝试加锁会失败 -- set aaa bbb ex 10 nx (nil)
过期后,数据消失,锁也不存在了 get aaa -- (nil)
尝试加锁成功 -- set aaa bbb ex 10 nx OK

在需要防止并发的业务流程中,加锁后,进行业务处理,之后必须用 del 解锁。超时时间只是 del 的兜底。

使用 redis 锁的注意事项

redis 锁很好用,但要考虑/注意下面一些问题:

  1. 如果 redis 服务挂了,怎么办?
  2. 为了防止redis挂掉,可能引入主从机制,当主库挂掉之后,如果主库的一些数据未同步到从库,这会导致防并发失效。能接受这种失效吗?还是引入更复杂的机制保证redis高可用?
  3. 对于 Java 服务,如果某次GC时间较长,导致程序重新运行时之前的锁已经失效,该怎么办 ?

针对上面的第3点,我们需要做的事情是:

不要用 redis 锁去保证 MySQL 等业务数据存储的数据库的约束(比如唯一性约束),而是数据库本身要加上约束。


( 本文完 )