Java案例:如何自定义主键生成策略?从原理到实战一篇搞懂
目录导读
- 为什么需要自定义主键生成?
- 主键生成的常见方案对比
- 基于数据库的自定义主键生成实现
- 基于Redis的分布式主键生成实战
- 使用雪花算法(Snowflake)自定义主键
- Spring JPA如何集成自定义主键生成器
- 常见问题与解决方案(Q&A)
为什么需要自定义主键生成?
在Java企业级开发中,主键(Primary Key)是数据表的核心标识,虽然数据库自增ID(如MySQL的AUTO_INCREMENT)简单易用,但在分布式系统、微服务架构或高并发场景下,它存在明显缺陷:

- 跨数据库迁移困难:不同数据库的自增实现不同(如MySQL、Oracle Sequence)。
- 分布式环境冲突:多个应用实例同时插入时,自增ID无法保证全局唯一。
- 业务可读性差:纯数字ID难以体现业务含义,如订单号需要包含时间、区域信息。
- 性能瓶颈:数据库自增锁可能成为高并发下的性能热点。
自定义主键生成策略成为Java开发者的必备技能,它不仅能解决上述问题,还能让主键承载业务特征,比如可反解时间、机器ID等信息。
问答环节
问:为什么不用UUID作为主键?
答:UUID虽是全局唯一,但长度为36位字符,占用存储空间大,且无序插入会导致B+树索引频繁分裂,严重影响写入性能,通常只适合小数据量或非索引场景。
主键生成的常见方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 数据库自增ID | 简单、有序、支持事务 | 分布式不适用、迁移困难 | 单机小系统 |
| UUID/GUID | 全局唯一、无需中心化 | 无序、存储大、查询慢 | 分布式但要求不高 |
| Redis生成ID | 高性能、有序、趋势递增 | 依赖Redis、需考虑持久化 | 高并发、微服务 |
| 雪花算法 | 分布式、高性能、时间有序 | 依赖机器时钟、ID偏长 | 核心业务、支付等 |
| 自定义组合ID | 业务语义强、可反解 | 实现复杂、需考虑冲突与位数 | 订单号、流水号 |
核心观点:没有银弹方案。自定义主键生成需要根据业务场景选择或组合多种策略,比如用雪花算法生成核心ID,再拼接待定业务前缀。
基于数据库的自定义主键生成实现
即使不依赖外部组件,我们也能在数据库层实现灵活的自定义主键。
1 使用数据库Sequence(适用于Oracle、PostgreSQL)
-- 创建序列,步长可自定义 CREATE SEQUENCE order_seq START WITH 1000 INCREMENT BY 1; -- 插入时获取序列值 INSERT INTO orders (id, order_name) VALUES (order_seq.NEXTVAL, 'Order-001');
Java调用示例:
@Repository
public class OrderRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public Long getNextOrderId() {
String sql = "SELECT order_seq.NEXTVAL FROM DUAL";
return jdbcTemplate.queryForObject(sql, Long.class);
}
}
2 基于数据库表的行锁生成ID
适用于MySQL(不支持Sequence)的环境:
CREATE TABLE id_generator (
id BIGINT NOT NULL AUTO_INCREMENT,
stub CHAR(1) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY stub (stub)
);
-- 替换法生成新ID
REPLACE INTO id_generator (stub) VALUES ('a');
SELECT LAST_INSERT_ID();
注意:此方案存在数据库单点瓶颈,建议仅在低并发下使用。
基于Redis的分布式主键生成实战
Redis提供了INCR(原子递增)命令,是构建分布式主键的利器。
1 基础版:纯数字递增
@Component
public class RedisIdGenerator {
@Autowired
private StringRedisTemplate redisTemplate;
// key: 业务前缀,如 "order:id"
private static final String ID_KEY_PREFIX = "id:";
public Long generateId(String businessType) {
String key = ID_KEY_PREFIX + businessType;
return redisTemplate.opsForValue().increment(key);
}
}
2 高可用版:带日期和业务前缀的ID
public String generateBizId(String prefix) {
LocalDate today = LocalDate.now();
String dateStr = today.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String key = "id:" + prefix + ":" + dateStr;
Long seq = redisTemplate.opsForValue().increment(key);
// 设置TTL,避免key无限增长(比如2天后过期)
redisTemplate.expire(key, 2, TimeUnit.DAYS);
// 格式: 前缀+日期+6位序列号(不足补零)
return prefix + dateStr + String.format("%06d", seq);
}
// 示例输出: ORDER20250312000001
注意:Redis方案需考虑RDB/AOF持久化,避免宕机后ID回退,建议使用Redis Cluster保障高可用。
问答环节
问:Redis ID生成在数据迁移或集群故障切换时可能重复吗?
答:理论上可能,Redis RDB快照可能丢失最近的递增步数,导致恢复后重复,解决方案:一是配置AOF持久化;二是在业务层额外增加去重检测(如写入数据库的唯一索引)。
使用雪花算法(Snowflake)自定义主键
雪花算法是Twitter开源的分布式ID生成算法,目前已成为业界标准方案。
1 算法结构
一个64位的long型ID,拆解为:
- 1位:符号位,固定0
- 41位:时间戳(毫秒级,可用69年)
- 10位:工作机器ID(支持1024台机器)
- 12位:序列号(同一毫秒内支持4096个ID)
2 Java实现版本
public class SnowflakeIdWorker {
// 起始时间戳 (设置一个最近时间,避免ID过长)
private final long twepoch = 1700000000000L;
// 机器ID占位
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
private final long sequenceBits = 12L;
private final long workerIdShift = sequenceBits;
private final long datacenterIdShift = sequenceBits + workerIdBits;
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
private long workerId;
private long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) throw new IllegalArgumentException();
if (datacenterId > maxDatacenterId || datacenterId < 0) throw new IllegalArgumentException();
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成ID");
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒序列已满,等待下一毫秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
3 使用示例
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(1, 1);
for (int i = 0; i < 5; i++) {
System.out.println(idWorker.nextId());
}
// 输出: 1893456789012345 (64位整数)
问答环节
问:雪花算法如果机器时钟出现回拨怎么办?
答:高级实现可等待时钟追上后再生成,或记录备用sequence跳过回拨时段,生产环境建议配置NTP同步并容忍秒级回拨,或搭配zookeeper选举主节点保活。
Spring JPA如何集成自定义主键生成器
在Spring Boot + JPA项目中,我们可以通过实现IdentifierGenerator接口,将自定义策略无缝嵌入ORM框架。
1 自定义生成器类
public class CustomIdGenerator implements IdentifierGenerator {
// 注入Redis或雪花算法组件
private RedisIdGenerator redisIdGenerator = SpringContextUtil.getBean(RedisIdGenerator.class);
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object) {
// 生成带业务前缀的ID
return redisIdGenerator.generateBizId("ORDER");
}
}
2 实体类配置
@Entity
@Table(name = "orders")
public class OrderEntity {
@Id
@GenericGenerator(name = "custom_id", strategy = "com.example.generator.CustomIdGenerator")
@GeneratedValue(generator = "custom_id")
private String id; // 主键改为String类型,如 ORDER20250312000001
private String orderName;
// getter/setter省略
}
这样,每次保存实体时,JPA会自动调用我们的自定义主键生成器,无需额外修改代码。
常见问题与解决方案(Q&A)
Q1:自定义主键生成时,如何保证ID不重复?
- 使用Redis的原子操作(INCR)或数据库唯一索引兜底。
- 雪花算法依赖唯一机器ID,可通过zookeeper分配。
- 对于高可靠性要求,可在插入数据库时加
ON DUPLICATE KEY UPDATE或检测UNIQUE异常后重试。
Q2:主键生成器在高并发下性能如何优化?
- 雪花算法本身可每毫秒生成4096个ID,足够大部分场景。
- Redis ID生成器建议使用批量预取(如一次申请100个ID缓存到本地,减少Redis网络IO)。
- 避免在循环中频繁调用生成器,善用批量插入。
Q3:是否需要支持主键反解业务信息?
- 推荐设计,如订单ID包含数据中心+时间戳+序列号,可根据ID快速定位数据中心和生成时间,无需额外查询。
- 反解方法:将long型ID通过位运算拆解为各字段。
Q4:微服务下如何统一主键生成策略?
- 独立一个ID生成微服务(如
id-service),所有服务通过Feign远程调用获取ID(适合中小规模)。 - 或使用开源组件如美团Leaf、百度UidGenerator,它们已内置完美方案。
- 每个微服务单独部署雪花算法实例,通过环境变量配置机器ID。
自定义主键生成是Java分布式开发的关键环节,从简单的数据库序列,到高性能的Redis和雪花算法,开发者需要根据系统的并发量、数据一致性要求、业务语义需求来平衡选择,本文不仅提供了多种方案的代码示例,还展示了如何在Spring JPA中无缝集成,希望通过这篇文章,你能对自定义主键生成有一个全面而深入的理解,从而在实际项目中灵活应用。
推荐阅读
- 源码分析:美团Leaf——分布式ID生成器
- 深入理解:数据库索引与无序ID的存储性能
- 实战提升:Redis哨兵模式下的高可用ID生成