如何通过MySQL存储过程高效实现基于原子操作的唯一序列号长尾词生成?

2026-04-27 18:432阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计750个文字,预计阅读时间需要3分钟。

如何通过MySQL存储过程高效实现基于原子操作的唯一序列号长尾词生成?

很多人第一次回应是:

UPDATE ... SET current_value = @next := current_value + 1 是最稳的原子写法

真正能扛并发、防重复的核心,是把读+写合并成一条 UPDATE,利用 MySQL 的行级锁和原子赋值。下面这段才是生产可用的最小可靠单元:

SET @next := 0; UPDATE sequences SET current_value = @next := current_value + 1 WHERE seq_name = 'order_no' RETURNING @next;

注意三点:

  • RETURNING 在 MySQL 8.0.23+ 才支持;低版本必须拆成两步:先 SELECT ... FOR UPDATE,再 UPDATE,且必须在同一个事务里
  • sequences 表的 seq_name 字段必须建唯一索引,否则 WHERE 条件可能锁住多行,引发死锁
  • 别用 INT 存大序列号——万一超 21 亿,直接溢出变负数;建议用 BIGINT

带日期前缀的流水号,千万别在存储过程里查最大值

SELECT MAX(order_no) FROM orders WHERE order_no LIKE 'NO20260411%' 这种写法,表面看逻辑清晰,实则灾难:全表扫描 + LIKE 无法走索引 + 并发时多个会话同时读到相同最大值,必然冲突。

正确做法是把「日期段」也作为序列键的一部分:

  • 建表时让 seq_name 包含日期,例如 'order_no_20260411'
  • 存储过程里用 DATE_FORMAT(NOW(), '%Y%m%d') 动态拼 seq_name
  • 首次调用自动插入默认值(INSERT IGNORE 或先 SELECTINSERT

这样每条记录只锁自己那一行,完全规避竞争。

MyBatis 调用存储过程时,@Options(statementType = StatementType.CALLABLE) 不够

光加这个注解只是告诉 MyBatis “这是个存储过程”,但没解决参数绑定和结果获取的实际问题。常见错误包括:

  • OUT 参数没声明为 mode=OUT,导致 Java 端收不到返回值
  • MySQL 存储过程里用了 SELECT 直接输出结果,而 MyBatis 默认只取第一个结果集,容易漏掉 OUT
  • 没设 fetchSize = -1,批量调用时连接被占满

推荐写法:

@Select("{CALL generate_serial_number(#{prefix, mode=IN}, #{suffix, mode=IN}, #{result, mode=OUT, jdbcType=VARCHAR})}") @Options(statementType = StatementType.CALLABLE, fetchSize = -1)

对应存储过程参数必须严格匹配 OUT 变量名和类型,且 Java 方法参数要用 @Param("result") 显式接收。

真正难的不是写出来,而是确认每个环节都落在「单行更新 + 显式锁 + 无隐式依赖」这条线上。任何试图绕开行锁、依赖应用层计数、或混用多种生成逻辑的地方,上线后早晚出重号。

标签:Mysql

本文共计750个文字,预计阅读时间需要3分钟。

如何通过MySQL存储过程高效实现基于原子操作的唯一序列号长尾词生成?

很多人第一次回应是:

UPDATE ... SET current_value = @next := current_value + 1 是最稳的原子写法

真正能扛并发、防重复的核心,是把读+写合并成一条 UPDATE,利用 MySQL 的行级锁和原子赋值。下面这段才是生产可用的最小可靠单元:

SET @next := 0; UPDATE sequences SET current_value = @next := current_value + 1 WHERE seq_name = 'order_no' RETURNING @next;

注意三点:

  • RETURNING 在 MySQL 8.0.23+ 才支持;低版本必须拆成两步:先 SELECT ... FOR UPDATE,再 UPDATE,且必须在同一个事务里
  • sequences 表的 seq_name 字段必须建唯一索引,否则 WHERE 条件可能锁住多行,引发死锁
  • 别用 INT 存大序列号——万一超 21 亿,直接溢出变负数;建议用 BIGINT

带日期前缀的流水号,千万别在存储过程里查最大值

SELECT MAX(order_no) FROM orders WHERE order_no LIKE 'NO20260411%' 这种写法,表面看逻辑清晰,实则灾难:全表扫描 + LIKE 无法走索引 + 并发时多个会话同时读到相同最大值,必然冲突。

正确做法是把「日期段」也作为序列键的一部分:

  • 建表时让 seq_name 包含日期,例如 'order_no_20260411'
  • 存储过程里用 DATE_FORMAT(NOW(), '%Y%m%d') 动态拼 seq_name
  • 首次调用自动插入默认值(INSERT IGNORE 或先 SELECTINSERT

这样每条记录只锁自己那一行,完全规避竞争。

MyBatis 调用存储过程时,@Options(statementType = StatementType.CALLABLE) 不够

光加这个注解只是告诉 MyBatis “这是个存储过程”,但没解决参数绑定和结果获取的实际问题。常见错误包括:

  • OUT 参数没声明为 mode=OUT,导致 Java 端收不到返回值
  • MySQL 存储过程里用了 SELECT 直接输出结果,而 MyBatis 默认只取第一个结果集,容易漏掉 OUT
  • 没设 fetchSize = -1,批量调用时连接被占满

推荐写法:

@Select("{CALL generate_serial_number(#{prefix, mode=IN}, #{suffix, mode=IN}, #{result, mode=OUT, jdbcType=VARCHAR})}") @Options(statementType = StatementType.CALLABLE, fetchSize = -1)

对应存储过程参数必须严格匹配 OUT 变量名和类型,且 Java 方法参数要用 @Param("result") 显式接收。

真正难的不是写出来,而是确认每个环节都落在「单行更新 + 显式锁 + 无隐式依赖」这条线上。任何试图绕开行锁、依赖应用层计数、或混用多种生成逻辑的地方,上线后早晚出重号。

标签:Mysql