稀土掘金技术社区 02月27日
业务交易号生成方式 —— 号段
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

文章探讨业务交易号的生成方式,提出应包含业务类型、日期、随机数和长整型数字,并介绍了保证号码不重复、不连续递增的实现方法,还提及了可能存在的隐患及性能测试情况。

业务交易号需包含业务类型、日期、随机数和长整型数字

通过设置固定尾号长度、范围及起始值保证号码特性

存在交易号可能重复的隐患,可根据业务量调整

单线程产生200w个序列号需2500ms,性能多数情况够用

原创 暮色妖娆丶 2025-02-26 08:31 重庆

点击关注公众号,“技术干货” 及时达!

前言

业务交易号的生成方式有很多,可以使用 UUID,也可以使用业务类型 bizType 拼接雪花算法产生的 SnowFlakeId,还可以用自增编号。但是这些方式似乎都不太合适

我们需要的业务交易号最好是 业务类型 + 日期 + N位随机数 + 一个不重复的不连续递增的长整型数字

源码分享

完整项目代码已分享到 Github syc-sequence

思路

例如我司的还款交易号 TQYHK20240920142987500 由以下几部分组成

这样,我们能知道这个交易号是哪个业务类型的,哪天产生的,但是看不出其他相关信息。现在我们要考虑的是交易日期后面的号码怎么保证不重复,怎么保证不连续递增。

不连续递增比较好处理,中间几位随机数就解决了,保证尾号不重复,同时还要注意性能,还需要让尾号不能太长,否则手机上位置有限可能会影响 UI 展示。

我们可以考虑固定尾号的长度比如为 6 位,然后给定一个范围比如 1000,起始值 为 1 ,在 1000 以内,产生的号码递增,号码不够 6 位长度的左边补 0 ,超过 1000,记录当前序列值,再更新起始值为 当前序列值1001,然后从这个值作为起始,继续自增,一直循环下去,直到起始的号段值超过 Long 类型的最大值,然后起始值再置为一个初始值 ,重新开始。流程图如下:

如果这段没看懂,没关系,看下面的代码就明白了。下面我们开始用代码实现

建表

CREATE TABLE `sequence` (  `sequence_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '序列号类型 = 区分业务类型',  `crt_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',  `start_value` bigint NOT NULL DEFAULT '1' COMMENT '起始值',  `curr_value` bigint NOT NULL DEFAULT '1' COMMENT '序列号当前值',  `upt_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',  PRIMARY KEY (`sequence_type`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

代码实现

@Component@Slf4jpublic class SequenceService {    @Autowired    private SequenceMapper sequenceMapper;    @Autowired    private SequenceService sequenceService;    /**     * 号段大小     * */    private final int allocateSize = 1000;    /**     * key - sequenceType value - 号段起始值     * */    private final Map<String, Long> allocateMaps = new ConcurrentHashMap<>();//当前号段    /**     * key - sequenceType value - 号段内自增值     * */    private final Map<String, AtomicLong> incrementMaps = new ConcurrentHashMap<>(); //业务号段当前值    /**     * 进程锁     * */    private final ReentrantLock lock = new ReentrantLock();
public long next(String sequenceType) { //用进程锁,这样每个服务实例就会用新的号段,避免出现连续递增的情况 lock.lock(); try { if (allocateMaps.containsKey(sequenceType) && incrementMaps.get(sequenceType).incrementAndGet() < allocateSize) { return allocateMaps.get(sequenceType) + incrementMaps.get(sequenceType).longValue(); } return sequenceService.nextValues(sequenceType,1); } finally { lock.unlock(); } }
/** * @param count 递增间隔 * */ @Transactional(propagation = Propagation.REQUIRES_NEW) public long nextValues(String sequenceType,int count) { Sequence sequence = sequenceMapper.getForUpdate(sequenceType); if (sequence == null) { sequence = new Sequence(); sequence.setCrtTime(LocalDateTime.now()); sequence.setSequenceType(sequenceType); sequence.setStartValue(1); sequence.setCurrValue(1); try { sequenceMapper.insert(sequence); } catch (Exception e) { // Duplicated conflict sequence = sequenceMapper.getForUpdate(sequenceType); if (sequence == null) { throw new RuntimeException("Unable init sequence, sequenceType=[" + sequenceType + "]."); } } } long seqValue = sequence.getCurrValue(); long value = seqValue; while (value >= 0 && Long.MAX_VALUE - value < count) { // 序列值循环: 当value + count 大于 0Long.MAX_VALUE时,从startValue重新开始累加 count -= (int) (Long.MAX_VALUE - value + 1); value = sequence.getStartValue(); } sequence.setCurrValue(value + count + allocateSize); // nextValue sequenceMapper.updateById(sequence); // currValue 大于 allocateMaps 一个号段值 allocateMaps.put(sequenceType, value + count); incrementMaps.put(sequenceType, new AtomicLong(0)); return seqValue; }}

在主类中测试

@SpringBootApplication@Slf4jpublic class SycSequenceApplication {
public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(SycSequenceApplication.class, args); SequenceService sequenceService = context.getBean(SequenceService.class); for (int i = 0; i < 10000; i++) { long l = sequenceService.next(SequenceTypes.TEST_SEQUENCE_ID); String seqId = StringUtil.subOrLefPad(String.valueOf(l), 6);//补 0 log.info("Sequence Id:{}", seqId); } }
}

隐患

从上面的设计逻辑,如果仔细分析的话我们会发现,由于我们截取了号段后六位,假如一天之内生成的多个号段里面的交易号分别是 987500、1987500、2987500。 那么后六位的号码就是相同的,此时正好前面三位随机数如果也是相同的,那么就会导致交易号重复

但是这种情况只有当我们一天之内生成的交易号超过 100w 才有可能出现这个问题,所以,根据业务量实际情况使用即可,如果这个量级都不够,那么就截取后八位即可。这样一天之内生成的交易号超过一亿才可能会出现重复。

结语

结尾的号段也可以用雪花算法生成,截取雪花算法的后六位或者后八位,但是这样一来没有号段的概念,并且雪花算法产生的序列是连续递增的。

虽然上面号段的实现逻辑里面访问了数据库,也用了进程锁,但是我测试了一下性能,单线程的情况下产生 200w 个序列号只需要 2500ms,这个业务量绝大多数情况下够用了。

如果这篇文章对你有帮助,记得点赞加关注!你的支持就是我继续创作的动力!

点击关注公众号,“技术干货” 及时达!

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

业务交易号 号码生成 性能测试 隐患
相关文章