在构建高并发系统的时候,计数类需求几乎无处不在,比如接口每分钟调用次数、商品库存扣减、活动参与人数统计等。Redis提供的INCR命令是最常被使用的原子递增手段,它能够在分布式多进程环境下保证计数准确无误,而不依赖外部锁机制。理解它的底层运行方式,有助于我们在设计时做出更合理的方案选择。

INCR的底层执行机制与单线程模型
Redis本身采用单线程处理命令的核心逻辑,所有客户端的请求都会进入一个统一的事件循环队列,按顺序被读取和执行。当我们发送INCR命令时,服务端不会把读取、加一、写回拆成多个步骤暴露给客户端,而是作为一个整体指令在内部完成。这意味着哪怕有一万个连接同时到达,它们也会在队列中排队,前一个请求的INCR完全结束之后,后一个才会开始,从根源上排除了多线程同时修改同一key的可能。
从源码层面看,INCR属于对字符串对象的特殊操作。如果key不存在,Redis会先创建值为0的字符串,再执行加一;如果已存在且能解析为整数,则直接递增并写回;若值不是整数格式,则返回错误。因为整个流程发生在同一个命令的调用栈内,没有任何让出CPU或等待IO的间隙,所以具备天然的原子性。下面的代码展示了用Python客户端调用INCR的基础写法:
import redis
client = redis.StrictRedis(host='127.0.0.1', port=6379, db=0)
# 对 article:view:1001 做原子递增
new_val = client.incr('article:view:1001')
print('当前浏览量:', new_val)
与关系型数据库中先SELECT再UPDATE的方式相比,INCR把两步合成一步,并且避免了两步之间被其他事务插入的风险。数据库方案若不使用悲观锁或乐观锁,就容易出现超卖;而Redis的INCR由于服务端串行化执行,客户端完全不需要关心并发细节。当然,单线程模型也意味着耗时命令会阻塞其他请求,因此不应在INCR之后紧跟重计算逻辑。
对比数据库自增与分布式锁的方案差异
很多团队在初期会用数据库的AUTO_INCREMENT字段或者先读后写来做计数,这在低并发下没有问题,但流量上来后就会暴露出明显短板。数据库的事务隔离级别如果不够高,两个事务同时读到同一数值并加一,落库时就会丢失一次更新。即便加上UPDATE table SET count=count+1 WHERE id=1这样的原子SQL,高频率写入也会因为行锁争用导致吞吐下降,且每一次写都涉及磁盘刷盘或缓冲池同步,延迟远高于内存操作。
另一种常见思路是使用分布式锁,比如基于Redis的SETNX或者Redlock,在修改计数前先抢锁,操作完再释放。这种做法虽然也能保证一致,但引入了锁超时、死锁、羊群效应等复杂度。一旦客户端崩溃未及时解锁,计数就可能被冻结一段时间。而直接用INCR则完全规避了锁的生命周期管理,因为它本身就是服务端原子指令,不需要客户端协商。下面的伪代码对比了两种写法:
// 使用分布式锁的繁琐写法
if (redis.setnx('lock:count', '1') == 1) {
int val = Integer.parseInt(redis.get('count'));
redis.set('count', String.valueOf(val + 1));
redis.del('lock:count');
}
// 直接使用INCR的简洁写法
long newCount = redis.incr('count');
从可用性和可维护性来看,INCR更适合纯计数场景,而分布式锁适合需要组合多个资源操作的业务。如果计数只是整个流程中的一个环节,且必须和扣库存、写日志放在同一事务边界,那么可以考虑用Lua脚本把INCR和其他命令打包,既保留原子性又减少网络交互。我们在设计时应当先问自己:这个计数是否独立?如果独立,INCR就是最低成本的正确解。
INCR在限流与库存扣减中的实战用法
固定窗口限流是INCR最直观的应用。每一个时间窗口用一个key,首次访问时INCR并返回1,同时设置过期时间为窗口长度;后续请求INCR后拿到的数值若超过阈值就拒绝服务。由于INCR和EXPIRE是两个命令,极端情况下进程在INCR后崩溃可能导致key永不过期,因此更稳妥的是用Lua将两者合并,或者使用支持同时设过期参数的客户端封装。示例如下:
-- 原子限流脚本
local current
current = redis.call('incr', KEYS[1])
if tonumber(current) == 1 then
redis.call('expire', KEYS[1], ARGV[1])
end
if tonumber(current) > tonumber(ARGV[2]) then
return 0
end
return 1
在秒杀库存扣减中,我们可以把商品ID作为key,用INCRBY negative值来扣减,或者先INCR再判断。但更推荐用DECR返回剩余量,当返回值小于0时说明超卖,此时再INCR回滚。这样即使有瞬间洪峰,Redis内存中的数字也不会出现并发错乱,每一个成功扣减的请求都对应一次真实的计数下降。配合异步消息队列把订单落库,就能做到前端计数绝对精准、后端处理平滑。
需要提醒的是,INCR只是保证单个key的递增原子,不提供跨key事务。如果你需要同时扣减A商品库存并增加B商品销量,就必须用Lua脚本或Redis事务(MULTI/EXEC)把它们绑在一起。另外,当Redis启用持久化但采用异步落盘时,极端宕机可能丢失最后几秒的计数,对精度要求极高的场景应开启AOF且配置为everysec或always,并辅以数据库对账任务。只有这样,INCR带来的高性能才不会以不可接受的数据丢失为代价。