分布式ID的生成方案有很多种,在网上都可以搜到。在这里详细介绍一下我们目前项目所用到的实际方案。
一. ID值获取及表结构设计
(1)获取ID值的接口
获取唯一ID的接口方法是由我们一个基础功能微服务提供,URL如下:
<IP>:<Port>/sequence/getSequence
请求参数为:String prefix(ID值的类型,一种类型对应ID值的一个前缀,比如TEST_NO对应ID前缀NO,只是起一个对应关系)
eg:入参prefix=TEST_NO,返回新ID: NO2018073000000054
入参prefix=REQ_MY,返回新ID:MY2018073000000054
(2)ID值组成
比如:NO2018073000000054
最前面的NO为设置的ID前缀,紧接着是生成的年月日(20180730, 共8位),最后几位是递增的数值(总长度为下表定义的seq_len值)。具体看下面二中的test_currVal存储过程的实现。
(3)表结构分析
涉及的表就一张,结构如下:
CREATE TABLE `id_sequence` (
`name` varchar(50) COLLATE utf8mb4_bin NOT NULL,
`current_value` int(11) NOT NULL,
`increment` int(11) NOT NULL DEFAULT '5',
`cur_date` date DEFAULT NULL,
`default_value` int(11) DEFAULT NULL,
`seq_len` int(8) DEFAULT NULL,
`prefix` varchar(10) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
其中,
name:分配器名字,表示有多个用于分配ID值的分配器;
current_value:当前ID值取的值;
increment:固定步长,这里是5;
cur_date:当前日期,用于ID值的中间几位;
default_value:刚插入数据时的默认初始值;
seq_len:ID值最后递增位数的长度;
prefix:ID值的前缀;
(4)往该表预插入数据
INSERT INTO `id_sequence` (`name`, `current_value`, `increment`, `cur_date`, `default_value`, `seq_len`, `prefix`) VALUES ('TEST_NO_0', 1, 5, '2018-07-24', 1, 8, 'NO');
INSERT INTO `id_sequence` (`name`, `current_value`, `increment`, `cur_date`, `default_value`, `seq_len`, `prefix`) VALUES ('TEST_NO_1', 2, 5, '2018-07-24', 2, 8, 'NO');
INSERT INTO `id_sequence` (`name`, `current_value`, `increment`, `cur_date`, `default_value`, `seq_len`, `prefix`) VALUES ('TEST_NO_2', 3, 5, '2018-07-24', 3, 8, 'NO');
INSERT INTO `id_sequence` (`name`, `current_value`, `increment`, `cur_date`, `default_value`, `seq_len`, `prefix`) VALUES ('TEST_NO_3', 4, 5, '2018-07-24', 4, 8, 'NO');
INSERT INTO `id_sequence` (`name`, `current_value`, `increment`, `cur_date`, `default_value`, `seq_len`, `prefix`) VALUES ('TEST_NO_4', 5, 5, '2018-07-24', 5, 8, 'NO');
表数据:
name | current_value | increment | cur_date | default_value | seq_len | prefix |
TEST_NO_0 | 1 | 5 | 2018-07-24 | 1 | 8 | NO |
TEST_NO_1 | 2 | 5 | 2018-07-24 | 2 | 8 | NO |
TEST_NO_2 | 3 | 5 | 2018-07-24 | 3 | 8 | NO |
TEST_NO_3 | 4 | 5 | 2018-07-24 | 4 | 8 | NO |
TEST_NO_4 | 5 | 5 | 2018-07-24 | 5 | 8 | NO |
可以看出,这里采用的方案是根据不同节点固定步长的方式。
(5)现通过(1)的接口新增一个ID:NO2018072400000008,此时表数据变成:
name | current_value | increment | cur_date | default_value | seq_len | prefix |
TEST_NO_0 | 1 | 5 | 2018-07-24 | 1 | 8 | NO |
TEST_NO_1 | 2 | 5 | 2018-07-24 | 2 | 8 | NO |
TEST_NO_2 | 8 | 5 | 2018-07-24 | 3 | 8 | NO |
TEST_NO_3 | 4 | 5 | 2018-07-24 | 4 | 8 | NO |
TEST_NO_4 | 5 | 5 | 2018-07-24 | 5 | 8 | NO |
可以看到,只有TEST_NO_2的current_value由3变成了8(因为步长increment为5),而取TEST_NO_2这条数是随机的。
二. 接口实现
获取ID接口的实现,其实就是调用test_nextVal存储过程来获取一个新ID。
首先看:
1. test_nextVal
BEGIN
DECLARE seqName VARCHAR(64);
set @seqName=CONCAT(seq_name,"_",ROUND(RAND()*4));
UPDATE id_sequence SET current_value = if (cur_date=current_date,current_value + increment,defult_value),cur_date=current_date WHERE name = @seqName;
RETURN test_currVal(@seqName);
END
可以看出,
(1)test_nextVal的入参是seq_name,这里传的是“TEST_NO”。而seqName=CONCAT(seq_name,"_",ROUND(RAND()*4))的作用是根据seq_name和随机生成(0-4)的数拼接成name值,这个作为后面更新的name条件值,此时就确定了要获取哪条数据(TEST_NO_0到TEST_NO_4中一条)的值。
(2)而UPDATE语句则是判断cur_date是不是当前日期(current_date是mysql函数),如果是,则current_value取当前值加上步长(increment=5,这样保证当天取的ID值都是从当前值进行递增),如果不是,则current_value取默认值(default_value,也即初始值,这样保证新一天取的ID值都是从初始值开始递增)。
(3)将seqName作为参数传入test_currVal存储过程里。
2. test_currVal
BEGIN
DECLARE retVal VARCHAR(64);
SET retval="-999999999";
SELECT CONCAT(prefix,DATE_FORMAT(CURRENT_DATE,'%Y%m%d'),LPAD(CAST(current_value AS CHAR),seq_len,0)) INTO retVal FROM id_sequence WHERE name = seq_name;
RETURN retVal;
END
在这里,
(1)定义一个VARCHAR(64)的retVal值,默认值是-999999999,只有出错情况下才返回该值。
(2)CONCAT处就定义了ID值是怎么组成的:
前缀prefix(这里是NO) + 年月日(DATE_FORMAT(CURRENT_DATE,'%Y%m%d')) + seq_len长度的递增数字(LPAD是mysql填充字符串函数,总长为seq_len=8,不够长度8时,左边以0补充;而CAST是mysql转换函数,将current_value转为CHAR类型)