
在分布式集群多台服务器环境下生成 全局唯一 的长整型ID当作订单ID、优惠券ID、业务主键替代数据库自增主键。基于 Redis 实现分布式 ID 生成器类似雪花算法思路时间戳(高位) 序列号(低位)解决集群环境下 ID 重复问题。组成结构long类型一共64位高 32 位时间戳从自定义起始时间到现在的秒数低 32 位Redis自增序列号同一天内从 1 开始递增优点不需要维护数据库Redis自增天然保证分布式唯一按日期做 key方便过期清理 Redis 数据。packagecom.hmdp.utils;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.stereotype.Component;importjava.time.LocalDateTime;importjava.time.ZoneOffset;importjava.time.format.DateTimeFormatter;importjava.util.concurrent.CountDownLatch;importjava.util.concurrent.TimeUnit;ComponentpublicclassRedisIdWorker{// 开始时间戳privatestaticfinallongBEGIN_TIMESTAMP1640995200L;// 序列号的位数privatestaticfinalintCOUNT_BITS32;privateStringRedisTemplatestringRedisTemplate;publicRedisIdWorker(StringRedisTemplatestringRedisTemplate){this.stringRedisTemplatestringRedisTemplate;}publiclongnextId(StringkeyPrefix){// 1.生成时间戳/* LocalDateTime本身不带时区, 底层会读取JVM默认时区, 即操作系统时区。 如果服务器时区设置为 Asia/Shanghai北京时间 UTC8拿到的数字就是北京时间 如果服务器是 UTC 时区拿到的就是 0 时区时间数字就差 8 小时。*/LocalDateTimenowLocalDateTime.now();// 2026-09-16T11:31:43.510longnowSecondnow.toEpochSecond(ZoneOffset.UTC);// 1789558303longtimestampnowSecond-BEGIN_TIMESTAMP;// 1789558303 - 1640995200L 148563103// 2.生成序列号// 2.1.获取当前日期精确到天输出样式2022:01:02Stringdatenow.format(DateTimeFormatter.ofPattern(yyyy:MM:dd));// date 2026:09:16Stringkeyicr:keyPrefix:date;/* 2.2.自增长 opsForValue().increment()Redis的INCR命令key不存在的时候自动初始化为 0然后 1返回新值。 第一次执行这个 key 的 increment返回的 count 1第二次 count2第三次 count3以此类推。 Redis 的 INCR 命令规则 如果 key 不存在先把 key 的值初始化为 0然后执行 1返回新值 1。 如果 key 已存在直接对原有值 1返回增加之后的值。 例子 keyPrefixorderdate2022:01:02 拼接出来 Redis keyicr:order:2022:01:02 程序第一次调用 nextId(order)Redis 里没有 icr:order:2022:01:02 increment0 1 → 返回 1count1 同一秒 / 同一天再次调用 nextId(order)key 已经存在值是 1 increment1 1 → 返回 2count2 第三次调用 → count3 */// increment 为什么不会并发重复 Redis是单线程执行命令INCR是原子命令多请求排队执行。longcountstringRedisTemplate.opsForValue().increment(key);// count 1代表本次是第一次创建这个 key给 key 设置过期时间if(count1){stringRedisTemplate.expire(key,3,TimeUnit.DAYS);}// 3.拼接并返回returntimestampCOUNT_BITS|count;}}nextId()返回的是一个Java long 类型数字64位长整型不是字符串。公式ID (timestamp 32) | count举个实际例子帮你看懂假设timestamp 148563103距离基准时间的秒差本次Redis自增得到count 5timestamp 32把 148563103 左移32位放到long高32位低32位全部变成0| 5低32位填上序列号5计算最终得到ID638073668777279493这就是方法返回的值直接是数字可以存入 MySQL 的BIGINT字段作为主键。二进制视角64位long【高32位timestamp】 【低32位count序列号】 148563103的二进制 00000000 00000000 00000000 00000101多次调用看看返回值变化以同一天、同一业务前缀order举例第1次调用 → count1 → ID (148563103 32) | 1第2次调用 → count2 → ID (148563103 32) | 2同一秒第100次 → count100 → ID (148563103 32) | 100同一秒生成的ID高位完全一样只有低位数字递增过了1秒timestamp变大整个ID会跳到一个更大的数字所以ID大体递增数据库索引友好。不同天的情况到第二天Redis key 变成icr:order:2026:09:17count重新从1开始同时timestamp也变大生成的ID整体一定比昨天所有ID更大。⚠️ 重点注意两个特性返回值是数字不是拼接字符串很多新手误以为是时间戳 序列号拼接成字符串这个是位运算合并成一个64位数字和雪花算法一样。当timestamp过大时long最高位符号位被置1返回值变成负数Java long是有符号。64位第一位是符号位一旦 timestamp 超过 31 位timestamp 32就会触发负数存入数据库BIGINT也会是负数。这就是前面提到的经典坑。测试代码直接打印看效果// 注入RedisIdWorkerRedisIdWorkeridWorker;TestvoidtestId(){longid1idWorker.nextId(order);longid2idWorker.nextId(order);System.out.println(id1 id1);System.out.println(id2 id2);}输出类似id1 638073668777279493 id2 638073668777279494数据库存储MySQL 字段类型要使用BIGINT对应Java long不能用INTINT只有32位存不下。