本文转载自微信公众号《Java极客技术》,作者鸭血范。转载本文请联系Java极客技术公众号。在前言里,我先说说为什么我们需要分布式ID,分布式ID用来解决什么问题。当我们的项目还是单体结构的时候,我们可以通过数据库的自增ID来解决很多数据识别问题。但是随着我们业务的发展,我们的架构会逐渐向分布式架构演进。这个时候使用数据的自增ID就不行了,因为一个业务的数据可能会放在几个数据库中。这个时候,我们需要一个分布式ID来标识一条数据,所以我们需要一个分布式ID生成服务。那么分布式身份服务有哪些需求和挑战呢?要求全局唯一性:由于是用来标识数据唯一性的,一个分布式ID必须是全局唯一的,在同一个业务下的各个服务下是一致的,不会改变,这是一个基本要求;globalincrement:increment也很好理解,我们需要保证生成的ID是顺序自增的,因为很多时候ID是给人看的,如果不自增,缺乏很多可读性;信息安全:分布式ID的安全性也很重要,因为我们提到生成的ID是增量的,可能会让竞争对手知道我们生成ID的频率,这在电商等场景下是个大问题,但是经常和全局增量冲突;高可用:分布式ID生成服务必须是高可用的。毕竟,一旦无法生成ID,后续的所有服务都无法继续使用;目前的互联网实现了通用的分布式ID。根据不同的业务场景和需求,分布式ID有几种实现方式:UUID;雷迪斯;变形的数据库自增ID;推特雪花算法美团的Leaf——雪花算法的变形;UUID写Java的朋友一定对UUID不陌生。7dbb9f04-d15e-4c88-b74b-72a35e0d7580这是一个标准的UUID。需求中的第一点,但是显然没有全局自增,这种分布式ID的可读性很差,如果只是用来记录日志或者不需要人看懂的场景,可以使用过,但不适合我们这里所说的业务数据的唯一标识。而如果将这个无序的UUID作为主键,会严重影响性能。RedisRedis有一个incr命令。该命令可以保证原子递增,也可以在一定程度上生成一个全局ID。但是,使用Redis有两个问题:不美观。虽然我们需要的是全局ID,但是incr命令是从1开始的整数,所以全局ID的长度会不一致。虽然也可以用来标识业务数据的唯一性,但由于不携带日期信息,在某些场景下也缺乏可读性;依赖Redis的高可用,因为Redis是基于内存的,为了保证ID不丢失,需要持久化Redis。不过Redis的两种持久化方式各有优缺点。具体可以参考上一篇采访者:请说说Redis是如何保证宕机后数据不丢失的;数据库自增ID前面我们提到,单个数据库在分布式环境下是不能再使用自增ID的,因为每个MySQL实例的自增ID都是从1开始,步长依次递增1。在这个case,很容易想到是否可以考虑为每个数据库设置不同的步长。如果我们设置不同的步长,这样可以保证每个数据库实例都能生成一个ID,不重复。一个简单的系统虽然可以这么用,但是还是有几个问题:依赖数据库DB,在分布式环境下,如果过多依赖数据库是有风险的,不能支持高并发,尤其是一些电商transactions场景下,数据库无法处理每秒几十万的QPS;不同数据库实例的数据不能直接链接,需要额外的存储将数据串起来,增加业务复杂度;推特的雪花算法——snowflake雪花算法是推特开源的分布式ID生成算法。这个算法提供了一个标准的思路。很多公司都参考这个算法做了自己的实现。比较有名的是美团的Leaf。这里重点介绍雪花算法是如何实现的。有兴趣的可以参考文章https://tech.meituan.com/2017/04/21/mt-leaf.html看美团的leaf实现原理。SnowflakeAlgorithm的思想是整体化为部分,将分布式ID的生成分发到各个机房和机器上,用一个64位的long类型结构来表示一个ID。64的结构如下,第一个符号Bit0,然后是41位时间戳,后面10位是机房加机,最后12位是序号。上面的结构就是雪花算法的基本结构。不同的公司会根据自己的业务做出相应的调整。有的可以使用32位或者其他数字,时间戳的位数也可以根据实际情况调整。10位workerID有机房的公司可以用机房和机器组成,没有机房的公司可以直接用机器组成,也可以根据情况适当调整序位。我们可以做个简单的计算,41位的时间位就是2^41/(365*24*3600*1000)=69年,每台机器每毫秒可以生成2^12=4096个ID。这是否意味着我们的代码只能运行69年?事实上,它不是。这里的服务在启动的时候会设置一个初始值,这里的时间戳就是机器时间和初始值的差值。那么SnowFlake算法有哪些优缺点呢?因为有时间戳,满足了自增的要求,也有一定的可读性;每个服务都可以直接在自己的机器上生成唯一的ID,只需要配置机房和机器号即可;长度可根据业务调整;缺点是它取决于机器的时钟。如果机器时钟有问题,生成的ID可能重复,需要控制;结合以上原理,我们可以通过Java代码来实现,代码如下:publicclassSnowFlakeUtil{//初始时间戳privatefinalstaticlongSTART_TIMESTAMP=1624796691000L;//数据中心占用的位数privatefinalstaticlongDATA_CENTER_BIT=5;//数量ofdigitscompoundbythemachineidentificationprivatefinalstaticlongMACHINE_BIT=5;//序列号占用的位数privatefinalstaticlongSEQUENCE_BIT=12;/***各部分的最大值*/privatefinalstaticlongMAX_SEQUENCE=~(-1L<
