我是靠谱客的博主 陶醉短靴,最近开发中收集的这篇文章主要介绍分布式唯一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计算。


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

最后

以上就是陶醉短靴为你收集整理的分布式唯一ID生成器的全部内容,希望文章能够帮你解决分布式唯一ID生成器所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(44)

评论列表共有 0 条评论

立即
投稿
返回
顶部