MySQL高并发生成唯一订单号的方法

本文介绍了一种在高并发环境下生成唯一订单号的方法,通过使用存储过程和数据表结合,确保即使在多用户同时操作的情况下,也能生成唯一的订单号,避免业务冲突。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

这篇博文发布后,有朋友问有没有SQL server版本的,现在有了==》传送门

一、场景再现

在一个erp进销存系统或0A等其他系统中,如果多人同时进行生成订单号的操作的话,容易出现多人获得同一个订单号的情况,对公司业务造成不可挽回的损失

二、如何避免高并发情况订单号不唯一

我们可以利用存储过程和数据表搭配,建立一张表和创建存储过程,存储过程负责生成订单号,表负责处理唯一性问题
在这里插入图片描述
当存储过程生成一个订单编号,首先先把订单号写进表中,再把订单号结果显示出来,把生成的订单号写进表里会出现两种情况,为什么呢?因为我们的表设置了主键(主键唯一性)

  • 能写进: 当表里没有相同的订单号,即把生成的订单号写进表里
  • 不能写进:当表里存在相同的订单号,即生成的订单号无法写到表里,也就不能获取到订单号,从而确保高并发下生成唯一订单号

三、高并发情况下生成唯一订单号的过程

下面将用代码和实际操作讲解下生成唯一订单号的过程

第一步:建一张数据表,设置订单号字段为主键(唯一订单号的关键)

在这里插入图片描述

第二步:创建生成订编号的存储过程

生成订单编号格式为:自定义前缀+年月日+后缀(001、002、003)
1.首先创建一个存储过程
输入为BILL_TYPE(前缀),输出为BILL_NOP(订单编号)

CREATE DEFINER = CURRENT_USER PROCEDURE `getbillno`(in BILL_TYPE VARCHAR(3), out BILL_NOP varchar(25))
BEGIN 

2.生成年月日和后缀
年月日为当前系统时间,后缀初始值为0

DECLARE currentDate varCHAR (15);
DECLARE lastno INT DEFAULT 0;
SELECT DATE_FORMAT(NOW(), '%Y%m%d') INTO currentDate;

3.查询表格,获取表格的订单编号
查询表格,获取前缀与自定义内容相关的最新一个订单编号

SELECT IFNULL(BILL_NO, 'notnull') INTO BILL_NOP
  FROM temp_bill 
  WHERE SUBSTRING(BILL_NO,1,3) =BILL_TYPE and
  SUBSTRING(BILL_NO,4,8) =currentDate
  ORDER BY BILL_NO DESC LIMIT 1;

4.生成订单编号

  • 如果上一步获取的订单编号不为空,新生成的订单号在原订单号上+1

    例:获取的订单号:UIE20200611015
    即生成的订单号为:UIE20200611016

  • 如果上一步获取的订单号为空,新生成的订单号后缀为001
    例:生成的订单号:UIE20200611001

IF BILL_NOP !='' THEN
 SET lastno = CONVERT(SUBSTRING(BILL_NOP, -3), DECIMAL) ; 
 SELECT 
  CONCAT(BILL_TYPE,currentDate,LPAD((lastno + 1),3, '0')) INTO BILL_NOP ;
ELSE
 SELECT 
  CONCAT(BILL_TYPE,currentDate,LPAD((lastno + 1),3, '0')) INTO BILL_NOP ;
END IF;

5.将生成的订单号插入表中
如果表中存在相同订单号则插入表失败
如果表中不存在相同订单号则插入表成功

INSERT INTO temp_bill (BILL_NO, BILL_TYPE) VALUES (BILL_NOP,BILL_TYPE) ;

6.返回唯一订单编号
当订单插入表成功,即可返回唯一的订单编号(如果上一步不成功,即不会运行这一步)

SELECT   BILL_NOP;

四、运行结果

1.首先我的表里没有数据,会生成前缀(我输入的:UIE)+年月日(
20200611)+001(第一个数据,所以是001)
即:UIE20200611001

2当我第二次输入时,由于表里有数据了,会根据最新的订单编号后缀加1
即:UIE20200611002

在这里插入图片描述

五、不是小结的小结

一个卑微菜鸡的自述:
希望这边文章可以帮到大家
积分充足的就直接下载吧,没有积分的对着图敲,反正代码量也不大
如果你是学生,没有积分,可以私聊我,免费给你发源码

代码下载链接:mysql_getbillno.sql
代码截图:在这里插入图片描述

<think>好的,我现在需要解决用户的问题:如何使用Java结合MySQL生成有序流水号。首先,我得理解用户的具体需求。用户提到“有序流水号”,可能是指在系统中生成连续的、唯一的序列号,比如订单号、交易流水号等,通常需要保证唯一性和顺序性,还可能有一定的格式要求,比如日期前缀等。 首先,我需要考虑在发环境下如何保证流水号的唯一和有序。因为如果多个应用实例同时生成流水号,可能会出现重复或者不连续的情况。常见的解决方案有数据库自增ID、使用数据库的事务控制、或者分布式ID生成算法(如雪花算法)。不过用户特别提到了MySQL和Java的结合,所以可能需要基于数据库的特性来实现。 接下来,思考可能的实现方式。一种常见的方法是利用MySQL的自增字段,但自增ID通常是数字且可能不够灵活,无法添加前缀。另一种方法是使用数据库的事务,通过先查询当前最大值,然后加一的方式生成下一个流水号。但这种方式在发下可能会有性能问题,或者需要锁表,导致瓶颈。 另外,用户可能需要流水号有一定的格式,比如包含日期、固定前缀等。例如,20231009-0001,其中日期部分每天变化,后面的序号每天从1开始。这种情况下,需要每天重置序号,且保证在发情况下正确生成。 这时候,可以考虑在数据库中使用一个表来维护当前的序号。每次生成时,先获取当前日期对应的最大序号,然后加一,更新回数据库。为了处理发,可以使用事务,且选择正确的隔离级别,或者使用行级锁(如SELECT ... FOR UPDATE)来确保同一时间只有一个线程能获取更新序号。 例如,创建一个表serial_numbers,包含字段prefix(日期部分),current_value(当前序号)。每天第一次生成时,插入当天的日期,current_value设为1。之后每次生成时,先查询当天的记录,current_value加一,更新回去。这个过程需要在事务中完成,且使用FOR UPDATE锁定该行,防止其他事务同时修改。 但这样可能存在性能问题,因为每次生成流水号都需要访问数据库且需要处理锁。如果发量很,可能会成为瓶颈。这时候可以考虑使用缓存,比如在应用层维护一个当前序号的范围,每次从数据库获取一批序号,比如一次获取100个,用完后再次获取。这样可以减少数据库访问次数,但需要处理好应用重启时的序号恢复,避免重复。 另外,MySQL的AUTO_INCREMENT虽然可以生成唯一ID,但无法直接生成带有日期前缀的流水号。这时候可以结合AUTO_INCREMENT和日期来拼接。例如,每天生成一个表,或者使用触发器,在插入时自动生成流水号。不过这种方法可能不够灵活,且维护成本较。 还有一种方法是使用UUID,但UUID是无序的,不符合用户的有序要求。雪花算法生成的ID是有序的,但通常是一个长数字,可能不符合用户需要的格式。可能需要将雪花算法生成的ID与日期前缀结合。 不过用户明确要求使用Java和MySQL,所以可能需要结合应用层和数据库层的逻辑。例如,在Java应用中,每次生成流水号时,先获取当天的日期,然后查询MySQL中该日期对应的最大序号,加一后保存。这需要处理好发情况下的原子操作。 可能的步骤: 1. 创建数据库表,存储日期前缀和当前序号。 2. 在Java应用中,使用事务和行级锁来确保每次获取和更新序号的原子性。 3. 生成流水号时,拼接日期和序号。 接下来,我需要验证这些思路的正确性。例如,使用SELECT ... FOR UPDATE来锁定行,确保其他事务必须等待当前事务提交后才能读取该行,这样可以避免发导致的重复问题。此外,需要注意事务的隔离级别,确保可重复读或者更级别,防止脏读等问题。 例如,表结构可能设计如下: CREATE TABLE serial_number ( prefix VARCHAR(8) PRIMARY KEY COMMENT '日期前缀,如20231009', current_value INT NOT NULL DEFAULT 1, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); 每次生成流水号时,Java代码执行以下步骤: - 获取当前日期,如20231009,作为prefix。 - 开启事务。 - 执行SELECT current_value FROM serial_number WHERE prefix = ? FOR UPDATE,如果存在记录,则获取current_value,然后更新为current_value + 1。 - 如果不存在,则插入一条新记录,current_value设置为1。 - 提交事务。 - 使用prefix和current_value生成流水号,如20231009-0001。 这样,在事务中通过FOR UPDATE锁定行,确保同一时间只有一个线程能处理该日期的序号生成,从而避免重复。 但是,如果应用部署在多个实例上,这种方式可能会因为数据库连接的不同而产生竞争,但由于使用了行锁,应该还是可以保证唯一性和顺序。不过,如果发量非常大,可能会影响性能。这时候可以考虑使用缓存批量获取序号,或者采用其他分布式ID生成方案,但需要结合用户的需求。 此外,还需要考虑序号重置的问题,比如每天自动重置为1。表结构中的prefix是日期,所以每天会有不同的记录,这样自然每天会从1开始。 在Java代码中,使用JDBC事务处理,确保在操作过程中锁定行,然后更新。例如: Connection conn = dataSource.getConnection(); try { conn.setAutoCommit(false); // 获取当前日期前缀 String prefix = getCurrentDatePrefix(); // 查询锁定行 PreparedStatement selectStmt = conn.prepareStatement( "SELECT current_value FROM serial_number WHERE prefix = ? FOR UPDATE"); selectStmt.setString(1, prefix); ResultSet rs = selectStmt.executeQuery(); int currentValue; if (rs.next()) { currentValue = rs.getInt("current_value"); // 更新 PreparedStatement updateStmt = conn.prepareStatement( "UPDATE serial_number SET current_value = ? WHERE prefix = ?"); updateStmt.setInt(1, currentValue + 1); updateStmt.setString(2, prefix); updateStmt.executeUpdate(); } else { // 插入新记录 PreparedStatement insertStmt = conn.prepareStatement( "INSERT INTO serial_number (prefix, current_value) VALUES (?, 1)"); insertStmt.setString(1, prefix); insertStmt.executeUpdate(); currentValue = 1; } conn.commit(); // 生成流水号,如格式化为四位数字 String serial = String.format("%s-%04d", prefix, currentValue); return serial; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.close(); } 这段代码通过事务和行锁保证了发安全。每次生成时,先锁定对应日期的行,然后更新序号。这样即使多个实例同时操作,也能保证顺序和唯一性。 不过,这里可能存在性能瓶颈,因为每次生成都需要数据库交互。为了优化,可以预先生成一批序号,比如每次获取一个区间(如1-100),然后在内存中分配,用完后再次获取。例如,表结构中记录current_value为100,应用获取后,在内存中使用1到100,然后更新current_value为200。这样减少数据库访问次数。但需要处理应用崩溃时可能丢失未使用的序号,导致不连续。不过对于很多场景,允许偶尔的间隔,不需要绝对连续。 另外,还可以考虑使用MySQL的AUTO_INCREMENT来生成序号部分,结合触发器或者存储过程,但这样可能增加数据库的复杂度。 总结,用户的需求可以通过数据库表记录每日序号,结合事务和行锁来实现。Java代码中处理日期前缀和序号生成,确保线程安全和发控制。这种方法发量不是极的情况下是可行的,且能保证流水号的有序和唯一。</think>### 使用Java和MySQL生成有序流水号的实现方法 #### 1. 数据库表设计 创建一个存储流水号状态的表,用于记录每日序列号: ```sql CREATE TABLE serial_sequence ( date_prefix VARCHAR(8) PRIMARY KEY COMMENT '日期前缀,格式: YYYYMMDD', current_value INT NOT NULL DEFAULT 1 COMMENT '当前序列号', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); ``` #### 2. Java核心逻辑 通过事务+行级锁保证发安全: ```java public String generateSerialNumber(DataSource dataSource) throws SQLException { String prefix = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); String serialNumber; try (Connection conn = dataSource.getConnection()) { conn.setAutoCommit(false); // 使用SELECT FOR UPDATE锁定记录 try (PreparedStatement selectStmt = conn.prepareStatement( "SELECT current_value FROM serial_sequence WHERE date_prefix = ? FOR UPDATE")) { selectStmt.setString(1, prefix); ResultSet rs = selectStmt.executeQuery(); int currentValue; if (rs.next()) { currentValue = rs.getInt("current_value"); try (PreparedStatement updateStmt = conn.prepareStatement( "UPDATE serial_sequence SET current_value = ? WHERE date_prefix = ?")) { updateStmt.setInt(1, currentValue + 1); updateStmt.setString(2, prefix); updateStmt.executeUpdate(); } } else { currentValue = 1; try (PreparedStatement insertStmt = conn.prepareStatement( "INSERT INTO serial_sequence (date_prefix, current_value) VALUES (?, ?)")) { insertStmt.setString(1, prefix); insertStmt.setInt(2, currentValue); insertStmt.executeUpdate(); } } serialNumber = String.format("%s-%04d", prefix, currentValue); } conn.commit(); } return serialNumber; } ``` #### 3. 实现原理说明 - **日期前缀管理**:每天自动生成新的日期前缀(如`20231009`) - **序列号生成**:使用`SELECT ... FOR UPDATE`实现行级锁,保证事务隔离性 - **零填充格式**:通过`String.format`实现4位数字补零(如`0001`) - **自动重置机制**:日期变更时自动创建新记录实现每日重置[^1] #### 4. 性能优化建议 - **批量预取**:每次获取序号区间(如1000个),内存中分配 - **缓存机制**:使用Redis等缓存层减少数据库压力 - **连接池配置**:使用HikariCP等性能连接池 #### 5. 扩展功能实现 ```sql -- 添加业务类型字段 ALTER TABLE serial_sequence ADD COLUMN biz_type VARCHAR(20) NOT NULL DEFAULT 'DEFAULT'; ALTER TABLE serial_sequence DROP PRIMARY KEY; ALTER TABLE serial_sequence ADD PRIMARY KEY (date_prefix, biz_type); ``` 此时Java代码需同时处理`bizType`参数,实现多业务序列号隔离。
评论 42
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值