分布式唯一ID设计

概述

分布式唯一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纯序号,无特殊意义
缺点补全方案
  1. 将时间放在zookeeper上,所有服务器从zk上拉取时间,不使用本地时间
  2. 取消同一毫秒才进行顺序递增设定, 在某一段时间或不限制,一直顺序递增,满了再归0,这样计数。如果再设定按某个类型技术,还可以产生均匀的分片。
  3. 可以从雪花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

相关推荐
prince055 分钟前
Kafka 生产者和消费者高级用法
分布式·kafka·linq
菜萝卜子1 小时前
【Project】基于kafka的高可用分布式日志监控与告警系统
分布式·kafka
幼稚园的山代王8 小时前
RabbitMQ 4.1.1初体验-队列和交换机
分布式·rabbitmq·ruby
小新学习屋9 小时前
Spark从入门到熟悉(篇三)
大数据·分布式·spark
沉着的码农12 小时前
【设计模式】基于责任链模式的参数校验
java·spring boot·分布式
ZHOU_WUYI1 天前
一个简单的分布式追踪系统
分布式
码不停蹄的玄黓1 天前
MySQL分布式ID冲突详解:场景、原因与解决方案
数据库·分布式·mysql·id冲突
王小王-1231 天前
基于Hadoop的公共自行车数据分布式存储和计算平台的设计与实现
大数据·hive·hadoop·分布式·hadoop公共自行车·共享单车大数据分析·hadoop共享单车
要开心吖ZSH1 天前
《Spring 中上下文传递的那些事儿》Part 4:分布式链路追踪 —— Sleuth + Zipkin 实践
java·分布式·spring
幼稚园的山代王1 天前
RabbitMQ 4.1.1初体验
分布式·rabbitmq·ruby