生成全局唯一的 `long` 类型 ID
- 在分布式集群(多台服务器)环境下,生成 全局唯一 的长整型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_TIMESTAMP=1640995200L;// 序列号的位数privatestaticfinalintCOUNT_BITS=32;privateStringRedisTemplatestringRedisTemplate;publicRedisIdWorker(StringRedisTemplatestringRedisTemplate){this.stringRedisTemplate=stringRedisTemplate;}publiclongnextId(StringkeyPrefix){// 1.生成时间戳/* LocalDateTime本身不带时区, 底层会读取JVM默认时区, 即操作系统时区。 如果服务器时区设置为 `Asia/Shanghai`(北京时间 UTC+8),拿到的数字就是北京时间; 如果服务器是 UTC 时区,拿到的就是 0 时区时间,数字就差 8 小时。*/LocalDateTimenow=LocalDateTime.now();// 2026-09-16T11:31:43.510longnowSecond=now.toEpochSecond(ZoneOffset.UTC);// 1789558303longtimestamp=nowSecond-BEGIN_TIMESTAMP;// 1789558303 - 1640995200L = 148563103// 2.生成序列号// 2.1.获取当前日期,精确到天(输出样式:2022:01:02)Stringdate=now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));// date = 2026:09:16Stringkey="icr:"+keyPrefix+":"+date;/* 2.2.自增长 opsForValue().increment():Redis的INCR命令,key不存在的时候,自动初始化为 0,然后 + 1,返回新值。 第一次执行这个 key 的 increment,返回的 count = 1;第二次 count=2;第三次 count=3,以此类推。 Redis 的 `INCR` 命令规则: 如果 key 不存在,先把 key 的值初始化为 `0`,然后执行 +1,返回新值 `1`。 如果 key 已存在,直接对原有值 +1,返回增加之后的值。 例子: keyPrefix="order",date="2022:01:02" 拼接出来 Redis key:`icr:order:2022:01:02` 程序第一次调用 `nextId("order")`,Redis 里没有 `icr:order:2022:01:02`, increment:0 +1 → 返回 1,count=1 同一秒 / 同一天,再次调用 nextId("order"),key 已经存在,值是 1 increment:1 +1 → 返回 2,count=2 第三次调用 → count=3 */// increment 为什么不会并发重复? Redis是单线程执行命令,INCR是原子命令,多请求排队执行。longcount=stringRedisTemplate.opsForValue().increment(key);// count == 1,代表,本次是第一次创建这个 key,给 key 设置过期时间if(count==1){stringRedisTemplate.expire(key,3,TimeUnit.DAYS);}// 3.拼接并返回returntimestamp<<COUNT_BITS|count;}}nextId()返回的是一个Java long 类型数字(64位长整型),不是字符串。
公式:
ID = (timestamp << 32) | count举个实际例子帮你看懂
假设:
timestamp = 148563103(距离基准时间的秒差)- 本次Redis自增得到
count = 5
timestamp << 32:把 148563103 左移32位,放到long高32位,低32位全部变成0| 5:低32位填上序列号5
计算:
最终得到ID:638073668777279493
这就是方法返回的值,直接是数字,可以存入 MySQL 的
BIGINT字段作为主键。
二进制视角(64位long)
【高32位:timestamp】 【低32位:count序列号】 148563103的二进制 00000000 00000000 00000000 00000101多次调用,看看返回值变化
以同一天、同一业务前缀order举例:
- 第1次调用 → count=1 → ID =
(148563103 <<32) | 1 - 第2次调用 → count=2 → ID =
(148563103 <<32) | 2 - 同一秒,第100次 → count=100 → ID =
(148563103 <<32) | 100
👉同一秒生成的ID:高位完全一样,只有低位数字递增
👉过了1秒,timestamp变大,整个ID会跳到一个更大的数字
所以ID大体递增,数据库索引友好。
不同天的情况
到第二天,Redis key 变成icr:order:2026:09:17,count重新从1开始;
同时timestamp也变大,生成的ID整体一定比昨天所有ID更大。
⚠️ 重点注意两个特性
返回值是数字,不是拼接字符串!
很多新手误以为是时间戳 + 序列号拼接成字符串,这个是位运算合并成一个64位数字,和雪花算法一样。当
timestamp过大时,long最高位(符号位)被置1,返回值变成负数
Java long是有符号。64位第一位是符号位,一旦 timestamp 超过 31 位,
timestamp << 32就会触发负数,存入数据库BIGINT也会是负数。
这就是前面提到的经典坑。
测试代码,直接打印看效果
// 注入RedisIdWorkerRedisIdWorkeridWorker;@TestvoidtestId(){longid1=idWorker.nextId("order");longid2=idWorker.nextId("order");System.out.println("id1 = "+id1);System.out.println("id2 = "+id2);}输出类似:
id1 = 638073668777279493 id2 = 638073668777279494数据库存储
MySQL 字段类型要使用:BIGINT(对应Java long),不能用INT,INT只有32位存不下。