当前位置: 首页 > news >正文

MySQL自定义序列数的实现

目录

  • 一、前言
  • 二、MySQL解决方案(InnoDB)
    • 2.1、AUTO_INCREMENT(表自增ID)
    • 2.2、FUNCTION(不能保证并发安全)
      • 2.2.1、定义FUNCTION
        • 2.2.1.1、DELIMITER
        • 2.2.1.2、last_insert_id()
        • 2.2.1.3、DETERMINISTIC
      • 2.2.2、不保证并发安全
    • 2.3、FUNCTION(可保证并发安全)
      • 2.3.1、定义FUNCTION
      • 2.3.2、调用
        • 2.3.2.1、原生SQL
        • 2.3.2.2、MyBatis
    • 2.4、PROCEDURE(可保证并发安全)
      • 2.4.1、定义PROCEDURE
      • 2.4.2、调用
        • 2.4.2.1、原生SQL
        • 2.4.2.2、MyBatis

一、前言

往往有很多情况下,我们需要使用自己生成的唯一Id或保证不重复的序列号,特别是在高并发的场景下。为此,很容易想到的实现方式有以下几种:

  • 通过原子操作和锁机制使用Java代码实现,在单JVM中维护内存中的序列数,另外需要自己保证持久化到外部存储系统;
  • 或者使用能保证分布式高并发下一致性的组件Zookeeper,借助于它的分布式锁很容易实现,缺点是需要在自己的系统或服务中引入Zookeeper的组件依赖,无疑增加了系统复杂度和可维护性(换用Redis也是同理);
  • 另一种容易想到的是,可以利用MySQL的保证事务性效果的操作,所以这种方式相比于第二种方式更加轻量。

本文将针对MySQL的解决方案(InnoDB存储引擎)进行介绍。

二、MySQL解决方案(InnoDB)

2.1、AUTO_INCREMENT(表自增ID)

  • 优点:MySQL内部对AUTO_INCREMENT自增ID,可以保证并发安全。
  • 缺点:每需要一组序列,可能就需要一张表,一张表不允许同时有多个AUTO_INCREMENT自增字段,不能同时维护多组序列,且不支持自定义的自增步长。

下文将介绍多组序列的方案,首先来创建一张序列表,并初始三组序列(步长increment均为1),如下:

DROP TABLE
    IF EXISTS sequence;
CREATE TABLE
    sequence
(
    name VARCHAR(50) NOT NULL COMMENT '序列名称',
    current_value BIGINT NOT NULL COMMENT '序列当前值',
    increment INT NOT NULL DEFAULT 1 COMMENT '序列步长',
    PRIMARY KEY (name)
) ENGINE=InnoDB;

INSERT INTO sequence VALUES ('seq1', 0, 1);
INSERT INTO sequence VALUES ('seq2', 0, 1);
INSERT INTO sequence VALUES ('seq3', 0, 1);

2.2、FUNCTION(不能保证并发安全)

2.2.1、定义FUNCTION

DROP FUNCTION IF EXISTS `seq`;
DELIMITER $$
CREATE FUNCTION `seq`(seq_name VARCHAR(50)) RETURNS BIGINT
DETERMINISTIC
BEGIN
UPDATE sequence SET current_value=last_insert_id(current_value + increment) WHERE name=seq_name;
RETURN last_insert_id();
END $$
DELIMITER ;

下面3小节将对此FUNCTION的定义作解释。

2.2.1.1、DELIMITER

DELIMITER主要在以下情况有用:

  1. 批量执行脚本:当你写的 SQL 脚本包含多个 SQL 语句,并且其中包括存储过程、函数、触发器等,它们的内部也包含多个语句时,使用 DELIMITER 可以避免语法冲突。
  2. 存储过程、函数等复杂对象:在创建存储过程或函数时,你可能会写很多语句,这时就需要用DELIMITER更改语句分隔符,避免 MySQL 把语句内部的;当作外部的语句结束符。
2.2.1.2、last_insert_id()
  1. LAST_INSERT_ID()(无参数):返回当前会话中最近一次 INSERT 操作时生成的自动增量 ID。
  2. LAST_INSERT_ID(<value>)(带参数):将传入的值设置为当前会话的 “最后插入 ID”,并返回该值。这不会改变数据库表的内容,仅仅是设置会话的返回 ID 值。
2.2.1.3、DETERMINISTIC

在MySQL中,DETERMINISTICNO SQLREADS SQL DATA声明通常出现在创建存储过程或函数时。他们告诉MySQL如何处理函数或过程并与**二进制日志(binlog)**记录有关,以便更高效地处理binlog里的操作日志参数。点击查看更多

  1. DETERMINISTIC:表示函数或过程是纯函数或过程,即它的输出完全由输入参数确定。自MySQL 5.0.0开始,DETERMINISTIC默认值为NOT DETERMINISTIC
  2. NO SQL:表示函数或过程没有使用MySQL内置的SQL语句,而是使用其他编程语言编写的代码。
  3. READS SQL DATA:表示函数或过程的代码包含SQL语句,并且只能读取数据而不能修改数据。

2.2.2、不保证并发安全

本方案并不能保证并发安全,原因在于,虽然UPDATE是原子操作,但不同的会话其实是可能读到同样的序列数值,则会导致不同会话之间最终返回的新序列值相同。

那么,也不能在函数定义里尝试去添加事务相关的操作,如START TRANSACTIONROLLBACKCOMMIT等,因为不被支持,否则报错:1422 - Explicit or implicit commit is not allowed in stored function or trigger.

2.3、FUNCTION(可保证并发安全)

2.3.1、定义FUNCTION

原理:select ... for update行锁,保证序列记录不会被同时修改。

DROP FUNCTION IF EXISTS `seq`;
DELIMITER $$

CREATE FUNCTION `seq`(seq_name VARCHAR(50)) RETURNS BIGINT
DETERMINISTIC
BEGIN
    DECLARE new_value BIGINT;

    -- 使用 FOR UPDATE 锁定该行,确保只有当前事务会修改此行
    SELECT current_value + increment INTO new_value
    FROM sequence
    WHERE name = seq_name
    FOR UPDATE;

    -- 更新 sequence 表并返回新的序列值
    UPDATE sequence 
    SET current_value = new_value
    WHERE name = seq_name;

    RETURN new_value;
END $$

DELIMITER ;

2.3.2、调用

2.3.2.1、原生SQL
select `seq`('seq1');
2.3.2.2、MyBatis
<select id="getSeqByName" parameterType="java.lang.String" resultType="long">
    select `seq`(
        #{name}
    )
</select>

2.4、PROCEDURE(可保证并发安全)

2.4.1、定义PROCEDURE

原理:在存储过程中使用事务操作保证并发安全。

DROP PROCEDURE IF EXISTS `seq`;

DELIMITER $$

CREATE PROCEDURE `seq`(IN seq_name VARCHAR(50), OUT new_value BIGINT)
BEGIN
    DECLARE cur_value BIGINT;

    -- 开始事务
    START TRANSACTION;

    -- 获取当前值
    SELECT current_value INTO cur_value FROM sequence WHERE name = seq_name FOR UPDATE;

    -- 更新序列值
    UPDATE sequence SET current_value = current_value + increment WHERE name = seq_name;

    -- 获取更新后的值
    SELECT current_value INTO new_value FROM sequence WHERE name = seq_name;

    -- 提交事务
    COMMIT;
END $$

DELIMITER ;

2.4.2、调用

2.4.2.1、原生SQL
-- 定义一个变量来存储输出值
SET @output_value = 0;

-- 调用存储过程,传入序列名称 'seq1',并将输出存储在 @output_value 变量中
CALL seq('seq1', @output_value);

-- 查看存储过程执行后的输出
SELECT @output_value AS new_value;
2.4.2.2、MyBatis

定义mapper接口:

void getSeqByName(Map<String, Object> params);

mapper接口对应的xml:

<!-- 定义参数映射 -->
<parameterMap id="seqParamMap" type="java.util.HashMap">
    <parameter property="seq_name" mode="IN" jdbcType="VARCHAR"/>
    <parameter property="new_value" mode="OUT" jdbcType="BIGINT"/>
</parameterMap>

<!-- 调用存储过程 -->
<select id="getSeqByName" parameterMap="seqParamMap" statementType="CALLABLE">
    {call seq(?, ?)}
</select>

调用存储过程,获取新序列值:

Map<String, Object> params = new HashMap<>();
params.put("seq_name", "seq1");
SpringUtil.getBean(SequenceMapper.class).getSeqByName(params);
log.info("new seq value: ", params.get("new_value"));

相关文章:

  • 【AIGC系列】6:HunyuanVideo视频生成模型部署和代码分析
  • 【Framework系列之Client】UIManager和UIPanel模块介绍
  • Go加spy++隐藏窗口
  • 梦三国2游戏下载
  • io学习------>进程
  • LeetCode 解题思路 11(Hot 100)
  • windows部署spleeter 版本2.4.0:分离音频的人声和背景音乐
  • Reverse DevTools | 逆向调试工具 使用手册
  • 机器学习深度学习基本概念:logistic regression和softmax
  • VScode 中文符号出现黄色方框的解决方法
  • 通用外设驱动模型(四步法)及配置GPIO步骤
  • 云原生系列之本地k8s环境搭建
  • 【pyqt】(十二)文本框
  • 神经网络中梯度计算求和公式求导问题
  • 2025年渗透测试面试题总结-字某某动-安全研究实习生(一面)(题目+回答)
  • BFS(七)662. 二叉树最大宽度 数组下标模拟存储二叉树
  • “深入浅出”系列之杂谈篇:(8)SQL Server 真的比不上 MySQL 吗?
  • 大模型中的剪枝、蒸馏是什么意思?
  • 【mlops】Cube Studio云原生机器学习平台架构解析
  • unity 让两个物体相遇时候刚体互不影响
  • 新加坡公布新内阁名单,黄循财继续担任总理及财政部长
  • 31只北交所主题基金齐刷净值纪录,年内最高涨超80%,已有产品打出“限购牌”
  • 新型基因编辑工具可精准嵌入完整基因
  • 越秀地产约41.49亿元出售北京海淀功德寺项目公司65%股权,此前已质押给华润置地
  • 三部门印发《2025年深入推进IPv6规模部署和应用工作要点》
  • 广州某科技公司遭网络攻击,境外“黑手”被锁定