前言:滴滴通过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;}// 获取到初始的idlong 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) {// 避免浪费 减掉系统自己占用的一个idcurrentId.addAndGet(0 - delta);isInit = true;return;}}}}public Result nextId() {// 通过 id % delta == remainder 最终得到 初始的id 值init();// 后续的id 可以直接增加设置好的delta 得到下一idlong 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 :
@Overridepublic 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;@Overrideprotected 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 {@Overridepublic 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