探秘分布式解决方案: 分布式ID——论高可用全局ID的诞生之道
为了解决在分布式系统中需要对某个资源进行全局的一个非重复ID生成,所以有了分布式ID这么一个概念
在分布式应用下,像分库分表的这种场景是很常见的, 这个时候如果还是用数据库本身的自增的话,那多个数据库ID肯定会重复。
比如订单表由于数据过多,分到了多个数据库中存储的话,那么这个ID要如何生成呢?
还是以原来的逻辑进行自增的话,那就会出现这种情况: 数据库A里边有订单1、2、3 , 数据库B里边也有个订单1、2、3, 这在业务逻辑上就冲突了。
那么这就需要有一个分布式ID生成系统。
而一个分布式ID系统需要满足的有这么几点:
1、高可用, 所有的业务系统都需要调用这么一种id生成器来生成id,这玩意千万不能挂
2、趋势有序, 无论是啥关系型数据库,在一个字段上加索引肯定是有序的更快
解决方案:
1、UUID
这个建议不用管,虽然好处多(无脑)。但是代价太大了。
主要就是在DB里边用UUID太长了,空间又占得大,你建的索引越多, 影响越严重。
UUID还是非自增,无论是增删改查相对于自增id的比较大小来说都要慢很多,尤其是在那种取数据段的场景。
2、数据库ID自增
那为了保证高可用,一个DB肯定是不行的,那必须得集群才行。
就用mysql举例:
主从: 那肯定是不行的。 slave同步master的数据会出现延时,在master挂掉时可能出现获取ID重复的情况
主主: 如果只是普通的使用自增策略,那这个也会有问题。假设有A/B两个数据库,都是1、2、3自增。那么每个ID都会是重复的。
这个时候就要引入一个步长的概念。假设设置MySQL自增策略步长为2。然后其中一个从1开始,另一个从2开始。那么A/B两个数据库自增的ID就会是1、3、5/2、4、6 ; 这样子就很美好了。
可以用这个语句设置, 多个库都用不用的起始值和相同的步长就行了。重要的是有多少个DB就要有多长的步长
set @@auto_increment_offset = 1; -- 设置起始值 set @@auto_increment_increment = 2; -- 设置自增步长
可是这样也存在一个问题,那就是无法扩容
用多主数据库来进行ID自增的话,只要设定好了,就再也无法变更了。以我上边的来说, 想要原基础上添加一个C数据库来进行ID自增,那是做不到的。
这样子就丧失掉了拓展性。虽说在一般的企业应用中这样子一个架构也确实够了 。
当然,每次生成ID都去走数据库也怪麻烦的,所以一般也有个ID生成服务,各个应用只需要请求那一个服务就行了。由那个服务去获取ID,达到一个解耦的目的。
这个服务为了高可用也要集群才行,不过这个集群只是为了防特殊情况导致服务挂掉 , 他们请求的数据源都是一样的。
既然可以用MySQL这种关系型数据库,那肯定也可以用Redis这种基于内存的NoSQL数据库来实现这个。
那这个效率不是比关系型数据库高不少?
使用Redis的INCRBY命令就可以从容的在值上增加指定的数字,设置起始值、自增步长。Redis也可以很轻松做到嘛。
但是用Redis的话有一个点要注意:
分布式ID首先得考虑高可用,所以得考虑持久化的问题。Redis支持RDB和AOF两种持久化的方式。
而我们只能用AOF,对每一条写命令进行持久化,因为RDB持久化相当于定时打快照来持久化,如果打完快照后,还生成了多次ID,没来得及做下一次快照的时候Redis挂了,重启Redis后就会出现ID重复。
3、号段
其实号段也算是数据库ID自增的一种形式。这里只是单独拿出来说
不过就是将原本的一个个获取改成了批量获取,比如直接获取这个数据库里边1 – 10000的ID (只要查出一个数据段落和步长就够了, 自增在自己服务里边自增)。存在分布式ID服务的缓存里边。
这样就免得一次次去和数据库交互。
而且这种方案不再强依赖数据库,就算数据库不可用,那么这个分布式ID服务也能继续支撑一段时间。缺点就是他挂掉重启的话,会丢失一段ID。
之前说过,分布式ID服务的数据源都是一样的。而正是因为数据源一样, 在号段模式里边可能出现那种两个ID服务同时请求一个数据库的同一段数据的情况。那这就很麻烦。
所以这个号段模式需要上一个乐观锁才可以,关于乐观锁可以看我这篇文章: [浅谈CAS与乐观锁] http://skypyb.com/2019/08/jishu/961/
当然,这种轮子就别自己实现了,人家早就给你安排的明明白白的,只管拿过来用就OK
比如滴滴开源的TinyId: https://github.com/didi/tinyid
4、SnowFlake
这个就是分布式ID界的头头, 著名的雪花算法
对,他是一种算法,是twitter开源的分布式ID生成算法,他和我上边说的数据库生成分布式ID机制完全不一样,最核心的就是它不依赖数据库。
不依赖数据库,这也太美妙了。要知道在软件中每引入一个额外变量都会使架构的复杂度几何上升,SnowFlake无疑就是那分布式ID的最佳实现者。
核心思想是:
分布式ID固定是一个long型的数字,一个long型占8个字节,也就是64个bit,那这64个bit的每一位分配如下:
第一个bit位是标识,在java中由于long的最高位是符号位,正数是0,负数是1,由于一般生成的ID为正数,所以固定为0。
时间戳部分占41bit,这个是毫秒级的时间,这个会储存时间戳的差值(当前时间-固定的开始时间),因为这样可以使产生的ID从更小值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
工作机器id占10bit,这里可以很灵活,比如,可以使用前5位作为数据中心机房标识,后5位作为单机房机器标识,可以部署1024个节点。
序列号部分占12bit,支持同一毫秒内同一个节点可以生成4096个ID
根据这个算法的逻辑,只需要将这个算法实现出来,封装为一个工具方法,那么各个业务应用可以直接使用他来获取分布式ID,只需保证每个业务应用有自己的工作机器id即可。直接降低大量复杂度。
关于这个算法的具体实现,搜索引擎搜索SnowFlake,一堆实现。直接复制到自己的项目里边用就完事了。
关于雪花算法,其实也有一个小缺点。那就是要为每个机器指定一个机器id, 几十台几百台机子还好,更多那就很操蛋了。
所以有大厂对SnowFlake进行了改造,让其可以自动分配。比如百度的 uid-generator: https://github.com/baidu/uid-generator
结语:
分布式ID这玩意,其实是个非常简单的东西。
我相信任何一个有点分布式经验的开发者都可以在短时间内理解并在生产中实践。
我就想说一句: 其实用哪个都无所谓。
你要是真的业务有什么东西每天几亿个ID自增,估摸着公司也是流水多多了,轮不到普通程序员操心这玩意。 要是就一普通软件,用户几万几十万的。搞两台Redis来incr也是一样的…