使用Redis Cache处理计数器访问竞态条件相关问题咨询
问题解答
认知纠正
你对Redis自增/自减功能的理解存在偏差。Redis的INCR/DECR系列命令就是专门为「业务唯一标识 + 整数计数器」的场景设计的,完全支持你需要的二元结构:直接将你的业务标识(比如代码中的url)作为Redis的key,计数器数值作为该key对应的字符串类型value即可,命令执行时会自动将value解析为整数完成原子增减操作,并非仅在新建key时生效。
Redis内置计数器特性
Redis针对计数器场景的原子操作特性如下:
- 所有
INCR/DECR/INCRBY/DECRBY命令都是原子执行的,即使多个客户端同时操作同一个key,也不会出现竞态条件 - 执行
DECR key时如果对应key不存在,会自动将key的value初始化为0后再执行减1操作,返回最终计算后的数值
适配业务场景的落地方案
你的业务逻辑是:每个url对应初始5次访问额度,每次访问扣减1次,额度大于0时允许访问,不存在的url自动初始化额度。可以直接用以下两种方案实现,完全不需要自行加锁:
方案1:可预枚举所有url的场景
如果业务中用到的url可以提前枚举,直接在Redis中初始化所有url对应的value为5,后续每次请求直接执行DECR your_url:
- 若返回结果 ≥ 0:表示扣减成功,允许访问
- 若返回结果 < 0:表示额度用尽,拒绝访问
方案2:url动态生成的场景
如果url是动态生成的无法预初始化,可以直接在Redis端执行原子Lua脚本实现完整逻辑,整个脚本执行过程是原子的,不会产生竞态问题:
-- 传入参数:KEYS[1] 为当前请求的url local current = redis.call('GET', KEYS[1]) -- key不存在,初始化额度为5,扣减1次后剩余4次,允许访问 if current == false then redis.call('SET', KEYS[1], 4) return 1 end current = tonumber(current) -- 剩余额度大于0,扣减后允许访问 if current > 0 then redis.call('DECR', KEYS[1]) return 1 end -- 额度用尽,拒绝访问 return 0
在Java代码中调用该Lua脚本,根据返回值判断即可:返回1设置allow=true,返回0设置allow=false。
方案优势
和你现有的synchronized+数据库的实现方案对比,Redis方案有以下优势:
- 天然规避竞态:不需要自行实现锁逻辑,Redis原子操作从底层避免了并发问题
- 支持分布式部署:synchronized是进程级锁,多实例部署时会失效,Redis方案只要对接同一个Redis集群,无论部署多少实例逻辑都安全
- 性能大幅提升:Redis内存操作的响应速度远快于数据库读写,高并发场景下表现更稳定
内容的提问来源于stack exchange,提问作者Tony
相关产品推荐
相关产品推荐

