企业级分布式主键Id解决方案

243 阅读4分钟

我正在参加「掘金·启航计划」

一、引言

公司之前做过一次关于分布式主键生成调研,因为做的是IM相关的业务,部分业务中的id有实际含义,例如:在消息服务中,每条消息都对应着一个id,用户已读未读或者拉取消息列表则主要通过id来分析。因此生成的id要满足以下几种情况

  • 唯一性
  • 线性单调递增而非趋势递增
  • 高可用,掉线重连后跨度区间要小
  • 高并发

二、方案调研

调研了市面上常用的分布式主键解决方案,包括uuid、雪花算法、

[美团leaf]  tech.meituan.com/2017/04/21/… 

[微信序列号]  www.infoq.cn/article/wec… 

[百度UidGenerator]  github.com/baidu/uid-g… 

有兴趣大家也可以了解下大厂的分布式主键解决方案。各个方案对比如下

方案唯一性递增趋势高可用区间跨度小高并发存在问题
uuiduuid不适合作为主键
雪花算法单调递增雪花算法高度依赖于时间戳,无法直接解决时间同步带来的回拨问题。且掉线前后所生成的id跨度区间大。每秒生成的个数又限制于低位
美团leaf趋势递增采用号段的模式发取Id,导致递增趋势为整体趋势递增,即后发放的id可能比先发放的id小,这是在消息模式中不可容忍的
微信序列号单调递增微信采用的方案于项目之前方案类似一致,则是通过路由的方式,让指定的服务路由到指定的节点生成id,但是这种情况一旦部分节点宕机,服务将处于部分不可用状态
百度UidGenerator趋势递增百度的双buff思想让qps峰值到达500w,但是因为大部分id都在内存中,其他服务不可见,因此服务一旦宕机重连后,先后生成的id跨度非常大。且多台机器生成的id也是整体呈趋势递增。

在公司项目中,单调递增趋势是必需品,因此这里针对此问题,采用下面解决方案。如果是普通项目,非常建议使用百度UidGenerator分布式主键。

三、解决方案

核心思想,利用中间价redis。使用官方提供的redisson客户端进行原子性操作,保证了整体趋势为 单调递增、数据的唯一性

这里不讨论redis在主从同步的时候产生多锁或者其他极端情况,默认是信任中间价的。

同时,借鉴百度的双buff思想,维护slot槽位概念,解决申请id过慢的问题,同时维护批量生产id的功能。由于采用的是redisson,因此高可用、掉线重连等问题也迎刃而解。

核心逻辑如下:

Solt,根据不同业务,路由到不同的槽位,确保相同业务的id是单调递增的。项目初始化的时候会维护slotId,curId,maxId

/**
 * 槽 (curId,maxId]
 */
@Data
public class Slot {
​
  /**
   * 槽位编号
   */
  private int slotId;
​
  /**
   * 当前已发出去的最大id
   */
  private AtomicLong curId;
​
  /**
   * 可产生的最大id
   */
  private AtomicLong maxId;
​
}

获取id核心代码:

//slotId 槽位id,size 申请的id数量
long currentCurId = slotCache.increment(getSlotCurIdKey(slotId), size);
// 获取该槽位的起始值 
long initValue = SlotUtil.getInitValue(slotId);
//判断redis值是否合法,即currentCurId一定要大于>initValue,否则证明这个值没有初始化过
// 小于起始值,认为redis值非法
if (currentCurId <= initValue) {
      // 获取分布式锁
      RLock lock = redissonClient.getLock("redisson:refreshslot:" + slotId);
      // 加锁
      lock.lock();
      try {
        // 再获取一次
        currentCurId = slotCache.incrementSlotCurId(slotId, size);
        if (currentCurId <= initValue) {
          // 刷新槽位(同步):redis值非法
          //syncRefreshSlot 就是往数据库里写入当前的槽位信息,根据设定好的步长来在数据库中持久化maxId
          clusterSlotManager.syncRefreshSlot(new Slot(slotId));
          // 再获取一次
          currentCurId = slotCache.incrementSlotCurId(slotId, size);
        }
​
      } finally {
        // 释放锁
        lock.unlock();
      }
   
}
​
// 批量获取
    if (size > 1) {
      // 先前的curId
      long oldCurId = currentCurId - size;
      // 刷新槽位的时机
      if (oldCurId / slotStep < currentCurId / slotStep) {
        // 刷新槽位(异步)
        clusterSlotManager.asyncRefreshSlot(new Slot(slotId));
      }
      // 组装返回值
      for (long id = oldCurId + 1; id <= currentCurId; id++) {
        list.add(id);
      }
​
    } else {
      // 刷新槽位的时机
      if (currentCurId % slotStep == 0) {
        // 刷新槽位(异步)
        clusterSlotManager.asyncRefreshSlot(new Slot(slotId));
      }
​
      // 组装返回值
      list.add(currentCurId);
​
    }
​
    return list;

起始值为 1000000,步长为100000

这里说一下,刷新Slot的几个时机

  • 在项目启动的时候,会初始化Slot(从数据库中读取),如果有记录则把当前的Slot赋值
  • 在生成id时,currentCurId <= initValue时
  • 这里采用的是双倍步长,即maxId和curId 一开始的时候相距两个步长 ,当oldCurId / slotStep < currentCurId / slotStep 即使用id到达可用的50%以后,会刷新maxId。 currentCurId % slotStep == 0 同理。

以上即为公司的分布式主键id解决方案,解决了IM聊天类系统分布式主键id非单调递增的主要问题,这里只讲解大概思路,详细代码由于未取得开源允许,这里就不再详解

四、性能测试

机器配置(48核 128G)

发压机:XXXXXX服务:XXXXXXXX
内存:128G
CPU:48核
配置内存:1G

getId(携程号)

集群版

# 500携程 执行 30 秒
[linkdood@localhost ant-chenlong]$ ./ant -c 500 -d 30 -r idgenerator-getId xxx.xxx.xxx.xxxx:xxxxx
Running 30s of  test @ xxx.xxx.xxx.xxxx:xxxxx
  500 goroutine(s) running concurrently
3168693 requests in 30.00sec
         success:3168693
         failed:0
Requests/sec(TPS):      105623.10
Avg Req Time:           4.73ms
Fastest Request:        0.56ms
Slowest Request:        1024.31ms

TP50:   4.58ms
TP90:   5.11ms
TP99:   8.34ms

listIds(携程号,100)

集群版

# 500携程 执行 30 秒
[linkdood@localhost ant-chenlong]$ ./ant -c 500 -d 30 -r idgenerator-listIds xxx.xxx.xxx.xxxx:xxxxx
Running 30s of  test @ xxx.xxx.xxx.xxxx:xxxxx
  500 goroutine(s) running concurrently
2928423 requests in 30.00sec
         success:2928423
         failed:0
Requests/sec(TPS):      97614.10
Avg Req Time:           5.12ms
Fastest Request:        0.72ms
Slowest Request:        1027.99ms

TP50:   4.75ms
TP90:   5.35ms
TP99:   16.03ms

测试结果

getId(携程号)listIds(携程号,100)
集群版:105623 tps集群版:97614 tps