Redis实现并发计数器

Redis 其中的一个应用场景就是计数器。虽然 Redis 本身是单线程命令处理机制,但是代码写不好的话,也会出 Bug

竞态什么时候发生?

多个线程请求同一个资源,如果是读操作的话,就不存在竞态。因为不管怎么读,读到的数据都是一样的。但是在多个线程对同一个资源进行写操作,就有可能发生竞态

总之一句话,读操作不要考虑竞态,写操作才去考虑

一个场景

多个线程对同一个数字进行增加,不限制增加量,直到数字达到一个限制为止

实现

利用 Redis 的自带的 INCRBY 命令实现计数

方案 1

问题

前面说了,竞态发生的情况是在写操作的时候。方案 1 中,写操作发生在执行 SET 命令和 INCR 命令的时候

有可能发生这两种情况:

  1. 线程 A 先执行 INCRBY,然后线程 B 执行 SET,造成数据不一致
  2. 计数器显示最大值为 100。线程 A、B 同时 GET 到当前的数量为 99,没有超过限制。于是继续执行 INCRBY 操作进行加 1,造成计数器的值为 101,超过限制了

由此,可以的使用方案 2

方案 2

改动了两个地方:

  1. 初始化使用 SETNX 代替 SET
  2. 先执行 INCRBY 后才判断是否超限

SETNX 命令只有 key 不存在时才能执行成功。先执行 INCRBY 就避免了两个线程读到同一个值的情况

方案 3

方案 2 还是有个 Bug。如果一次可以加 2 个及以上的数字,当执行 INCRBY 之后发现超过限制了,需要告知计数增加失败,然后还要把值恢复到执行 INCR 操作之前的值。如果一次只能加 1 的话,这个 Bug 就不存在

Bug 出现的原因在于:同一时间有多个线程去操作 Redis。因此不能让所有的线程都同时对 Redis 进行操作,而应该把线程放入一个队列里,按照顺序进行处理。这样的话,可以继续沿用方案 1,但是需要将线程放入队列中。这样就避免了 Bug