Redis 其中的一个应用场景就是计数器。虽然 Redis 本身是单线程命令处理机制,但是代码写不好的话,也会出 Bug
竞态什么时候发生?
多个线程请求同一个资源,如果是读操作的话,就不存在竞态。因为不管怎么读,读到的数据都是一样的。但是在多个线程对同一个资源进行写操作,就有可能发生竞态
总之一句话,读操作不要考虑竞态,写操作才去考虑
一个场景
多个线程对同一个数字进行增加,不限制增加量,直到数字达到一个限制为止
实现
利用 Redis 的自带的 INCRBY
命令实现计数
方案 1
问题
前面说了,竞态发生的情况是在写操作的时候。方案 1 中,写操作发生在执行 SET 命令和 INCR 命令的时候
有可能发生这两种情况:
- 线程 A 先执行
INCRBY
,然后线程 B 执行SET
,造成数据不一致 - 计数器显示最大值为 100。线程 A、B 同时
GET
到当前的数量为 99,没有超过限制。于是继续执行INCRBY
操作进行加 1,造成计数器的值为 101,超过限制了
由此,可以的使用方案 2
方案 2
改动了两个地方:
- 初始化使用
SETNX
代替SET
- 先执行
INCRBY
后才判断是否超限
SETNX
命令只有 key 不存在时才能执行成功。先执行 INCRBY
就避免了两个线程读到同一个值的情况
方案 3
方案 2 还是有个 Bug。如果一次可以加 2 个及以上的数字,当执行 INCRBY
之后发现超过限制了,需要告知计数增加失败,然后还要把值恢复到执行 INCR
操作之前的值。如果一次只能加 1 的话,这个 Bug 就不存在
Bug 出现的原因在于:同一时间有多个线程去操作 Redis。因此不能让所有的线程都同时对 Redis 进行操作,而应该把线程放入一个队列里,按照顺序进行处理。这样的话,可以继续沿用方案 1,但是需要将线程放入队列中。这样就避免了 Bug