使用场景示例:业务上希望用户的某个操作是串行的。用户可能并发请求,可以通过对该用户加公共的锁保证同一时刻,只处理一个请求。此时可以使用 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 锁很好用,但要考虑/注意下面一些问题:
- 如果 redis 服务挂了,怎么办?
- 为了防止redis挂掉,可能引入主从机制,当主库挂掉之后,如果主库的一些数据未同步到从库,这会导致防并发失效。能接受这种失效吗?还是引入更复杂的机制保证redis高可用?
- 对于 Java 服务,如果某次GC时间较长,导致程序重新运行时之前的锁已经失效,该怎么办 ?
针对上面的第3点,我们需要做的事情是:
不要用 redis 锁去保证 MySQL 等业务数据存储的数据库的约束(比如唯一性约束),而是数据库本身要加上约束。