1. 问题描述
并发竞争key这个问题简单讲就是:
同时有多个客户端去set一个key。
示例场景 1
例如有多个请求一起去对某个商品减库存,通常操作流程是:
- 取出当前库存值
- 计算新库存值
- 写入新库存值
假设当前库存值为 20
,现在有2个连接都要减 5
,结果库存值应该是 10
才对,但存在下面这种情况:
示例场景 2
比如有3个请求有序的修改某个key,按正常顺序的话,数据版本应该是 1->2->3
,最后应该是 3
。
但如果第二个请求由于网络原因迟到了,数据版本就变为了 1->3->2
,最后值为 2
,出问题了。
2. 解决方案
2.1 乐观锁
乐观锁适用于大家一起抢着改同一个key,对修改顺序没有要求的场景。
watch
命令可以方便的实现乐观锁。
需要注意的是,如果你的 redis 使用了数据分片的方式,那么这个方法就不适用了。
watch
命令会监视给定的每一个key,当 exec
时如果监视的任一个key自从调用watch后发生过变化,则整个事务会回滚,不执行任何动作。
2.2 分布式锁
适合分布式环境,不用关心 redis 是否为分片集群模式。
在业务层进行控制,操作 redis 之前,先去申请一个分布式锁,拿到锁的才能操作。
分布式锁的实现方式很多,比如 ZooKeeper、Redis 等。
2.3 时间戳
适合有序需求场景,例如 A
需要把 key 设置为 a
,然后 B
设置为 b
,C
再设置为 c
,最后的值应该是 c
。
这时就可以考虑使用时间戳的方式:
A => set key1 {a 11:01}
B => set key1 {b 11:02}
C => set key1 {c 11:03}
就是在写入时保存一个时间戳,写入前先比较自己的时间戳是不是早于现有记录的时间戳,如果早于,就不写入了。
假设 B 先执行了,key1 的值为 {b 11:02}
,当A执行时,发现自己的时间戳11:01
早于现有值,就不执行 set 操作了。
2.4 消息队列
在并发量很大的情况下,可以通过消息队列进行串行化处理。这在高并发场景中是一种很常见的解决方案。
3. 小结
“Redis 并发竞争” 问题就是高并发写同一个key时导致的值错误。
常用的解决方法:
- 乐观锁,注意不要在分片集群中使用
- 分布式锁,适合分布式系统环境
- 时间戳,适合有序场景
- 消息队列,串行化处理