本文主要总结市面上常见的分布式ID生成方案,并结合自身项目经历对齐总结
定义
分布式 ID 是分布式系统下的 ID,其具有唯一标识。类似系统中常见的用户ID、订单号、券码,都应该具备唯一性。
算法要求
- 全局唯一:生成的 ID 必须全局唯一
- 趋势递增:我们应该尽量选择有序的主键来保证索引的性能
- 好接入:要秉着拿来即用的设计原则,在系统设计和实现上要尽可能的简单
- 信息安全:如果是连续的 ID,攻击者很容易就猜出下一条记录的 ID,所以有些情况下尽量让 ID 无规则(可选)
- 含时间戳:含时间戳便于追踪(可选)
可靠性要求
- 高可用:要保证在99.999%的情况下能够生成一个唯一的分布式 ID
- 低延迟:生成分布式 ID 的速度一定要快
- 高QPS:假如一下子来10w个生成分布式 ID 的请求,服务器要能扛得住并且能够一下子生成10w个
生成方案
UUID
包含32个16进制的数字,以连字符分割成五段,格式是8-4-4-4-12
优点:
性能好,本地生成,无网络消耗
缺点:
存储消耗空间大(32 个字符串,128 位)
不安全(基于 MAC 地址生成 UUID 的算法会造成 MAC 地址泄露)
无序(非自增)
没有具体业务含义
需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID)
无序,不能生成递增的 ID,而且很长,入库性能差(MYSQL)。因为MySQL的 是 B+ 树索引,每插入一条新数据,都会对索引进行改造,因为 UUID 的无序,每次插入数据时 B+ 树的改造就会很大,也就是导致索引分裂
应用场景:
用于分布式日志追踪的traceId生成,可参照:基于LOGBACK实现的分布式日志跟踪
案例
java
import java.util.UUID;
public class IdGenerator {
public IdGenerator() {
}
public static String createID() {
String id = UUID.randomUUID().toString();
id = id.replace("-","");
return id;
}
}
数据库自增主键
直接依赖关系型数据库机制-自增主键来产生唯一ID
优点:
实现起来比较简单、ID 有序递增、存储消耗空间小
缺点:
支持的并发量不大(依赖数据库性能)
存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)
ID 没有具体业务含义
每次获取 ID 都要访问一次数据库(增加了对数据库的压力)
应用场景
可以用于TPS不高场景的分布式唯一ID获取
不过需要注意自增键初始值和步长,不能随意更改 主要是
auto_increment_increment
和auto_increment_offset
参数
案例
建表
sql
drop TABLE IF EXISTS `sequence_id`;
CREATE TABLE `sequence_id` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT comment '自增ID',
`stub` varchar(20) NOT NULL DEFAULT '' comment '随机值',
PRIMARY KEY (`id`),
UNIQUE KEY `stub` (`stub`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=10 comment '自增序列表';
mybatics xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.toby.dynamic.data.source.db.dao.config.SeqIdMapper">
<insert id="getNextId">
<selectKey keyProperty="id" resultType="java.lang.Long" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO sequence_id (stub) VALUES (#{name})
</insert>
</mapper>
mapper文件
java
public interface SeqIdMapper {
long getNextId(@Param("name") String name);
}
test用例
java
package com.toby.dynamic.data.source.db.dao.config;
import com.toby.dynamic.data.source.start.PointApplication;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = PointApplication.class)
public class SeqIdMapperTest {
private static final Logger LOGGER = LoggerFactory.getLogger(SeqIdMapperTest.class);
@Autowired
private SeqIdMapper seqIdMapper;
@Test
public void getNextId() {
long ret = seqIdMapper.getNextId(String.valueOf(System.currentTimeMillis()));
LOGGER.info("getNextId ret={}", ret);
Assert.assertTrue(ret > 0);
}
}
运行结果
sql
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@54be6213] was not registered for synchronization because synchronization is not active
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@322b09da] will not be managed by Spring
==> Preparing: INSERT INTO sequence_id (stub) VALUES (?)
==> Parameters: 1703319042518(String)
<== Updates: 1
==> Preparing: SELECT LAST_INSERT_ID()
==> Parameters:
<== Columns: LAST_INSERT_ID()
<== Row: 1
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@54be6213]
具体代码参考:github.com/hongyuwen/H...
未完待续