AI摘要
文章对比Redis自增、号段模式、雪花算法三种分布式ID方案:Redis上手快但高并发易成瓶颈;号段模式一次取批号,性能高、最稳,适合分库分表;雪花算法纯内存、无依赖、吞吐量最高,但需解决机器ID分配与时间回拨。选型按并发量级:低并发用Redis,业务系统用号段,高并发用雪花。
作为有着多年经验的 Java 后端开发,在做微服务、分库分表、订单流水、操作日志这些业务时,分布式 ID 是绕不开的基础组件。以前单体应用用数据库自增 ID 就能搞定,但是到了分布式环境,数据库自增 ID 会出现重复、扩展性差、数据量一大就扛不住等问题。
这篇文章我不讲什么“生产突发故障”,只聊平常开发、联调、压测过程中真实遇到的问题、踩过的坑,把雪花算法、号段模式、Redis 实现这三种最常用的分布式 ID 方案,从原理、代码、优缺点、适用场景一次性讲透,你看完就能直接在项目里落地。
一、为什么需要分布式 ID?
平常开发时,我们很容易遇到这些场景:
- 订单表分库分表后,多个库的自增 ID 会重复;
- 多个服务同时生成业务编号(如支付号、退款号、物流号);
- 前端需要唯一标识做幂等、去重;
- 日志、链路追踪需要全局唯一 ID。
一个合格的分布式 ID,一般要满足:
- 全局唯一,不重复;
- 趋势递增,方便数据库索引;
- 高可用,不依赖单点;
- 高性能,能抗住高并发;
- 长度可控,最好是数字或固定长度字符串。
下面分别讲三种最常用的实现。
二、Redis 实现分布式 ID
2.1 实现思路
平常开发里最简单、上手最快的就是 Redis 方案。
核心原理:
- 利用 Redis 单线程原子自增 特性:
INCR/INCRBY; - 保证多服务、多线程调用时不会重复;
- 可以拼接前缀、时间戳,做成有业务意义的 ID。
2.2 简单实现(日常开发最常用)
直接用 RedisTemplate 调用自增命令:
public Long generateId(String key) {
// 原子自增,每次+1
return redisTemplate.opsForValue().increment(key, 1);
}如果你需要带日期的业务号,比如:20250303000012345,可以这样拼:
public String generateOrderId() {
String date = DateTimeFormatter.ofPattern("yyyyMMdd").format(LocalDate.now());
String key = "id_generator:order:" + date;
// 自增
Long seq = redisTemplate.opsForValue().increment(key, 1);
// 设置当天过期
redisTemplate.expire(key, 1, TimeUnit.DAYS);
// 拼接成 8 位日期 + 8 位序列
return date + String.format("%08d", seq);
}2.3 平常开发遇到的问题
- Redis 挂了,ID 生成就不可用
- 本地测试、联调时经常遇到 Redis 重启,接口直接报错。
- 集群环境下不能直接多实例自增
- 如果你用多个 Redis 实例,不能简单各自自增,会重复。
- 高并发下 Redis 会成为瓶颈
- 压测时能明显感觉到,QPS 一高,自增接口 RT 会上升。
2.4 优点 & 缺点
- 优点:实现简单、上手快、适合低并发业务编号;
- 缺点:强依赖 Redis、高并发性能一般、不适合超大规模分布式系统。
三、号段模式(业务系统最稳、最常用)
3.1 为什么要用号段模式?
平常开发中,号段模式是我最推荐的方案,兼顾性能、可靠性、实现难度。
它的思路很简单:
- 不是每次去数据库拿 1 个 ID;
- 而是一次拿一批(比如 1000 个),放到本地内存;
- 用完一批再去数据库取下一批。
3.2 数据库表设计(通用模板)
建一张 ID 生成表:
CREATE TABLE id_generator (
id int primary key auto_increment,
biz_type varchar(32) not null comment '业务类型',
max_id bigint not null default 0 comment '当前最大ID',
step int not null default 1000 comment '每次取多少',
unique key uk_biz_type (biz_type)
);插入一条业务数据:
INSERT INTO id_generator (biz_type, max_id, step)
VALUES ('order', 0, 1000);3.3 核心流程(开发时一定要理解)
- 服务启动,查询
biz_type = 'order',拿到max_id和step; - 计算本地可用段:
start = max_id + 1,end = max_id + step; - 更新数据库:
max_id = end; - 本地从 start 到 end 逐个分配 ID;
- 快用完时,异步提前加载下一个号段。
3.4 平常开发遇到的真实问题
- 号段用完再去加载,会有瞬间性能抖动
- 所以实际开发时,一般号段使用到 10% 或 20% 就提前加载下一段,避免卡顿。
- 服务重启会浪费一段 ID
- 比如号段只用了 100 个就重启了,剩下 900 个直接丢弃,ID 会不连续,但不影响使用。
- 并发更新数据库会出现超发
必须用 CAS 思想更新:
UPDATE id_generator SET max_id = #{newMaxId} WHERE biz_type = #{bizType} AND max_id = #{oldMaxId}这是我在联调时多次踩坑后才固定下来的写法。
3.5 优点 & 缺点
- 优点:性能极高、不依赖中间件、数据库即可、适合分库分表;
- 缺点:实现比 Redis 复杂一点、会有少量 ID 浪费、依赖数据库可用性。
实际公司内部的中间件、通用 ID 生成服务,绝大多数都是基于号段模式。
四、雪花算法 Snowflake(高并发分布式首选)
4.1 算法结构
雪花算法是 Twitter 开源的,一个 64 位 long 型 ID 结构:
- 1 位:符号位,固定 0;
- 41 位:时间戳;
- 10 位:机器 ID(5 位数据中心 + 5 位机器);
- 12 位:序列号,同一毫秒内自增。
最大优势:
- 纯内存生成,不依赖任何中间件;
- 性能极高,单机每秒能生成几十万 ID;
- 趋势递增,对 MySQL 索引非常友好。
4.2 手写一个雪花算法工具类
平常开发我自己封装过一版,稳定用了很多项目:
public class SnowflakeIdGenerator {
// 起始时间戳(2025-01-01)
private static final long EPOCH = 1735689600000L;
// 机器ID 5位 + 数据中心ID 5位
private long workerId;
private long dataCenterId;
private long sequence = 0L;
// 位数
private static final long WORKER_ID_BITS = 5L;
private static final long DATA_CENTER_ID_BITS = 5L;
private static final long SEQUENCE_BITS = 12L;
// 最大值
private static final long MAX_WORKER_ID = (1L << WORKER_ID_BITS) - 1;
private static final long MAX_DATA_CENTER_ID = (1L << DATA_CENTER_ID_BITS) - 1;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long dataCenterId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException("workerId 非法");
}
if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
throw new IllegalArgumentException("dataCenterId 非法");
}
this.workerId = workerId;
this.dataCenterId = dataCenterId;
}
public synchronized long nextId() {
long now = System.currentTimeMillis();
if (now < lastTimestamp) {
throw new RuntimeException("时间回拨,拒绝生成ID");
}
if (now == lastTimestamp) {
sequence = (sequence + 1) & ((1L << SEQUENCE_BITS) - 1);
if (sequence == 0) {
// 同一毫秒序列号用完,等待下一毫秒
now = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = now;
return ((now - EPOCH) << (WORKER_ID_BITS + DATA_CENTER_ID_BITS + SEQUENCE_BITS))
| (dataCenterId << (WORKER_ID_BITS + SEQUENCE_BITS))
| (workerId << SEQUENCE_BITS)
| sequence;
}
private long waitNextMillis(long lastTimestamp) {
long now = System.currentTimeMillis();
while (now <= lastTimestamp) {
now = System.currentTimeMillis();
}
return now;
}
}4.3 平常开发遇到的真实问题
- 机器 ID 如何分配?
- 这是最常见的问题。如果手动配置,多实例部署容易重复;
- 可以用:IP 后 10 位、Docker 容器序号、Redis 自增注册机器 ID。
- 时间回拨问题
- 本地测试、服务器同步时间时,会出现时间往回跳。
简单处理:
- 小幅度回拨:等待时间追上来;
- 大幅度回拨:直接抛异常,避免重复 ID。
- 只能用 69 年
- 因为只有 41 位时间戳,从一个固定起点算,大概能用 69 年。
- 平常开发基本不用考虑,属于工程上可接受的限制。
4.4 优点 & 缺点
- 优点:性能极高、不依赖第三方、趋势递增、long 型存储友好;
- 缺点:依赖机器时间、机器 ID 要规划、强内存生成。
五、三种方案怎么选?(开发直接照这个来)
我在平常做技术选型时,基本按这个逻辑来:
- 简单业务编号、低并发
- 用 Redis 自增,实现最快,成本最低。
- 公司内部通用 ID、分库分表、稳定性优先
- 用 号段模式,最稳、最可控、最适合业务系统。
- 高并发、微服务集群、链路追踪、订单 ID
- 用 雪花算法,性能天花板最高。
六、总结
其实分布式 ID 不算复杂技术,但细节非常多,很多坑都是在平常开发、联调、压测里一点点踩出来的。
- Redis 方案:简单、适合小业务;
- 号段模式:稳定、通用、业务系统首选;
- 雪花算法:高性能、分布式系统首选。
实际开发中,不用追求“最牛逼”,而是和业务量级匹配、团队能维护、出问题能快速定位,就是最合适的方案。