Java案例如何自定义主键生成?

wen java案例 54

Java案例:如何自定义主键生成策略?从原理到实战一篇搞懂

目录导读

  1. 为什么需要自定义主键生成?
  2. 主键生成的常见方案对比
  3. 基于数据库的自定义主键生成实现
  4. 基于Redis的分布式主键生成实战
  5. 使用雪花算法(Snowflake)自定义主键
  6. Spring JPA如何集成自定义主键生成器
  7. 常见问题与解决方案(Q&A)

为什么需要自定义主键生成?

在Java企业级开发中,主键(Primary Key)是数据表的核心标识,虽然数据库自增ID(如MySQL的AUTO_INCREMENT)简单易用,但在分布式系统、微服务架构或高并发场景下,它存在明显缺陷:

Java案例如何自定义主键生成?

  • 跨数据库迁移困难:不同数据库的自增实现不同(如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生成

抱歉,评论功能暂时关闭!