概述
分布式唯一ID是在分布式系统中生成全局唯一标识符的解决方案,用于确保不同节点产生的ID不重复。
核心需求
- 全局唯一性:确保不同节点、不同时间的ID不重复。
- 高性能:低延迟、高吞吐,适应高并发场景。
- 高可用:避免单点故障,容灾能力强。
- 有序性:趋势递增(有利于数据库索引优化)。
- 可扩展:支持节点动态扩缩容。
各类方案优缺点
UUID
本机生成方案
实现:基于随机数(v4)或时间戳+MAC地址(v1)生成128位字符串。
优点:本地生成,速度快,不消耗网络,保证了唯一性
缺点:
- 字符串类型,而且较长,很浪费空间,并且会影响B+树结构的数据库结构,造成每页可存储的索引较少,增加树高
- 无法保证趋势递增,对数据库很不友好,索引效率低
- 可读性差,无业务含义
- 暴露mac地址
雪花算法Snowflake
本机生成方案
实现:64位Long型 = 时间戳(41位) + 机器ID(10位) + 序列号(12位)
- 1空位
- 41位当前毫秒, 保证id是顺序递增的。
- 10位 worker id 用户自己设置
- 12位 顺序递增, 当在同一毫秒时,顺序位递增,防止同一时间id冲突
优点: - 本地生成,速度快,不消耗网络,保证了唯一性
- 保证了顺序递增
- id为long类型,比较小
- 算法很灵活,有多种扩展方案
缺点: - 在分布式场景下,每个机器的时间都有差异,而雪花算法是基于时间的,会导致产生的id不是顺序递增,时间慢的机器产生的id会逆流。需要进行时间同步。
- workid需要自己分配,手动分配在服务很多的时候难以进行,必须要依赖zookeeper进行分配,增加了对zk的依赖性。
- 当要用雪花id进行hash分片时,如果id产生的不频繁,比如每毫秒只产生一个id,那么顺序位肯定一直是1,造成分片永远是1
- 如123123101%4=1,21423452301%4=1,1231231201%4=1....
- id纯序号,无特殊意义
缺点补全方案
- 将时间放在zookeeper上,所有服务器从zk上拉取时间,不使用本地时间
- 取消同一毫秒才进行顺序递增设定, 在某一段时间或不限制,一直顺序递增,满了再归0,这样计数。如果再设定按某个类型技术,还可以产生均匀的分片。
- 可以从雪花id的中间位置,挤出3位,插入基因片段,为id增加业务特征。
mysql
通过数据库产生唯一id。每生成一个id,就插入一条数据,可以利用数据库的自增id提高效率
数据库既能保证唯一性,又提供了持久化以及并发访问。
但单台数据库的性能是有限的,无法满足高并发场景。
高并发改造
集群化,然后以改变步进值的方式,让不同实例之间产生的id不重复。
部署n台数据库。
每个数据库以n步进,如第一台数据库从0开始, 第二台从n开始,第三台从n+n开始,每次产生一个序号后,加n, 这样n个数据库就能间隔着递增id。
批量化:
每次取一批id,减少网络io通信。
优点:
- 实现简单,可靠性高
- id顺序递增
缺点 - 每次都需要网络io
- 存在性能瓶颈,扩容复杂。
redis
同为数据库,方案和mysql类似,但redis是内存操作,并发性能比mysql更高,但持久化的安全性不如mysql,无论是快照还是aof都有丢失数据的风险。
redis的集群更加强大。
优点:
- 实现简单,并发能力强大
- id顺序递增
- 集群方案成熟,简单
缺点: - 每次都需要网络io
- 可靠性略差
leaf-segment 数据库方案
基于mysql的方案进行了增强。每次发一批id,减少了网络io,同时增加了业务标记,对id进行了分区。通过biz_tag分表,可以支持更高的并发。
表设计方面:
- biz_tag 业务标记,用于区分业务,各个类型业务id自增互不干扰,这也为分表提供了依据
- max_id 当前分配的最大id
- step 每次分配的步进值
- update_time:记录表结构的最后更新时间
begin
update t set t.max_id = t.max_id+step where t.biz_tag=xx
select ...
commit;
优点: - 很容易扩容,id顺序,
缺点: - 同业务下id连续,容易被推算出id或数量,如取0点id,和23点id, 相减就是一天的生成数量
- 较依赖数据库,容易出现毛刺,如数据库响应慢,导致客户端申请id时间长
双buffer改进
服务内部维护两个buffer, 首次取两批次id,当被请求下一批次时,由于已经存在于内存,可以快速返回,同时再申请下下批次的id。
以此类推
如果服务存储的buffer较多,id很多,即使数据库宕机一小会,也不影响id的申请。
高可用容灾
对数据库进行主从集群,提高可用性
秒杀业务高并发下使用优化
秒杀业务下,如果还用rpc创建一个订单取一个id,必然会影响并发量。
所以需要在本地维护一个id缓冲池,避免频繁通信。
实现如下:
1、缓冲池实现: 线程安全队列ConcurrentLinkedQueue, 如果需要更高的并发可以使用Disruptor代替
2、id补充策略: 初始化时,,申请一批id, 然后启动一个异步定时任务, 每个一段时间就申请一部分id。
代码:
java
/*存放预制orderId的list,同时也可限流每秒允许个数,在更高并发的请求下,
可以使用Disruptor代替 https://github.com/LMAX-Exchange/disruptor/wiki */
private ConcurrentLinkedQueue<String> orderIdList = new ConcurrentLinkedQueue();
private ConcurrentLinkedQueue<String> orderItemIdList = new ConcurrentLinkedQueue();
// 每秒获取的id数量
public static final int ORDER_COUNT_LIMIT_SECOND = 2000;
// 每次获取id的间隔时长(毫秒)
public static final int FETCH_PERIOD = 100;
// 定时任务
private ScheduledExecutorService refreshService = Executors.newSingleThreadScheduledExecutor();
@PostConstruct
public void init(){
// 初始化获取一批订单id放入列表
List<String> segmentIdList = unqidFeignApi.getSegmentIdList(LEAF_ORDER_ID_KEY, ORDER_COUNT_LIMIT_SECOND);
orderIdList.addAll(segmentIdList);
List<String> segmentItemIdList = unqidFeignApi.getSegmentIdList(LEAF_ORDER_ITEM_ID_KEY, ORDER_COUNT_LIMIT_SECOND);
orderItemIdList.addAll(segmentItemIdList);
//启动定时任务,定时添加订单id,
refreshService.scheduleAtFixedRate(new RefreshIdListTask(orderIdList,unqidFeignApi,orderItemIdList),
0, FETCH_PERIOD, TimeUnit.MILLISECONDS);
}
//==========RefreshIdListTask==========
@Override
public void run() {
if (!orderIdList.isEmpty()) return;
try {
// 这里的计算是因为设计的每秒刷新2000个订单id,那么应当设计1秒的定时任务,每次刷新2000个id。
// 但实际的定时任务设计了0.1秒刷新一次,每次刷新1/10的总id。
// 这么做的目的是为了订单id刷新的更加平滑,避免订单生成时分布不均,
// 比如前200毫秒消耗了2000个id,如果按1秒设计,需要等800毫秒才刷新,但按1/10秒刷新,就只需要等100毫秒。
int getCount = ORDER_COUNT_LIMIT_SECOND / (1000 / FETCH_PERIOD);
List<String> segmentIdList = unqidFeignApi.getSegmentIdList(LEAF_ORDER_ID_KEY, getCount);
orderIdList.addAll(segmentIdList);
List<String> segmentItemIdList = unqidFeignApi.getSegmentIdList(LEAF_ORDER_ITEM_ID_KEY, ORDER_COUNT_LIMIT_SECOND);
orderItemIdList.addAll(segmentItemIdList);
} catch (Exception e) {
log.error("获取订单id列表异常:",e);
}
}
leaf-snowflake 雪花算法方案
本机产生id
基于雪花算法,通过zookeeper解决了雪花算法中workerId分配问题
解决了leaf-segment中,id顺序被推测的问题。
服务在启动时,连接zookeeper,通过顺序node,得到自己的workerId。连接后,如果存在取回id,不存在创建一个
弱依赖zookeeper
由于workerId很少会改变,将得到的workerId存到本机文件中,即使zookeeper离线,也不影响id产生。
解决时钟问题
因为基于了雪花算法,所以同样有雪花算法的时间问题。
有以下几个解决方法
- 通过rpc访问zookeeper上所有worker,取它们的时间,得到一个平均值,对偏离平均值太远的警告。
- 当出现时钟回拨时,停止服务,等追回后继续服务
优点 - 本机产生id,无网络io消耗
- id递增,但不连续
缺点: - 依赖zookeeper
- 存在时间问题
cosid 方案
segment方案之一
增加了机器id