不能直接用incr完事,因其不支持带前缀、定长、分段、超限判断等业务需求;多步逻辑(查-判-增)非原子,须用lua脚本封装为单次原子操作,确保并发安全。

为什么不能直接用 INCR 就完事?
单靠 INCR 确实能原子递增,但分布式自增 ID 通常要带业务前缀、固定长度、支持重置或分段(比如每台服务分一段号段),这时纯 INCR 不够用。更关键的是:如果业务逻辑需要「先查当前值 → 判断是否超限 → 再递增」,这三步拆开就不是原子的,Lua 脚本正是为把这类多步逻辑锁进一次 Redis 执行而存在的。
EVAL 脚本里怎么安全生成带前缀的自增 ID?
核心是把「读、判断、写」全塞进 Lua,由 Redis 单线程保证原子性。例如生成形如 order_00001234 的 ID:
local key = KEYS[1]
local prefix = ARGV[1]
local width = tonumber(ARGV[2])
local max_val = tonumber(ARGV[3])
<p>local current = redis.call('INCR', key)
if current > max_val then
redis.call('DECR', key) -- 回滚,避免溢出
return -1
end
return prefix .. string.format('%0'..width..'d', current)</p>
调用时:redis-cli --eval idgen.lua order_counter , order_ 8 99999999
-
KEYS[1]必须是键名,不能拼接在脚本里(否则无法被 Redis 集群识别) -
string.format在 Redis 内置 Lua 中可用,但不支持%x或浮点格式 - 别用
redis.call('GET', key)再INCR—— 这会漏掉并发递增,必须用INCR获取并更新一步到位
如何防止脚本执行超时导致 ID 重复或跳号?
Redis 默认 lua-time-limit 是 5 秒,超时后脚本被 kill,但已执行的 INCR 不会回滚 —— 这就是跳号根源。应对方式只有两个:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 脚本内避免循环、字符串拼接过大、或调用耗时命令(如
KEYS、SCAN) - 业务层加兜底校验:生成 ID 后,用
EXISTS检查对应 ID 是否已被占用(适用于 ID 映射到实际资源的场景) - 绝不依赖
redis.call('TIME')做逻辑分支 —— 它返回秒级时间戳,精度不够且不可靠
集群模式下 EVAL 报 CROSSSLOT 错误怎么办?
Redis Cluster 要求所有 KEY 必须落在同一个 slot,而 EVAL 脚本里多个 KEYS 如果哈希后 slot 不同,就会触发 CROSSSLOT Keys in request don't hash to the same slot 错误。
解决办法只有一条:所有涉及的 key 必须用相同哈希标签,例如都写成 {order}_counter 和 {order}_last_used —— 大括号内的内容决定 slot 分配,这样它们就一定路由到同一节点。
如果确实需要跨 slot 操作(比如同时更新用户计数器和全局流水号),只能放弃 Lua 原子性,改用客户端加锁(如 SET key val NX EX 10)+ 重试,或者接受最终一致性。
真正难的不是写对脚本,而是想清楚:这个 ID 是否真的需要全局严格有序?多数场景下,用 INCR + 时间戳 + 实例标识组成的雪花风格 ID,比强求 Lua 全局自增更健壮、更易水平扩展。










