Java 基于 MySQL 行锁实现高并发业务序列号生成器 📅 2026/8/9 5:22:29 目录前言一、为什么不直接使用 UUID二、为什么不直接使用数据库自增主键三、本文实现目标1. 支持自定义业务前缀2. 序列号固定9位3. 支持多线程并发生成4. 支持多实例部署五、实现方案六、service 实现类七、Mapper 、建表语句 和 xml前言在企业级应用开发过程中我们经常会遇到需要生成唯一业务编号的场景例如财务系统中的凭证号、批次号、请求流水号订单系统中的订单编号支付系统中的交易流水号文件上传系统中的文件编号第三方接口调用时要求的唯一请求ID。这些业务编号通常具有以下特点必须保证全局唯一同一个业务场景下生成的编号不能重复否则可能导致数据覆盖、财务账务错误、接口幂等失败等严重问题。需要具备一定业务含义很多业务系统并不希望使用完全随机的 UUID而是希望编号中包含一些业务信息。例如HX44030010001686XAc000000001其中HX44030010001686XAc业务前缀可以表示公司、业务类型、日期等信息000000001递增序列号用于区分同一批次下的不同数据。需要支持高并发环境在生产环境中应用通常会部署多个实例。例如服务实例A | | 服务实例B --- 生成业务编号 | | 服务实例C多个线程、多个服务节点可能同时请求生成编号如果没有正确的并发控制很容易产生重复编号。一、为什么不直接使用 UUID很多开发人员第一时间想到 UUID例如550e8400-e29b-41d4-a716-446655440000UUID 确实可以保证较高概率唯一但是在很多业务场景下并不适合。原因包括(1) . 可读性较差业务人员看到 UUID 无法快速判断哪个系统生成哪一天生成属于哪个业务批次。而业务编号HX44030010001686XAc000000001可以携带一定业务信息。(2). 长度较长UUID36位而很多财务系统、第三方接口对于编号长度存在限制。例如最大长度20位最大长度32位最大长度64位UUID 可能不符合要求。(3). 无法保证递增很多业务希望编号具有递增特征例如000000001000000002000000003方便查询排查问题对账日志追踪。UUID 不具备这种特性。二、为什么不直接使用数据库自增主键MySQL 自增 IDAUTO_INCREMENT确实可以生成1 2 3 4但是它通常只适合作为数据库内部主键。业务编号往往需要指定前缀按业务维度重新计数不同业务使用不同序列。例如公司ACOMPANY_A000000001 COMPANY_A000000002公司BCOMPANY_B000000001 COMPANY_B000000002数据库自增无法满足这种需求。三、本文实现目标本文设计并实现一个基于 MySQL 的业务序列号生成器满足以下要求1. 支持自定义业务前缀例如输入HX44030010001686XAc生成HX44030010001686XAc0000000012. 序列号固定9位规则1 - 000000001 111 - 000000111 123456789 - 123456789最大支持999999999即同一个业务前缀最多可以生成999999999个唯一编号。3. 支持多线程并发生成例如100个线程同时请求generateSeqId(HX44030010001686XAc)最终结果HX44030010001686XAc000000001 HX44030010001686XAc000000002 HX44030010001686XAc000000003 ... HX44030010001686XAc000000100不会出现重复。4. 支持多实例部署即使应用部署服务器A 服务器B 服务器C多个节点同时生成编号也能够保证唯一。五、实现方案本文采用MySQL InnoDB SELECT FOR UPDATE Spring事务实现数据库级别的分布式序列控制。核心思想每一个业务前缀对应数据库中的一条序列记录例如seq_key current_value HX44030010001686XAc 100表示HX44030010001686XAc 已经生成到第100个编号生成编号时执行SELECTcurrent_valueFROMgenerator_seq_idWHEREseq_key?FORUPDATE锁定当前序列记录。当前事务内current_value 1更新数据库。格式化生成000000101最终拼接HX44030010001686XAc000000101为什么选择数据库锁相比 Redis 分布式锁或者第三方 ID 服务数据库方案具有实现简单无需额外中间件数据可靠持久化与业务数据库保持一致。对于财务系统ERP系统内部管理系统这种对唯一性要求高但生成压力不是百万级 QPS 的场景非常适合。六、service 实现类packagecom.somedemo.service;importcom.somedemo.mapper.SeqIdGeneratorMapper;importlombok.extern.slf4j.Slf4j;importorg.apache.commons.lang3.StringUtils;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Component;importorg.springframework.transaction.annotation.Propagation;importorg.springframework.transaction.annotation.Transactional;importjava.util.Locale;/** * 生成seqId生成器。 * * 规则 * * 前缀长度1 ~ 20位 * 序列长度固定9位 * 序列范围000000001 ~ 999999999 * * 示例 * * prefix: * HX44030010001686XAc * * 第一次 * HX44030010001686XAc000000001 * * 第二次 * HX44030010001686XAc000000002 */Slf4jComponentpublicclassSeqIdGeneratorService{/** * 前缀最大长度。 */privatestaticfinalintPREFIX_MAX_LENGTH20;/** * 后缀序列固定长度。 */privatestaticfinalintSEQUENCE_LENGTH9;/** * 后缀序列最大值。 */privatestaticfinalintMAX_SEQUENCE_VALUE999_999_999;AutowiredprivateSeqIdGeneratorMapperseqIdGeneratorMapper;/** * 生成seqId * * 使用独立事务的目的 * * 1. 序列号生成事务与外部业务事务隔离 * 2. 一个序列一旦成功生成就不因为外层业务回滚而重新使用 * 3. 避免出现重复 seqId */Transactional(propagationPropagation.REQUIRES_NEW,rollbackForException.class)publicStringgenerateSeqId(Stringprefix){/* * 1. 校验前缀 */if(StringUtils.isBlank(prefix)){thrownewIllegalArgumentException(生成seqId失败前缀不能为空);}StringsequenceKeyprefix.trim();if(sequenceKey.length()PREFIX_MAX_LENGTH){thrownewIllegalArgumentException(生成SEQID失败前缀长度不能超过PREFIX_MAX_LENGTH位当前长度sequenceKey.length());}/* * 2. 初始化序列。 * * 如果不存在 INSERT current_value 0 * * 如果已经存在INSERT IGNORE忽略主键冲突。 * * 并发情况下 * 多个线程同时插入相同seq_key时 * 最终数据库中也只会存在一条记录。 */seqIdGeneratorMapper.insertIfAbsent(sequenceKey);/* * 3. SELECT ... FOR UPDATE * * 对当前 seq_key 对应的数据行加排他锁。 * * 事务提交之前其他事务针对相同 seq_key 的SELECT FOR UPDATE会等待。 * * 不同seq_key之间互不影响。 */IntegercurrentValueseqIdGeneratorMapper.selectCurrentValueForUpdate(sequenceKey);if(currentValuenull){thrownewIllegalStateException(生成SEQID失败未查询到序列记录sequenceKeysequenceKey);}/* * 4. 检查序列是否已经达到最大值。 * * 这里先判断再执行1。 * * 比下面这种写法更加安全 * * int nextValue currentValue 1; * * 因为理论上如果数据库数据异常currentValue已经达到Integer.MAX_VALUE直接1会发生整数溢出。 * */if(currentValueMAX_SEQUENCE_VALUE){thrownewIllegalStateException(SEQID序列已用完sequenceKeysequenceKeycurrentValuecurrentValuemaxValueMAX_SEQUENCE_VALUE);}/* * 5. 序列1 */intnextValuecurrentValue1;/* * 6. 更新数据库。 */intupdateCountseqIdGeneratorMapper.updateCurrentValue(sequenceKey,nextValue);if(updateCount!1){thrownewIllegalStateException(更新SEQID序列失败sequenceKeysequenceKeynextValuenextValue);}/* * 7. 固定格式化成9位数字。 * 例如 * 1 - 000000001 * 111 - 000000111 * 123456789 - 123456789 */StringsequenceString.format(Locale.ROOT,%0SEQUENCE_LENGTHd,nextValue);StringseqIdsequenceKeysequence;log.info(生成财务在线SEQID成功sequenceKey{}currentValue{}SEQID{},sequenceKey,nextValue,seqId);returnseqId;}}七、Mapper 、建表语句 和 xmlpackagecom.somedemo.mapper;importorg.apache.ibatis.annotations.Param;importorg.apache.ibatis.annotations.Mapper;MapperpublicinterfaceSeqIdGeneratorMapper{/** * 如果序列不存在则初始化为0。 */intinsertIfAbsent(Param(seqKey)StringseqKey);/** * 查询当前序列值并对当前记录加排他锁。 */IntegerselectCurrentValueForUpdate(Param(seqKey)StringseqKey);/** * 更新当前序列值。 */intupdateCurrentValue(Param(seqKey)StringseqKey,Param(currentValue)IntegercurrentValue);}建表语句CREATETABLEgenerator_seq_id(seq_keyVARCHAR(32)NOTNULLCOMMENT序列键/前缀例如HX44030010001686XAc,current_valueINTNOTNULLDEFAULT0COMMENT当前序列值最大999999999,update_timeDATETIMENOTNULLCOMMENT更新时间,PRIMARYKEY(seq_key))ENGINEInnoDBCOMMENT用于生成序列号ID;xml:?xml version1.0 encodingUTF-8?!DOCTYPEmapperPUBLIC-//mybatis.org//DTD Mapper 3.0//ENhttp://mybatis.org/dtd/mybatis-3-mapper.dtdmappernamespacecom.somedemo.mapper.SeqIdGeneratorMapper!-- 序列不存在时初始化。 不存在 current_value 0 已存在 INSERT IGNORE忽略主键冲突。 在并发情况下即使多个线程同时执行 因为seq_key是主键也只会存在一条记录。 --insertidinsertIfAbsentINSERT IGNORE INTO generator_seq_id (seq_key, current_value, update_time) VALUES (#{seqKey},0,NOW())/insert!-- 查询当前序列并对这一行加排他锁。 注意 SELECT FOR UPDATE必须在事务中执行。 --selectidselectCurrentValueForUpdateresultTypejava.lang.IntegerSELECT current_value FROM generator_seq_id WHERE seq_key #{seqKey} FOR UPDATE/select!-- 更新当前序列值。 --updateidupdateCurrentValueUPDATE generator_seq_id SET current_value #{currentValue}, update_time NOW() WHERE seq_key #{seqKey}/update/mapper