MySQL自定义序列数的实现

一、前言

往往有很多情况下,我们需要使用自己生成的唯一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"));
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DayDayUp丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值