MySQL没有Sequence,我们可以用两种方法解决:
- 做一张独立的表,只含有一个id自增字段,模拟Sequence;
- 写一个通用的Sequence表,一条记录表示一个Sequence,业务可共用。
我们对这两种情况提供实现和效率测试。
实现过程基于Spring Boot v1.2.5 / MyBatis v3.2 / MySQL v5.1。测试使用JUnit进行并发请求模拟。
一、自增字段模拟Sequence
1. 建表
SQL:
1 2 3 4 5 | CREATE TABLE AUTOINCREMENT_SEQ ( id INT NOT NULL AUTO_INCREMENT, PRIMARY KEY (id) ); |
3. 实体定义
Java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | package info.kimiazhu.demo.model; public class AutoincrementSeq { private int id; public int getId() { return id; } public void setId(int id) { this.id = id; } } |
3. Insert语句
AutoincrementSeqMapper.xml:
1 2 3 | <insert id="insert" useGeneratedKeys="true" keyProperty="id" parameterType="info.kimiazhu.demo.model.AutoincrementSeq"> INSERT INTO AUTOINCREMENT_SEQ () VALUES () </insert> |
Insert语句在插入之后返回主键设入实体中,以后要使用这个ID就调用实体的get方法获取。
二、建一张通用的Sequence表
1. 建表
这里使用了MySQL用户自定义函数来定义了三个函数:currval/nextval/setval。
SQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | CREATE TABLE SEQUENCE ( name VARCHAR(64) NOT NULL, current_value BIGINT NOT NULL, step INT NOT NULL DEFAULT 1, PRIMARY KEY (name) ); ALTER TABLE SEQUENCE COMMENT 'Sequence自增表'; DROP FUNCTION IF EXISTS currval; DELIMITER $$ CREATE FUNCTION currval (seq_name VARCHAR(64)) RETURNS INTEGER CONTAINS SQL BEGIN DECLARE value INTEGER; SET value = 0; SELECT current_value INTO value FROM SEQUENCE WHERE name = seq_name; RETURN value; END $$ DELIMITER ; DROP FUNCTION IF EXISTS nextval; DELIMITER $$ CREATE FUNCTION nextval (seq_name VARCHAR(64)) RETURNS INTEGER CONTAINS SQL BEGIN UPDATE SEQUENCE SET current_value = last_insert_id(current_value + step) WHERE name = seq_name; RETURN last_insert_id(); END; $$ DELIMITER ; DROP FUNCTION IF EXISTS setval; DELIMITER $$ CREATE FUNCTION setval (seq_name VARCHAR(64), value INTEGER) RETURNS INTEGER CONTAINS SQL BEGIN UPDATE SEQUENCE SET current_value = value WHERE name = seq_name; RETURN currval(seq_name); END; $$ DELIMITER ; |
2. 初始化数据
以APP_ID为例,我们规定APP_ID每次自动生成,但它不是数据库表中的主键,所以我们准备使用Sequence表中的一条记录来生成。
SQL:
1 | INSERT INTO sequence(name, current_value, step) VALUES ("APP_ID", 0, 1); |
3. Mapper语句
SequenceMapper.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <mapper namespace="info.kimiazhu.demo.mapper.SequenceMapper"> <select id="nextValue" resultType="int"> select nextval(#{0}); </select> <select id="currentValue" resultType="int"> select currval(#{0}); </select> <select id="setValue" resultType="int"> select setval(#{0}, #{1}); </select> </mapper> |
三、测试
测试代码使用JUnit,加入GroboUtils进行多线程并发模拟next_value请求。起100个线程,每个线程内部请求100次next_value。
1. 自增表的测试用例:
AutoincrementSeqMapperTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | package info.kimiazhu.demo.mapper; import org.junit.Test; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import info.kimiazhu.demo.DemoApplication; import info.kimiazhu.demo.model.AutoincrementSeq; import net.sourceforge.groboutils.junit.v1.MultiThreadedTestRunner; import net.sourceforge.groboutils.junit.v1.TestRunnable; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = DemoApplication.class) public class AutoincrementSeqMapperTest { private static final Logger LOGGER = LoggerFactory.getLogger(AutoincrementSeqMapperTest.class); @Autowired private AutoincrementSeqMapper autoincrementSeqMapper; @Test public void testInsert() throws Exception { TestRunnable runner = new TestRunnable() { @Override public void runTest() throws Throwable { for(int i = 0; i < 100; i++) { AutoincrementSeq autoincrementSeq = new AutoincrementSeq(); autoincrementSeqMapper.insert(autoincrementSeq); } } }; int runnerCount = 100; TestRunnable[] runners = new TestRunnable[runnerCount]; for (int i = 0; i < runnerCount; i++) { runners[i] = runner; } MultiThreadedTestRunner mttr = new MultiThreadedTestRunner(runners); long start = System.currentTimeMillis(); try { mttr.runTestRunnables(); } catch (Throwable t) { LOGGER.error("Error occur", t); } LOGGER.info("elapse time: " + ((System.currentTimeMillis() - start)/1000.f) + "s"); } } |
2. Sequence表测试
SequenceMapperTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | package info.kimiazhu.demo.mapper; import org.junit.Test; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import info.kimiazhu.demo.DemoApplication; import info.kimiazhu.demo.mapper.SequenceMapper.SEQ_NAME; import net.sourceforge.groboutils.junit.v1.MultiThreadedTestRunner; import net.sourceforge.groboutils.junit.v1.TestRunnable; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = DemoApplication.class) public class SequenceMapperTest { private static final Logger LOGGER = LoggerFactory.getLogger(SequenceMapperTest.class); @Autowired private SequenceMapper sequenceMapper; @Test public void testNextValue() throws Exception { TestRunnable runner = new TestRunnable() { @Override public void runTest() throws Throwable { for(int i = 0; i < 100; i++) { sequenceMapper.nextValue(SEQ_NAME.APP_ID); } } }; int runnerCount = 100; TestRunnable[] runners = new TestRunnable[runnerCount]; for (int i = 0; i < runnerCount; i++) { runners[i] = runner; } MultiThreadedTestRunner mttr = new MultiThreadedTestRunner(runners); long start = System.currentTimeMillis(); try { mttr.runTestRunnables(); } catch (Throwable t) { LOGGER.error("Error occur", t); } LOGGER.info("elapse time: " + ((System.currentTimeMillis() - start)/1000.f) + "s"); } } |
3. 测试结果
- 执行1万次next_value操作,自增表的方式消耗25s-26s
- 执行1万次next_value操作,自建Sequence表的方式消耗18s-19s
- 基本上内建函数的Sequence方式是自增表方式效率提升1/4
- 内建函数的方式并不存在并发问题。
- 更正,关于效率的测试是有问题的,sequence表的方式效率会比较低,需要一个高效的实现方式,我们后来做了优化,在一次性取出步长为N的长度,然后在内存中进行分配。当N条都用完之后,再从数据库Sequence表中申请。
三、结论
自建Sequence和内建函数的方式更为通用,当有多个业务需要自增序列的时候,一个表就可以满足需求,并且不会因为使表的记录数暴增。
独立建一张表,用自增序列模拟的方式实现较简单,不依赖数据库内建函数,迁移成本较小。但是效率相对低一些。