如何重写唯一ID生成逻辑?自定义ID生成器实现遇阻
嘿,我来帮你搞定这个问题!刚好之前在项目里做过类似的自定义ID生成逻辑,针对你遇到的两个痛点——FooRepository实例为null、不想用“查最大ID转BigInteger”的笨办法,给你几个实用又优雅的方案:
一、先搞定FooRepository为null的问题(如果一定要用Repository的话)
大概率是你的FooGenerator没有被Spring容器托管,而是手动new出来的,导致依赖注入失效。如果是Hibernate自定义ID生成器的场景,可以通过实现ApplicationContextAware接口来获取Spring上下文,手动获取Repository实例:
public class FooGenerator implements IdentifierGenerator, ApplicationContextAware { private static ApplicationContext springContext; private FooRepository fooRepository; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { springContext = applicationContext; } @Override public Serializable generate(SharedSessionContractImplementor session, Object entity) throws HibernateException { // 延迟初始化Repository if (fooRepository == null) { fooRepository = springContext.getBean(FooRepository.class); } // 这里就可以正常调用fooRepository的方法了 } }
不过真心不建议用查最大ID的方式生成新ID——高并发下很容易出现重复,而且每次生成都要查库,性能拉胯。下面给你几个更靠谱的替代方案:
二、优雅生成15-25位唯一数字ID的方案
方案1:利用数据库原生序列/自增机制+自定义前缀
如果你的数据库支持序列(比如PostgreSQL的SEQUENCE),或者MySQL的自增主键,可以直接用数据库的原子性递增能力,再拼接固定前缀(比如时间戳的前几位)来凑够15-25位:
比如PostgreSQL中先创建序列:
CREATE SEQUENCE foo_id_seq START WITH 1000000000 INCREMENT BY 1;
然后在生成器里通过SessionImplementer执行SQL获取下一个序列值:
@Override public Serializable generate(SharedSessionContractImplementor session, Object entity) throws HibernateException { SessionImplementer sessionImpl = (SessionImplementer) session; // 执行SQL获取序列值 BigInteger seqValue = (BigInteger) sessionImpl.createNativeQuery("SELECT nextval('foo_id_seq')") .uniqueResult(); // 拼接前缀(比如当前时间戳的前8位),生成16位ID:8位时间戳+8位序列值 String prefix = String.valueOf(System.currentTimeMillis()).substring(0, 8); return prefix + String.format("%08d", seqValue); }
MySQL的话可以用自增主键,插入后获取自增值再拼接前缀,不过这种方式需要先插入再更新ID,不如序列方便。
方案2:用Redis原子递增生成ID(推荐高并发场景)
如果项目里有Redis,这绝对是最优解——Redis的INCR命令是原子性的,不会出现重复,而且性能超高。你可以给每个业务类型定义一个Redis key,每次生成ID时递增这个key的值,再拼接前缀生成15-25位的数字ID:
// 假设你已经通过Spring上下文获取了StringRedisTemplate public String generateFooId() { String redisKey = "foo:id:counter"; // 原子递增,返回当前递增后的值 Long counter = stringRedisTemplate.opsForValue().increment(redisKey); // 拼接前缀(比如10位时间戳),生成18位ID:10位时间戳+8位计数器 String prefix = String.valueOf(System.currentTimeMillis()).substring(0, 10); // 格式化计数器为8位,不足补0 return prefix + String.format("%08d", counter); }
这种方式完全不需要操作数据库,也不存在Repository依赖的问题,高并发下稳得一批。
方案3:基于Snowflake算法的变种(分布式无依赖)
Snowflake算法生成的是64位整数,转换成十进制刚好是18位左右,完美符合你15-25位的要求。而且它不需要依赖任何外部存储,本地就能生成,分布式场景下也能保证唯一。
你可以自己实现一个变种,调整时间戳、机器ID、序列号的位数,比如:
- 时间戳部分:用当前时间减去一个固定起始时间,取40位(约34年)
- 机器ID:10位(支持1024台机器)
- 序列号:14位(每毫秒生成16384个ID)
把这些部分拼接成一个64位的Long,转换成十进制就是18位左右的数字ID,完全满足你的需求。
总结
优先推荐方案2(Redis)或者方案3(Snowflake变种),这两种方式既解决了FooRepository为null的问题(根本不需要用Repository),又避免了查最大ID的性能和并发问题,优雅又可靠。
内容的提问来源于stack exchange,提问作者Viktor M.

