前言:滴滴通过mysql来定义好id 的初始值和增长的步长,每次可以将一段连续的数字id取出放入到内存中,当需要使用id 的使用,每次id+1 ,如果发现id 的值已经超出了改段最大的id 值,则取下个段的id 继续使用;本文将简单介绍id生成的实现并将其与Mybatis-plus进行整合。
滴滴id 的设计思路:https://github.com/didi/tinyid/wiki
1 tinyid 实现概述:
关于tinyid 的设计思路在https://github.com/didi/tinyid/wiki 中已经阐述的比较清楚了,本文只对个别关注点做概述;
1.1 table:
tinyid 设计时创建了两张表,tiny_id_token和tiny_id_info;
- tiny_id_token:这个主要做了一层简单的token 检验,在向tinyid 服务获取id端和id时,根据业务类型做了一次校验;
- tiny_id_info:这个表是id段使用的表:表中字段的标识也比较清楚,通过biz_type 来进行业务的隔离,max_id 来作为下一段id的起始值,step 表明每次要取的id 长度;version 做更新的并发控制;
CREATE TABLE `tiny_id_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`biz_type` varchar(63) NOT NULL DEFAULT '' COMMENT '业务类型,唯一',
`begin_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '开始id,仅记录初始值,无其他含义。初始化时begin_id和max_id应相同',
`max_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '当前最大id',
`step` int(11) DEFAULT '0' COMMENT '步长',
`delta` int(11) NOT NULL DEFAULT '1' COMMENT '每次id增量',
`remainder` int(11) NOT NULL DEFAULT '0' COMMENT '余数',
`create_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '更新时间',
`version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_biz_type` (`biz_type`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='id信息表';
-
delta和remainder 则决定了Tinyid 使用db 的服务数上限;以及解决多台Tinyid 服务 号段相同的问题;如果现在有5台Tinyid 的生成服务分别连接5个db,每次id的增量步长都为5,如果号段都是100到200,初始id 都是100 则生成id 如下:
|db | Server | delta | remainder | 生成的id |
|db1 | server1 | 5 | 0 | 100 |
| db2| server2 | 5 | 1 | 101 |
| db3| server3 | 5 | 2 | 102 |
| db4| server4 | 5 | 3 | 103 |
| db5| server5 | 5 | 4 | 104 | -
通过remainder 余数控制id 的生成,可以使得多台服务即使id段相同,但最终生成的id 是不重复的;
通过 id 生成的方法 可以清楚的看到 如何通过余数控制 的生成:每次得到新的id段,都进入init 控制 初始值currentId 的生成
public void init() {
if (isInit) {
return;
}
synchronized (this) {
if (isInit) {
return;
}
// 获取到初始的id
long id = currentId.get();
// 通过 id % delta == remainder 最终得到 初始的id 值
if (id % delta == remainder) {
isInit = true;
return;
}
for (int i = 0; i <= delta; i++) {
id = currentId.incrementAndGet();
if (id % delta == remainder) {
// 避免浪费 减掉系统自己占用的一个id
currentId.addAndGet(0 - delta);
isInit = true;
return;
}
}
}
}
public Result nextId() {
// 通过 id % delta == remainder 最终得到 初始的id 值
init();
// 后续的id 可以直接增加设置好的delta 得到下一id
long id = currentId.addAndGet(delta);
// 如果id 超出 id 段
if (id > maxId) {
return new Result(ResultCode.OVER, id);
}
// id 超出预设的一定值,则加载下一个id 段
if (id >= loadingId) {
return new Result(ResultCode.LOADING, id);
}
return new Result(ResultCode.NORMAL, id);
}
1.2 当通过client 获取 Tinyid 的id 段时,如果多个db数据库,则会随机选择一个db 数据库,获取id段:
客户端选择服务端url HttpSegmentIdServiceImpl :
@Override
public SegmentId getNextSegmentId(String bizType) {
// 服务端url获取
String url = chooseService(bizType);
String response = TinyIdHttpUtils.post(url, TinyIdClientConfig.getInstance().getReadTimeout(),
TinyIdClientConfig.getInstance().getConnectTimeout());
logger.info("tinyId client getNextSegmentId end, response:" + response);
if (response == null || "".equals(response.trim())) {
return null;
}
SegmentId segmentId = new SegmentId();
String[] arr = response.split(",");
segmentId.setCurrentId(new AtomicLong(Long.parseLong(arr[0])));
segmentId.setLoadingId(Long.parseLong(arr[1]));
segmentId.setMaxId(Long.parseLong(arr[2]));
segmentId.setDelta(Integer.parseInt(arr[3]));
segmentId.setRemainder(Integer.parseInt(arr[4]));
return segmentId;
}
private String chooseService(String bizType) {
List<String> serverList = TinyIdClientConfig.getInstance().getServerList();
String url = "";
if (serverList != null && serverList.size() == 1) {
url = serverList.get(0);
} else if (serverList != null && serverList.size() > 1) {
Random r = new Random();
url = serverList.get(r.nextInt(serverList.size()));
}
url += bizType;
return url;
}
server 端 DynamicDataSource:
public class DynamicDataSource extends AbstractRoutingDataSource {
private List<String> dataSourceKeys;
@Override
protected Object determineCurrentLookupKey() {
if(dataSourceKeys.size() == 1) {
return dataSourceKeys.get(0);
}
// 随机获取一个id db
Random r = new Random();
return dataSourceKeys.get(r.nextInt(dataSourceKeys.size()));
}
public List<String> getDataSourceKeys() {
return dataSourceKeys;
}
public void setDataSourceKeys(List<String> dataSourceKeys) {
this.dataSourceKeys = dataSourceKeys;
}
}
2 Mybatis-plus整合:
对于Mybatis-plus 整合也比较简单,因为在实体中当 id 使用Mybatis-plus 自带的雪花算法id 生成时,最终会通过package com.baomidou.mybatisplus.core.incrementer DefaultIdentifierGenerator 的 nextId 获取下一id ,所以只要遵从Mybatis-plus 雪花算法id 的生成规范,重写掉DefaultIdentifierGenerator 的nextId 方法即可实现 id 的自定义生成;
2.1 实体中需要通过TableId 修饰
@TableId(value = "id", type = IdType.ASSIGN_ID )
private String id;
2.2 map层需要继承BaseMapper:
@Repository
public interface UserMapper extends BaseMapper<User> {
}
2.3 创建package com.baomidou.mybatisplus.core.incrementer 包,并创建DefaultIdentifierGenerator 类:重写nextId 方法,通过
TinyId生成id主键。
package com.baomidou.mybatisplus.core.incrementer;
import com.alibaba.fastjson2.JSONObject;
import com.xiaoju.uemc.tinyid.client.utils.TinyId;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
@Slf4j
public class DefaultIdentifierGenerator implements IdentifierGenerator {
@Override
public Long nextId(Object entity) {
Object o = null;
if (null != entity) {
Map<String, Object> eMap = JSONObject.parseObject(JSONObject.toJSONString(entity), Map.class);
if (eMap.containsKey("bizType")) {
o = eMap.get("bizType");
}
}
if (null == o) {
o = "test";
}
Long id = TinyId.nextId(o.toString());
return id;
}
}
git 参考地址:https://codeup.aliyun.com/61cd21816112fe9819da8d9c/dd-uid.git