需求背景

有个三级目录(例如国家-省份-城市)的数据要以树的形式展示,由于每一级都是独立的实体,需要单独一张表存储,不同级别的id会重复,于是前端提意见要求整棵树不同级别的id都不能重复,不然他们的组件会出问题.于是只好研究一下多表统一主键的方案.

Snowflake雪花算法

虽然很自然就想到了分布式id中的雪花算法,不过我们对分布式场景和id严格自增没有要求,这个方案麻烦,没有必要.

UUID

从唯一性想到uuid也是很自然的. MySQL5.7不支持建表时使用函数,可以用触发器替代.

CREATE TABLE FOO (
  id CHAR(36) PRIMARY KEY
);



DELIMITER ;;
CREATE TRIGGER `foo_before_insert` 
BEFORE INSERT ON `foo` FOR EACH ROW 
BEGIN
  IF new.id IS NULL THEN
    SET new.id = uuid();
  END IF;
END;;
DELIMITER ;

不过我们很少让MySQL自己生成uuid,数据库的资源还是很宝贵的,这应该让应用自己生成. 此外UUID当主键的利弊也讨论很充分了,占用空间以及插入慢,适用于数据少的情况.

序列表

思路是另外专门用一张表存放自增的id,分表新建数据的时候来这里拿号.

## 创建序列表
DROP TABLE IF EXISTS `sequence`;
CREATE TABLE `sequence` (
  `tablename` varchar(64) NOT NULL,
  `nextid` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`tablename`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


## 创建主键策略函数
DROP FUNCTION IF EXISTS `auto_seq`;
CREATE DEFINER = `root`@`localhost` FUNCTION `auto_seq`(tb_purpose VARCHAR(64))
 RETURNS int(11)
BEGIN
DECLARE reid int; 
set reid = (select `nextid` from `sequence` where `tablename`=tb_purpose limit 1);
update `sequence` set `nextid`=reid + 1;
return reid;
end;

## 在分表中创建触发器,此触发器为插入前触发,调用主键策略函数返回的结果赋予id
DROP TRIGGER `auto_seq`;
 
CREATE TRIGGER `auto_seq` BEFORE INSERT ON `item`
FOR EACH ROW begin
set new.id = auto_seq('item');
end;

缺点是多张表同时写入的时候存在竞争.

间隔插入

上面的做法是插入数据时多张表排同一条队,其实没必要,只要提前约定每张表拿哪个id就可以保证不重复.比如有N张表,那么N个id为一个循环,第一个位置留给表A,第二个位置留给表B…. 这可以通过设置MySQL生成id的步进和id初始值即可.


## 设置步长为3
SET @@auto_increment_increment=3;

## 错开分表的初始id
CREATE TABLE `A` (ID INT PRIMARY KEY AUTO_INCREMENT) AUTO_INCREMENT=1;
CREATE TABLE `B` (ID INT PRIMARY KEY AUTO_INCREMENT) AUTO_INCREMENT=2;
CREATE TABLE `C` (ID INT PRIMARY KEY AUTO_INCREMENT) AUTO_INCREMENT=3;

不过这个步长控制的粒度是库级别,会影响别的表.

分段插入

更进一步,直接划分多个大的区段就可以避免设置步进带来的问题.

## 错开分表的初始id
CREATE TABLE `A` (ID INT PRIMARY KEY AUTO_INCREMENT) AUTO_INCREMENT=1;
CREATE TABLE `B` (ID INT PRIMARY KEY AUTO_INCREMENT) AUTO_INCREMENT=100000;
CREATE TABLE `C` (ID INT PRIMARY KEY AUTO_INCREMENT) AUTO_INCREMENT=10000000;

表A的id范围是1~99999,表B是100000~9999999,表C是10000000~MAX,只要能保证数据量不是非常大以至于耗尽约定的范围就行,这个方法最简单,我们的各级目录的数量又是很有限的,所以就用了这个方法. 另外用这个方案时要记得写清楚注释,不然别人手抖改了AUTO_INCREMENT就出问题了.