三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

MySQL生成‘年月日+自增序号’订单号?一个timeseq函数就搞定(避坑并发问题)

MySQL生成‘年月日+自增序号’订单号?一个timeseq函数就搞定(避坑并发问题)

MySQL高并发订单号生成方案:从函数设计到分布式优化

在电商、金融等业务系统中,订单号的生成看似简单,实则暗藏玄机。一个典型的订单号往往由日期前缀和自增序号组成,例如"202405200015"。这种格式既能体现时间顺序,又能保证唯一性。但当你真正在MySQL中实现时,会发现并发场景下的序号重复、跳号等问题接踵而至。

1. 订单号生成的核心挑战与设计原则

订单号生成看似只是简单的字符串拼接,但要满足生产环境要求,必须考虑以下几个核心指标:

  • 全局唯一性:这是最基本的要求,任何两个订单号不能相同
  • 有序性:通常希望订单号能反映时间顺序,便于查询和归档
  • 可读性:包含日期等有意义的信息,便于人工识别
  • 高性能:在高并发下仍能保持稳定的生成速度
  • 可扩展性:随着业务增长,方案能够平滑扩展

传统的自增ID虽然简单,但无法满足可读性要求;而UUID虽然唯一,但完全无序且过长。因此,"日期+序号"的组合方案成为了业务系统的常见选择。

2. MySQL原生函数方案实现

我们先来看一个基于MySQL函数的基础实现方案。这个方案不需要引入外部组件,适合中小型系统。

2.1 创建序列管理表

首先需要创建一个表来管理各种序列:

CREATE TABLE `sequence_manager` ( `seq_name` varchar(50) NOT NULL COMMENT '序列名称', `current_val` bigint NOT NULL COMMENT '当前值', `step` int NOT NULL DEFAULT 1 COMMENT '步长', `max_val` bigint DEFAULT NULL COMMENT '最大值', `padding_length` int NOT NULL DEFAULT 4 COMMENT '填充位数', `date_format` varchar(20) DEFAULT '%Y%m%d' COMMENT '日期格式', PRIMARY KEY (`seq_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2.2 实现序列生成函数

接下来创建三个核心函数:

DELIMITER $$ CREATE FUNCTION `next_seq_val`(seq_name VARCHAR(50)) RETURNS BIGINT BEGIN DECLARE next_val BIGINT; UPDATE sequence_manager SET current_val = current_val + step WHERE seq_name = seq_name; SELECT current_val INTO next_val FROM sequence_manager WHERE seq_name = seq_name; RETURN next_val; END$$ DELIMITER ; DELIMITER $$ CREATE FUNCTION `generate_formatted_id`(seq_name VARCHAR(50)) RETURNS VARCHAR(100) BEGIN DECLARE formatted_id VARCHAR(100); DECLARE padding_len INT; DECLARE date_part VARCHAR(20); SELECT DATE_FORMAT(NOW(), date_format), padding_length INTO date_part, padding_len FROM sequence_manager WHERE seq_name = seq_name; SELECT CONCAT( date_part, LPAD(next_seq_val(seq_name), padding_len, '0') ) INTO formatted_id; RETURN formatted_id; END$$ DELIMITER ;

2.3 并发安全优化

上述基础实现在高并发下会出现问题,我们需要添加事务和锁机制:

DELIMITER $$ CREATE FUNCTION `safe_generate_id`(seq_name VARCHAR(50)) RETURNS VARCHAR(100) BEGIN DECLARE formatted_id VARCHAR(100); START TRANSACTION; -- 使用SELECT FOR UPDATE加锁 SELECT current_val INTO @dummy FROM sequence_manager WHERE seq_name = seq_name FOR UPDATE; SET formatted_id = generate_formatted_id(seq_name); COMMIT; RETURN formatted_id; END$$ DELIMITER ;

注意:在高并发场景下,这种行锁方式可能导致性能瓶颈,需要根据实际业务压力评估

3. 高并发场景下的性能优化

当QPS达到数百甚至上千时,上述方案会遇到明显的性能瓶颈。以下是几种优化思路:

3.1 批量预分配序列号

一次性获取一批序号,减少数据库交互:

DELIMITER $$ CREATE PROCEDURE `allocate_seq_batch`( IN p_seq_name VARCHAR(50), IN p_batch_size INT, OUT p_start_val BIGINT, OUT p_end_val BIGINT ) BEGIN START TRANSACTION; SELECT current_val INTO p_start_val FROM sequence_manager WHERE seq_name = p_seq_name FOR UPDATE; SET p_end_val = p_start_val + p_batch_size - 1; UPDATE sequence_manager SET current_val = current_val + p_batch_size WHERE seq_name = p_seq_name; COMMIT; END$$ DELIMITER ;

应用层可以定期调用此存储过程,预分配一批序号缓存在内存中。

3.2 分段锁优化

将序列分成多个段,减少锁竞争:

CREATE TABLE `segment_sequence` ( `seq_name` varchar(50) NOT NULL, `segment` int NOT NULL COMMENT '分段编号', `current_val` bigint NOT NULL, `step` int NOT NULL DEFAULT 1000 COMMENT '每段大小', PRIMARY KEY (`seq_name`, `segment`) ) ENGINE=InnoDB;

3.3 性能对比

方案QPS上限优点缺点
基础方案~500实现简单锁竞争严重
批量预分配~3000减少DB交互可能浪费序号
分段锁~5000高并发性能好实现复杂

4. 分布式环境下的进阶方案

对于大型分布式系统,MySQL方案可能不再适用,需要考虑以下替代方案:

4.1 Redis原子计数器

Redis的INCR命令是原子操作,非常适合序号生成:

import redis r = redis.Redis(host='localhost', port=6379) def generate_order_id(): date_str = datetime.now().strftime('%Y%m%d') seq = r.incr(f'order_seq:{date_str}') return f"{date_str}{str(seq).zfill(6)}"

4.2 雪花算法(Snowflake)

雪花算法生成的是64位的ID,结构如下:

0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000

1位符号位 + 41位时间戳 + 10位机器ID + 12位序列号

Java实现示例:

public class SnowflakeIdGenerator { private final long twepoch = 1288834974657L; private final long workerIdBits = 5L; private final long datacenterIdBits = 5L; private final long maxWorkerId = -1L ^ (-1L << workerIdBits); private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); private final long sequenceBits = 12L; private final long workerIdShift = sequenceBits; private final long datacenterIdShift = sequenceBits + workerIdBits; private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; private final long sequenceMask = -1L ^ (-1L << sequenceBits); private long workerId; private long datacenterId; private long sequence = 0L; private long lastTimestamp = -1L; public SnowflakeIdGenerator(long workerId, long datacenterId) { // 初始化代码... } public synchronized long nextId() { // 生成ID逻辑... } }

4.3 方案选型建议

根据业务特点选择合适的方案:

  • 中小型系统:MySQL函数方案足够,实现简单
  • 高并发但单数据中心:Redis方案性能优异
  • 大型分布式系统:雪花算法更适合,但需要解决时钟回拨问题
  • 需要严格单调递增:可以考虑美团Leaf等方案

在实际项目中,我曾遇到过MySQL方案在QPS达到800时出现明显延迟的情况。后来我们迁移到Redis方案,性能提升了10倍以上。但对于金融类业务,我们最终选择了基于ZK的分布式序列服务,因为需要保证绝对的顺序和唯一性。

← 返回列表