Java 基于 MySQL 行锁实现高并发业务序列号生成器
2026/8/9 5:22:27 网站建设 项目流程

目录

  • 前言
  • 一、为什么不直接使用 UUID?
  • 二、为什么不直接使用数据库自增主键?
  • 三、本文实现目标
    • 1. 支持自定义业务前缀
    • 2. 序列号固定9位
    • 3. 支持多线程并发生成
    • 4. 支持多实例部署
  • 五、实现方案
  • 六、service 实现类
  • 七、Mapper 、建表语句 和 xml

前言

在企业级应用开发过程中,我们经常会遇到需要生成唯一业务编号的场景,例如:

  • 财务系统中的凭证号、批次号、请求流水号;
  • 订单系统中的订单编号;
  • 支付系统中的交易流水号;
  • 文件上传系统中的文件编号;
  • 第三方接口调用时要求的唯一请求ID。

这些业务编号通常具有以下特点:

  1. 必须保证全局唯一

    同一个业务场景下,生成的编号不能重复,否则可能导致数据覆盖、财务账务错误、接口幂等失败等严重问题。

  2. 需要具备一定业务含义

    很多业务系统并不希望使用完全随机的 UUID,而是希望编号中包含一些业务信息。

    例如:HX44030010001686XAc000000001

    其中:

    • HX44030010001686XAc:业务前缀,可以表示公司、业务类型、日期等信息;
    • 000000001:递增序列号,用于区分同一批次下的不同数据。
  3. 需要支持高并发环境

在生产环境中,应用通常会部署多个实例。

例如:

服务实例A | | 服务实例B ---> 生成业务编号 | | 服务实例C

多个线程、多个服务节点可能同时请求生成编号,如果没有正确的并发控制,很容易产生重复编号。

一、为什么不直接使用 UUID?

很多开发人员第一时间想到 UUID:

例如:550e8400-e29b-41d4-a716-446655440000

UUID 确实可以保证较高概率唯一,但是在很多业务场景下并不适合。

原因包括:

(1) . 可读性较差

业务人员看到 UUID 无法快速判断:

  • 哪个系统生成;
  • 哪一天生成;
  • 属于哪个业务批次。

而业务编号:HX44030010001686XAc000000001

可以携带一定业务信息。

(2). 长度较长

UUID:36位

而很多财务系统、第三方接口对于编号长度存在限制。

例如:

  • 最大长度20位
  • 最大长度32位
  • 最大长度64位

UUID 可能不符合要求。

(3). 无法保证递增

很多业务希望编号具有递增特征,例如:

  • 000000001
  • 000000002
  • 000000003

方便:

  • 查询;
  • 排查问题;
  • 对账;
  • 日志追踪。

UUID 不具备这种特性。


二、为什么不直接使用数据库自增主键?

MySQL 自增 ID:

AUTO_INCREMENT

确实可以生成:

1 2 3 4

但是它通常只适合作为数据库内部主键。

业务编号往往需要:

  • 指定前缀;
  • 按业务维度重新计数;
  • 不同业务使用不同序列。

例如:

公司A:

COMPANY_A000000001 COMPANY_A000000002

公司B:

COMPANY_B000000001 COMPANY_B000000002

数据库自增无法满足这种需求。


三、本文实现目标

本文设计并实现一个基于 MySQL 的业务序列号生成器,满足以下要求:

1. 支持自定义业务前缀

例如:

输入:

HX44030010001686XAc

生成:

HX44030010001686XAc000000001

2. 序列号固定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 */@Slf4j@ComponentpublicclassSeqIdGeneratorService{/** * 前缀最大长度。 */privatestaticfinalintPREFIX_MAX_LENGTH=20;/** * 后缀序列固定长度。 */privatestaticfinalintSEQUENCE_LENGTH=9;/** * 后缀序列最大值。 */privatestaticfinalintMAX_SEQUENCE_VALUE=999_999_999;@AutowiredprivateSeqIdGeneratorMapperseqIdGeneratorMapper;/** * 生成seqId * * 使用独立事务的目的: * * 1. 序列号生成事务与外部业务事务隔离; * 2. 一个序列一旦成功生成,就不因为外层业务回滚而重新使用; * 3. 避免出现重复 seqId */@Transactional(propagation=Propagation.REQUIRES_NEW,rollbackFor=Exception.class)publicStringgenerateSeqId(Stringprefix){/* * 1. 校验前缀 */if(StringUtils.isBlank(prefix)){thrownewIllegalArgumentException("生成seqId失败,前缀不能为空");}StringsequenceKey=prefix.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之间互不影响。 */IntegercurrentValue=seqIdGeneratorMapper.selectCurrentValueForUpdate(sequenceKey);if(currentValue==null){thrownewIllegalStateException("生成SEQID失败,未查询到序列记录,sequenceKey="+sequenceKey);}/* * 4. 检查序列是否已经达到最大值。 * * 这里先判断,再执行+1。 * * 比下面这种写法更加安全: * * int nextValue = currentValue + 1; * * 因为理论上如果数据库数据异常,currentValue已经达到Integer.MAX_VALUE,直接+1会发生整数溢出。 * */if(currentValue>=MAX_SEQUENCE_VALUE){thrownewIllegalStateException("SEQID序列已用完,sequenceKey="+sequenceKey+",currentValue="+currentValue+",maxValue="+MAX_SEQUENCE_VALUE);}/* * 5. 序列+1 */intnextValue=currentValue+1;/* * 6. 更新数据库。 */intupdateCount=seqIdGeneratorMapper.updateCurrentValue(sequenceKey,nextValue);if(updateCount!=1){thrownewIllegalStateException("更新SEQID序列失败,sequenceKey="+sequenceKey+",nextValue="+nextValue);}/* * 7. 固定格式化成9位数字。 * 例如: * 1 -> 000000001 * 111 -> 000000111 * 123456789 -> 123456789 */Stringsequence=String.format(Locale.ROOT,"%0"+SEQUENCE_LENGTH+"d",nextValue);StringseqId=sequenceKey+sequence;log.info("生成财务在线SEQID成功,sequenceKey:{},currentValue:{},SEQID:{}",sequenceKey,nextValue,seqId);returnseqId;}}

七、Mapper 、建表语句 和 xml

packagecom.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))ENGINE=InnoDBCOMMENT='用于生成序列号ID';

xml:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.somedemo.mapper.SeqIdGeneratorMapper"><!-- 序列不存在时初始化。 不存在: current_value = 0 已存在: INSERT IGNORE忽略主键冲突。 在并发情况下,即使多个线程同时执行, 因为seq_key是主键,也只会存在一条记录。 --><insertid="insertIfAbsent">INSERT IGNORE INTO generator_seq_id (seq_key, current_value, update_time) VALUES (#{seqKey},0,NOW())</insert><!-- 查询当前序列,并对这一行加排他锁。 注意: SELECT FOR UPDATE必须在事务中执行。 --><selectid="selectCurrentValueForUpdate"resultType="java.lang.Integer">SELECT current_value FROM generator_seq_id WHERE seq_key = #{seqKey} FOR UPDATE</select><!-- 更新当前序列值。 --><updateid="updateCurrentValue">UPDATE generator_seq_id SET current_value = #{currentValue}, update_time = NOW() WHERE seq_key = #{seqKey}</update></mapper>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询