不能直接用incr生成全局唯一有序id,因其仅保证单调递增,缺乏时间信息与结构化特征,且存在重启重复、扩缩容乱序、单点故障及无时钟回拨处理等问题;真正可用的分布式id需融合时间戳、实例标识与序列号,并通过lua脚本保障原子性。

为什么不能直接用 INCR 生成全局唯一有序ID
单纯靠 Redis 的 INCR 只能保证单调递增,但无法携带时间信息,也不具备“毫秒级时间戳 + 实例标识 + 序列号”的结构化特征。一旦服务重启或集群扩缩容,序列可能重复或乱序;更关键的是,INCR 本身不防止单点故障(比如主从切换期间的命令丢失),也缺乏对时钟回拨的兜底处理。
真正可用的分布式 ID 必须同时满足:全局唯一、大致有序、无中心依赖、可读性强。所以得把时间戳(精度到毫秒)、机器/实例标识、本地原子计数器三者拼装,再用 INCR 管理每毫秒内的序列号。
如何用 EVAL 脚本保证时间戳 + 计数器的原子性
Redis 单命令是原子的,但“先取当前时间戳、再查对应 key 的计数、再 INCR”这三步不是。必须用 Lua 脚本把逻辑锁死在服务端执行。常见错误是把时间戳由客户端生成后传入——这会因网络延迟、客户端时钟不准导致重复或跳变。
脚本核心逻辑是:TIME 获取秒+微秒 → 拼出毫秒级时间戳 → 构造 key(如 id:seq:20240521142345)→ INCR 并设过期(避免 key 泛滥)→ 拼装最终 ID。
示例 Lua 脚本(供 redisTemplate.execute() 调用):
local ts = math.floor(tonumber(redis.call('TIME')[1]) * 1000 + tonumber(redis.call('TIME')[2]) / 1000)
local key = 'id:seq:' .. math.floor(ts / 1000)
local seq = redis.call('INCR', key)
redis.call('EXPIRE', key, 86400)
return ts * 10000 + seq
注意:TIME 返回两个整数(秒和微秒),需手动转成毫秒;EXPIRE 必须加,否则每毫秒一个 key 会撑爆内存;返回值中乘以 10000 是预留 4 位序列空间(单毫秒最多 9999 个 ID)。
RedisTemplate 调用 Lua 脚本的几个硬坑
Spring Boot 默认的 RedisTemplate 对 Lua 脚本支持较弱,容易踩坑:
- 脚本里不能用
redis.call('GET', KEYS[1])这类带变量的写法,KEYS 和 ARGV 必须显式传入,否则报NOSCRIPT - Spring 的
execute()方法默认不缓存脚本 SHA1,高频调用会反复传输脚本内容,拖慢性能 —— 必须用execute(RedisScript, keys, args)并提前注册脚本 - 返回值类型默认是
Object,需强转为Long,否则反序列化失败抛ClassCastException - 如果 Redis 是集群模式,所有 KEY 必须落在同一个 slot,否则报
CROSSSLOT错误 —— 所以 key 命名必须带固定哈希标签,例如{id}:seq:20240521142345
如何安全嵌入实例标识避免多节点 ID 冲突
纯时间戳 + 序列号在多实例部署下仍可能冲突(同一毫秒、同一序列号)。解决方案不是加随机数(破坏有序性),而是把实例维度编码进 ID 高位。
推荐做法:启动时从配置或 Consul/Etcd 拉取唯一 workerId(范围 0–1023),与时间戳左移后拼接。例如:
(timestamp - epoch)
其中 epoch 是服务上线时间戳(避免 ID 过长),workerId 占 10 位(支持 1024 个实例),sequence 占 12 位(单毫秒最多 4096 个 ID)。这个结构和 Snowflake 兼容,下游系统识别成本低。
关键点:workerId 不能写死在代码里,也不能用 IP 自动推导(容器环境 IP 不稳定);必须通过外部配置中心下发,并监听变更 —— 否则扩容新实例时 ID 段重叠,后果严重。
Redis 本身不存储 workerId,它只管序列号部分;整个 ID 的组装必须在应用层完成,且确保同一 JVM 内 workerId 全局单例。











