分布式唯一ID生成器

要求
    1. 全局唯一
    2. 趋势递增
    3. 单调递增:保证上一个 ID 的大小一定小于下一个 ID
    4. 信息安全

# 方案1: 数据库自增ID

# 方案2: 美团leaf (依赖MySql)

思想:预分配、批量发号

- TxID段取值经验值: 服务高峰期发号QPS的600倍(10分钟)

1. leaf服务节点: 用于生成TxID
2. 每个leaf服务节点,在对外提供生成TxID之前: 先向Mysql申请能够分配的TxID段,将SELECT的结果(tag,max_id,step)保存在内存中
3. 分配TxID: leaf服务节点,在内存中,对cur_txid++,直到到达cur_txid==max_id时,才重新向Mysql申请下一个TxID段

缺点: 该方案,在向Mysql申请TxID段时,会存在P99波动,采用"双buffer"进行优化(即: 当TxID使用达到10%时,提前去Mysql申请TxID段)

# 方案3: 雪花算法

64个二进制位 = 符号位0 + 机器码位10 + 时间戳位41(ms) + 序列号位12
QPS: 1ms单节点生成序列号数2^12=4096,1s单节点生成4096*1000=400万
- 说明: 各个厂商都对雪花算法进行了改进,不同点,主要是在"时钟回拨"的解决方案,常见的解决方案
1. 索尼
   1. 当发生时钟回拨时,采用sleep的方式,同步等待(Sonyflake),该方案存在明显的弊端,若时钟回拨的时间很长,会超时,无法对外提供服务
2. 较完美的解决方案: 根据时钟回拨的时间长短,设置不同的
   1. <= 1s: 回拨时间较短
      1. 在内存中,保存最近1s的最大序列号,即 map[elapsedTime]max_sequence: { elapsedTime=生成TxID的时间戳,max_sequence=在这个elapsedTime时间戳,序列号的最大值}
      2. 查找到该elapsedTime对应的max_sequence,在max_sequence基础上+1
   2. 1s < 回拨时间 <= 5s
      1. 将请求转发到其他机器 (等该机器时钟回拨问题解决后,可以继续对外提供服务)
      2. 上报监控告警
   3. 时钟回拨 > 5s
      1. 该服务不对外提供服务,执行服务下线操作

时钟回拨
1.定义:所谓时间回拨,即当前得到的本地时间小于了之前获取的本地时间,感觉时针被“回拨”了。
2.那么什么情况下可能出现时间回拨问题呢?人工设置、NTP网络时间同步

索尼Sonyflake
type Sonyflake struct {
   mutex     *sync.Mutex // 线程安全,主要是在多协程的时候通过锁去保证id唯一性
   machineID uint16      // 实例机器id号
   startTime int64       // 算法起始时间的时间戳,以1ms为单位时间,是一个固定值,不会更新
   /* 关键变量 */
   elapsedTime int64  // 当前时间 - startTime
   sequence    uint16 // 序列号
}

func (sf *Sonyflake) NextID() (string, error) {
   const maskSequence = uint16(1<<BitLenSequence - 1) // 掩码号

   sf.mutex.Lock() // 上锁
   defer sf.mutex.Unlock()

   current := time.Now() - sf.startTime // 当前时间 > startTime
   if sf.elapsedTime < current { 
      sf.elapsedTime = current // 更新elapsedTime
      sf.sequence = 0          // 重置seq
   } else {
      // case1: elapsedTime=current,就意味着还在同一个单位时间内,生成多个TxID
      // case2: elapsedTime>current,就说明发生了时间回拨的问题 
      sf.sequence = (sf.sequence + 1) & maskSequence // 序列号+1
      if sf.sequence == 0 { // 若在单位时间内达到了最大可生成序列号的限制
         sf.elapsedTime++                     // 将elapsedTime设为下一个周期
         overtime := sf.elapsedTime - current // 计算时长,睡眠等待(如果回拨的时间过长,会sleep很久)
         time.Sleep(overtime)
      }
   }

   return sf.toID() 
}

func (sf *Sonyflake) toID() (uint64, error) {
   if sf.elapsedTime >= 1<<BitLenTime {
      return 0, errors.New("over the time limit")
   }
   return uint64(sf.elapsedTime)<<(BitLenSequence+BitLenMachineID) | uint64(sf.sequence)<<BitLenMachineID |uint64(sf.machineID), nil
}

采用“历史时间”:

这里是我们方案的核心,我们这里采用的不是实际时间,而是历史时间,在进程启动后,我们会将当前时间(实际处理采用了延迟10ms启动)作为该业务这台机器进程的时间戳中的起始时间字段。后续的自增是在序列号自增到最大值时候时间戳增1,而序列号重新归为0,算是将时间戳和序列号作为一个大值进行自增,只是初始化不同。

序列号自增:

每次有数据请求,直接对序列号增加即可,序列号从0增加到最大,到达最大时,时间戳字段增加1,其实是时间增加1毫秒,序列号重0计算。


参考:彻底解决雪花算法时间回拨问题新方案

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值