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
主要在以下情况有用:
- 批量执行脚本:当你写的 SQL 脚本包含多个 SQL 语句,并且其中包括存储过程、函数、触发器等,它们的内部也包含多个语句时,使用 DELIMITER 可以避免语法冲突。
- 存储过程、函数等复杂对象:在创建存储过程或函数时,你可能会写很多语句,这时就需要用
DELIMITER
更改语句分隔符,避免 MySQL 把语句内部的;
当作外部的语句结束符。
2.2.1.2、last_insert_id()
LAST_INSERT_ID()(无参数)
:返回当前会话中最近一次 INSERT 操作时生成的自动增量 ID。LAST_INSERT_ID(<value>)(带参数)
:将传入的值设置为当前会话的 “最后插入 ID”,并返回该值。这不会改变数据库表的内容,仅仅是设置会话的返回 ID 值。
2.2.1.3、DETERMINISTIC
在MySQL中,DETERMINISTIC
、NO SQL
和READS SQL DATA
声明通常出现在创建存储过程或函数时。他们告诉MySQL如何处理函数或过程并与**二进制日志(binlog)**记录有关,以便更高效地处理binlog
里的操作日志参数。点击查看更多
DETERMINISTIC
:表示函数或过程是纯函数或过程,即它的输出完全由输入参数确定。自MySQL 5.0.0开始,DETERMINISTIC
默认值为NOT DETERMINISTIC
。NO SQL
:表示函数或过程没有使用MySQL内置的SQL语句,而是使用其他编程语言编写的代码。READS SQL DATA
:表示函数或过程的代码包含SQL语句,并且只能读取数据而不能修改数据。
2.2.2、不保证并发安全
本方案并不能保证并发安全,原因在于,虽然UPDATE
是原子操作,但不同的会话其实是可能读到同样的序列数值,则会导致不同会话之间最终返回的新序列值相同。
那么,也不能在函数定义里尝试去添加事务相关的操作,如START TRANSACTION
、ROLLBACK
、COMMIT
等,因为不被支持,否则报错: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"));