需求:两个客户端同时对[key1]执行自增操作,不会相互影响
操作:下面两个客户端并发操作会导致[key1]输出结果与预期不一致
- [客户端一]读取[key1],值为[1]
- [客户端二]读取[key1],值为[1]
- [客户端一]将[key1]自增1,值为[2]
- [客户端二]将[key1]自增1,值为[2]
- [客户端一]输出[key1],值为[2]
- [客户端二]输出[key2],值为[2]
解决思路
- [客户端一]、[客户端二]的R(读)、M(自增)、W(写)三个操作作为一个原子操作执行
- [客户端]对RMW整个操作过程加锁,加锁期间其它客户端不能对[key1]执行写操作
- Lua脚本
思路一:单命令操作
1. 概念
- Redis 提供了 INCR/DECR/SETNX 命令,把RMW三个操作转变为一个原子操作
- Redis 是使用单线程串行处理客户端的请求来操作命令,所以当 Redis 执行某个命令操作时,其他命令是无法执行的,这相当于命令操作是互斥执行的
思路二:加锁
1. 概念
加锁主要是将多客户端线程调用相同业务方法转换为串行化处理,比如多个客户端调用同一个方法对某个键自增(这里不考虑其它方法或业务会对该键同时执行自增操作)
- 调用SETNX命令对某个键进行加锁(如果获取锁则执行后续RMW操作,否则直接返回未获取锁提示)
- 执行RMW业务操作
- 调用DEL命令删除锁
2. 加锁风险一
- 假如某个客户端在执行了SETNX命令加锁之后,在后面操作业务逻辑时发生了异常,没有执行 DEL 命令释放锁。
- 该锁就会一直被这个客户端持有,其它客户端无法拿到锁,导致其它客户端无法执行后续操作。
解决思路:给锁变量设置一个过期时间,到期自动释放锁
SET key value [EX seconds | PX milliseconds] [NX]
3. 加锁风险二
如果客户端 A 执行了 SETNX 命令加锁后,客户端 B 执行 DEL 命令释放锁,此时,客户端 A 的锁就被误释放了。如果客户端 C 正好也在申请加锁,则可以成功获得锁。
解决思路:加锁操作时给每个客户端设置一个唯一值(比如UUID),唯一值可以用来标识当前操作的客户端。
在释放锁操作时,客户端判断当前锁变量的值是否和唯一标识相等,只有在相等的情况下,才能释放锁。(同一客户端线程中加锁、释放锁)
SET lock_key unique_value NX PX 10000
思路三:Lua脚本
1. 概念
多个操作写到一个 Lua 脚本中(Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了 Lua 脚本中操作的原子性)
2. 需求
限制所有客户端在一定时间范围内对某个方法(键)的访问次数。客户端 IP 作为 key,某个方法(键)的访问次数作为 value
3. 脚本
local current current = redis.call("incr",KEYS[1])
if tonumber(current) == 1
then redis.call("expire",KEYS[1],60)
end
4. 调用执行
redis-cli --eval lua.script keys , args