分布式ID

本文总阅读量

前记

在业务中, 我们经常需要有一个唯一id, 来区分每个数据, 如每个订单都有一个自己的id, 如果是一个单体应用, 实现起来会非常容易, 但很多时候是有多个应用同时索要id, 这时候就要确保任一次生成的id都不重复, 这就催生了分布式id的概念, 分布式id一般有几个特性:

  • 全局唯一, 不会再生成与这一样的id
  • 高可用, 所有的业务系统都需要依赖这个方法生成id,所以不能出现问题.
  • 趋势有序, 这个id在业务中通常会充当索引字段, 所以要趋势上有序.
  • 数量不可猜测(非必要), 一般的业务场景中, 会要求生成的id不会被他人猜测到今天生成的量有多少.
  • 高性能, 生成id的速度一定要快.

1.UUID方案

UUID方案是最方便引用的方案, 由于没有网络依赖, 全靠本地生成, 所以他的性能是最高的. 但同时他的劣势也非常的明显:

  • UUID太长了, 不适合做数据库的索引存储
  • 非有序, 由于业务上基本都是用MySQL的InnoDB引擎, 所以UUID的无序会严重影响InnoDB的写入性能.
  • 如果选用随机的UUID生成方式, 当使用伪随机的时候, 在量大的时候会出现冲突的可能, 如果使用真随机, 会出现硬件的熵因子不够的情况, 性能会急剧下降.
  • 如果选用mac生成UUID的方式, 则会造成mac泄露

2.MySQL自增id方案

在业务中, 经常会使用MySQL中的自增id来作为id, 但是为了保证性能和高可用, 一个MySQL肯定是不行的, 必须得用集群, 这时候就得使用步进式的方案,假设有3台MySQL服务器, 那么1,2,3台服务器的初始id分别为1, 2, 3, 且他们的自增系数都为3.如下所配置:

1
2
set @@auto_increment_offset = 1;     -- 设置起始值
set @@auto_increment_increment = 2; -- 设置自增步长

这样能很完美的解决问题,可是这样也存在一个问题,那就是无法扩容, 用多主数据库来进行ID自增的话,只要设定好了,就再也无法变更了, 如果需要增加MySQL服务器, 上面的配置就得改了, 还要重新设置起始值(一般取当前集群最大的id), 而且每次调用都要插入一条数据, 性能会受到影响, 同时是对MySQL有强依赖, 以及自增id数量可猜测.

PS也可以用mongodb的自增id

3.Redis自增id方案

既然用MySQL不行, 那就用Redis吧, 通过Redis集群保证了高可用, 再使用incr指令达到跟MySQL一样的效果, 也解决的扩容的问题, 而且他的性能非常高. 但是也存在了对Redis的强依赖和订单量可猜测问题, 同时Redis只保证了3个9的可用性, 如果涉及到金额相关的场景, 3个9的可用性是不安全的. Redis之所以是3个9的一个原因是Redis的数据是存在内存的, 为了保证高可用, 需要依赖RDB和AOF来持久化, 为了保证一条命令都不能缺失, 一定要启用AOF, 每执行一条命令就写入一次, 这样的性能的不够的, 如果使用时间间隔的AOF, Redis挂了之后容易导致生成的Id重复了.

4.号段

号段一般是依赖与数据库的, 但是他是通过预拿一批id放到代理池里面, 本来代替每次需要的时候才拿, 减少了性能的消耗, 这种方案不会强依赖数据库, 即使数据库挂了也能撑一段时间, 但是内存所在的程序挂了的话, 这批id就丢了, 所以使用号段来实现分布式id最重要的是确定一段有多少个id, 使其性能最好, 但不会很浪费.除此之外他还有以下几个优点:

  • 可以很方便的线性扩展,性能完全能够支撑大多数业务场景
  • ID号码是趋势递增的
  • 由于可以自定义启动id, 可以方便的从别的业务迁移过来

但是号段方式也会造成发号数量的泄露, 不太安全, 同时每当一个号段用完了就会再去取一次数据, 这时候耗时会明显变大. 这个问题我们可以通过预加载来解决, 通过判断号段内存池剩余的数量变少到一定的数量时, 就使用后台任务重新拉一批新的号段放入号段池, 这样就可以解决用完号段再去拉号段的问题了.

5.雪花算法

雪花算法是一个非常通用的方案, 由于只需要在启动时分配机器id, 之后全靠算法来生成分布式id, 所以实现成本低, 速度也快, 没有什么特殊情况, 一般都推荐使用雪花算法来实现分布式id.

雪花id按照一定的规则进行填充:时间(毫秒级)+集群ID+机器ID+序列号 来生成分布式id, 除了初始化阶段外, 只依赖时间, 没有其他的依赖方式, 同时生成速度也非常快, 生成的id也是趋势有序的.雪花算法十分优秀, 只不过因为需要依赖时间, 会出现两个问题:

  • 1.时间回拨的问题: 机器有可能发生时间回拨, 在遇到时间回拨时, 可以直接不处理直接报错, 或者等到时间追上来为止, 或者借用未来时间, 但是在借用未来时间后, 程序在重启时要等现实时间追上未来时间后才启动.
  • 2.当前时间序列号用完了: 一般情况下, 序列号的够用的, 但也有极端情况下出现不够用的情况, 这时候需要等待到下一个时间单位.

雪花算法的具体实现规则是: 雪花id是一个64bit的整数, 这64bit根据上面说的规则分别代表不同的参数值(从左到右开始数):

  • 第一bit: 一般是符号位,不做处理
  • 第2-42bit: 用来记录时间戳,这里可以记录69年, 需要自己设置个开始时间戳, 用于后面的计算,twitter使用的元年时间戳是1288834974657
  • 第43-52bit: 用来记录机器ID,总共可以记录1024台机器, 一般会把前几位当做集群id或者区域id, 后面几位再代表机器id
  • 最后12bit: 用来对同一个毫秒之内产生不同的ID,12位可以最多记录4095个,也就是在同一个机器同一毫秒最多记录4095个,多余的需要进行等待下毫秒. 一般情况下没有业务的并发量会这样大, 所以是可以接受的.

简单的python实现

总结

分布式id由于带有分布式的名头, 自然也就没有一个完美的解决方案, 如果选择无中间件依赖就要解决时间回拨问题, 如果选择中间件依赖, 就要做好中间件出问题的解决办法.一般情况下雪花分布式id都适合, 但还是需要结合自己的业务去选择或定制分布式id方案.

查看评论