业务交易号生成方式 —— 号段

前言

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

  • UUID 是纯字母,从这个交易号中完全看不出任何业务信息,还怪怪的
  • 雪花算法,生成的号码是纯数字,看起来也不直观,拼接一些业务类型等信息还可以
  • 自增编号,能看出业务信息,但是太直观了,也能让别人看出一天有多少业务量,很不合适

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

源码分享

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

思路

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

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

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

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

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

建表

sql 复制代码
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;

代码实现

java 复制代码
@Component
@Slf4j
public 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;
    }
}

在主类中测试

java 复制代码
@SpringBootApplication
@Slf4j
public 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,这个业务量绝大多数情况下够用了。

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

相关推荐
zhou lily1 小时前
高可用(HA)架构:在混合云环境下,如何通过多活容灾保障核心业务流的稳定性
架构
IT_Octopus2 小时前
IntelliJ 本地日志路径自定义:`-DLOG_PATH=./logs` 为什么总“不听话“
java·log4j·intellij-idea
吴佳浩 Alben2 小时前
Agent 安全红线:越狱防御、间接注入与数据防泄漏实战
人工智能·安全·语言模型·架构·ai编程
码事漫谈2 小时前
DeepSeek 这波操作很凶
后端
长征coder3 小时前
【无标题】
java·性能优化
木木学AI3 小时前
2026主流大模型电话机器人系统解析:技术架构、业务执行与企业落地实践
架构·系统架构·机器人
ChampaignWolf3 小时前
在 SAP S/4HANA 内部基于 ICF Handler 从零实现 ABAP MCP Server:架构、代码与踩坑实录
c++·架构·sap·abap·mcp
新时代牛马3 小时前
PCI与PCIe 硬件原理、配置空间/BAR 与 Linux 驱动完整篇:从 LTSSM、TLP 到 ECAM 与probe
java·linux·服务器
国科安芯3 小时前
星链供电架构中的高集成度电源管理方案研究——基于ASP4644四通道降压稳压器在低轨卫星星座中的应用分析
大数据·单片机·嵌入式硬件·架构·低轨卫星·星间链路
Bs_MoneyMagnet3 小时前
基于springboot+vue的医疗健康便民服务平台的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring