TINYID介绍
项目地址:https://github.com/didi/tinyid
Tinyid
是滴滴开发的一款分布式ID系统,Tinyid
是在美团(Leaf)
的leaf-segment
算法基础上升级而来,不仅支持了数据库多主节点模式,还提供了tinyid-client
客户端的接入方式,使用起来更加方便。但和美团(Leaf)不同的是,Tinyid只支持号段一种模式不支持雪花模式。
适用场景:只关心ID是数字,趋势递增的系统,可以容忍ID不连续,可以容忍ID的浪费
不适用场景:像类似于订单ID的业务,因生成的ID大部分是连续的,容易被扫库、或者推算出订单量等信息
TINYID原理
Tinyid
是基于号段模式实现,再简单啰嗦一下号段模式的原理:就是从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000]
代表1000个ID,业务服务将号段在本地生成1~1000
的自增ID并加载到内存.。
Tinyid
会将可用号段加载到内存中,并在内存中生成ID,可用号段在首次获取ID时加载,如当前号段使用达到一定比例时,系统会异步的去加载下一个可用号段,以此保证内存中始终有可用号段,以便在发号服务宕机后一段时间内还有可用ID。
核心源码
客户端
获取ID入口
public class ClientTest {
@Test
public void testNextId() {
for (int i = 0; i < 100; i++) {
Long id = TinyId.nextId("test");
System.out.println("current id is: " + id);
}
}
}
TinyId
初始化
public class TinyId {
private static IdGeneratorFactoryClient client = IdGeneratorFactoryClient.getInstance(null);
private TinyId() {
}
}
IdGeneratorFactoryClient#getInstance
,初始化连接工厂,主要是指定tinyid.server
的地址信息。
public static IdGeneratorFactoryClient getInstance(String location) {
if (idGeneratorFactoryClient == null) {
synchronized (IdGeneratorFactoryClient.class) {
if (idGeneratorFactoryClient == null) {
if (location == null || "".equals(location)) {
init(DEFAULT_PROP);
} else {
init(location);
}
}
}
}
return idGeneratorFactoryClient;
}
private static void init(String location) {
idGeneratorFactoryClient = new IdGeneratorFactoryClient();
Properties properties = PropertiesLoader.loadProperties(location);
String tinyIdToken = properties.getProperty("tinyid.token");
String tinyIdServer = properties.getProperty("tinyid.server");
String readTimeout = properties.getProperty("tinyid.readTimeout");
String connectTimeout = properties.getProperty("tinyid.connectTimeout");
if (tinyIdToken == null || "".equals(tinyIdToken.trim())
|| tinyIdServer == null || "".equals(tinyIdServer.trim())) {
throw new IllegalArgumentException("cannot find tinyid.token and tinyid.server config in:" + location);
}
TinyIdClientConfig tinyIdClientConfig = TinyIdClientConfig.getInstance();
tinyIdClientConfig.setTinyIdServer(tinyIdServer);
tinyIdClientConfig.setTinyIdToken(tinyIdToken);
tinyIdClientConfig.setReadTimeout(TinyIdNumberUtils.toInt(readTimeout, DEFAULT_TIME_OUT));
tinyIdClientConfig.setConnectTimeout(TinyIdNumberUtils.toInt(connectTimeout, DEFAULT_TIME_OUT));
String[] tinyIdServers = tinyIdServer.split(",");
List<String> serverList = new ArrayList<>(tinyIdServers.length);
for (String server : tinyIdServers) {
String url = MessageFormat.format(serverUrl, server, tinyIdToken);
serverList.add(url);
}
logger.info("init tinyId client success url info:" + serverList);
tinyIdClientConfig.setServerList(serverList);
}
TinyId#nextId(java.lang.String)
,根据业务类型获取IdGenerator
public static Long nextId(String bizType) {
if(bizType == null) {
throw new IllegalArgumentException("type is null");
}
IdGenerator idGenerator = client.getIdGenerator(bizType);
return idGenerator.nextId();
}
AbstractIdGeneratorFactory
,生成IdGenerator
的工厂。
public abstract class AbstractIdGeneratorFactory implements IdGeneratorFactory {
private static ConcurrentHashMap<String, IdGenerator> generators = new ConcurrentHashMap<>();
@Override
public IdGenerator getIdGenerator(String bizType) {
if (generators.containsKey(bizType)) {
return generators.get(bizType);
}
synchronized (this) {
if (generators.containsKey(bizType)) {
return generators.get(bizType);
}
IdGenerator idGenerator = createIdGenerator(bizType);
generators.put(bizType, idGenerator);
return idGenerator;
}
}
/**
* 根据bizType创建id生成器
*
* @param bizType
* @return
*/
protected abstract IdGenerator createIdGenerator(String bizType);
}
IdGeneratorFactoryClient#createIdGenerator
,生成CachedIdGenerator
,对应的SegmentIdService
的实现是HttpSegmentIdServiceImpl
。这边的IdGenerator
是不能用Spring的单例bean来实现的,因为每一个业务都对应一个CachedIdGenerator
。
@Override
protected IdGenerator createIdGenerator(String bizType) {
return new CachedIdGenerator(bizType, new HttpSegmentIdServiceImpl());
}
CachedIdGenerator
初始化,初始化current
和next
。这里特别注意的是loadCurrent
用的是同步方法。current.useful()
会判断当前号段是否已经用完。
protected volatile SegmentId current;
protected volatile SegmentId next;
public CachedIdGenerator(String bizType, SegmentIdService segmentIdService) {
this.bizType = bizType;
this.segmentIdService = segmentIdService;
loadCurrent();
}
public synchronized void loadCurrent() {
if (current == null || !current.useful()) {
if (next == null) {
SegmentId segmentId = querySegmentId();
this.current = segmentId;
} else {
current = next;
next = null;
}
}
}
private SegmentId querySegmentId() {
String message = null;
try {
SegmentId segmentId = segmentIdService.getNextSegmentId(bizType);
if (segmentId != null) {
return segmentId;
}
} catch (Exception e) {
message = e.getMessage();
}
throw new TinyIdSysException("error query segmentId: " + message);
}
CachedIdGenerator#nextId()
,获取下一个ID的方法,判断当前号段是否用完,没有用完,从当前号段中获取;用完,则加载下一个号段。CachedIdGenerator#loadNext
,用的是异步的方式。
public Long nextId() {
while (true) {
if (current == null) {
loadCurrent();
continue;
}
Result result = current.nextId();
if (result.getCode() == ResultCode.OVER) {
loadCurrent();
} else {
if (result.getCode() == ResultCode.LOADING) {
loadNext();
}
return result.getId();
}
}
}
public void loadNext() {
if (next == null && !isLoadingNext) {
synchronized (lock) {
if (next == null && !isLoadingNext) {
isLoadingNext = true;
executorService.submit(new Runnable() {
@Override
public void run() {
try {
// 无论获取下个segmentId成功与否,都要将isLoadingNext赋值为false
next = querySegmentId();
} finally {
isLoadingNext = false;
}
}
});
}
}
}
}
HttpSegmentIdServiceImpl#getNextSegmentId
,通过Http调用,获取号段信息。
@Override
public SegmentId getNextSegmentId(String bizType) {
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;
}
服务端
IdContronller#nextSegmentId
,判断是否可以访问,核心是查询tiny_id_token
表里面是否存在对应的业务和token数据。调用segmentIdService.getNextSegmentId(bizType);
@RequestMapping("nextSegmentId")
public Response<SegmentId> nextSegmentId(String bizType, String token) {
Response<SegmentId> response = new Response<>();
if (!tinyIdTokenService.canVisit(bizType, token)) {
response.setCode(ErrorCode.TOKEN_ERR.getCode());
response.setMessage(ErrorCode.TOKEN_ERR.getMessage());
return response;
}
try {
SegmentId segmentId = segmentIdService.getNextSegmentId(bizType);
response.setData(segmentId);
} catch (Exception e) {
response.setCode(ErrorCode.SYS_ERR.getCode());
response.setMessage(e.getMessage());
logger.error("nextSegmentId error", e);
}
return response;
}
DbSegmentIdServiceImpl#getNextSegmentId
,该方法用到了重试和读已提交事务。主要是并发情况下可能存在版本冲突。获取号段的信息,主要是根据业务更新tiny_id_info
这张表。
@Override
@Transactional(isolation = Isolation.READ_COMMITTED)
public SegmentId getNextSegmentId(String bizType) {
// 获取nextTinyId的时候,有可能存在version冲突,需要重试
for (int i = 0; i < Constants.RETRY; i++) {
TinyIdInfo tinyIdInfo = tinyIdInfoDAO.queryByBizType(bizType);
if (tinyIdInfo == null) {
throw new TinyIdSysException("can not find bizType:" + bizType);
}
Long newMaxId = tinyIdInfo.getMaxId() + tinyIdInfo.getStep();
Long oldMaxId = tinyIdInfo.getMaxId();
int row = tinyIdInfoDAO.updateMaxId(tinyIdInfo.getId(), newMaxId, oldMaxId, tinyIdInfo.getVersion(),
tinyIdInfo.getBizType());
if (row == 1) {
tinyIdInfo.setMaxId(newMaxId);
SegmentId segmentId = convert(tinyIdInfo);
logger.info("getNextSegmentId success tinyIdInfo:{} current:{}", tinyIdInfo, segmentId);
return segmentId;
} else {
logger.info("getNextSegmentId conflict tinyIdInfo:{}", tinyIdInfo);
}
}
throw new TinyIdSysException("get next segmentId conflict");
}
tiny_id_info
这张表,step表示步长,号段的长度。begin_id和max_id是用来记录当前获取的号段的起始位和终止位。
CREATE TABLE `tiny_id_info` (
`id` bigint(20) unsigned 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=1 DEFAULT CHARSET=utf8 COMMENT 'id信息表';