本文转载自微信公众号《潜行前行》,作者cscw。转载本文请联系SneakUp公众号。前言每一次HTTP请求、数据库事务执行,在跟踪代码执行的过程中,我们都需要一个与这些业务操作关联的唯一值。对于单机系统,可以使用数据库的自增ID或者时间戳来增加一个机器自增的值,达到唯一值。但是在分布式中,如何实现唯一ID分布式ID的特点数据库自增IDRedis分布式IDZookeeper分布式ID全局唯一UUID优缺点Twitter的雪花算法生成分布式ID分布式ID的特点全局唯一性,必要性,幂等性,如果是根据某些信息生成的,需要保证幂等性,注意安全性,ID中隐藏了一些信息,无法猜到,也猜不到如何生成一个有递增趋势的ID,查询时并进行比较,可以判断出企业经营的时间顺序。数据库自增ID实现简单,ID单调递增,数值类型查询速度快,但单点DB存在宕机风险,无法处理高并发场景CREATETABLEFLIGHT_ORDER(idint(11)unsignedNOTNULLauto_increment,#Auto-incrementIDPRIMARYKEY(id),)ENGINE=innodb;集群下如何保证数据库ID的唯一性当服务随着业务发展扩展到多个大集群时,为了解决单点数据库的压力,数据库也会相应的变成一个集群,那么如何保证集群下数据库ID的唯一性每个数据库实例设置一个初始值和增长步长缺点:不利于后续扩容,后续需要扩容需要手动扩容介入修改初始值和增长步长Redis分布式ID如果系统有上亿条数据,依赖数据库的自增ID。分表分库后,每个数据库实例都需要手动修改,扩展性差,可维护性差。基于Redis的INCR命令生成一个分布式的全局唯一ID。服务从redis获取ID,ID与数据库解耦,可以解决ID和分表分库的问题。而且redis比数据库性能更快,可以支持集群服务并发获取ID。redis的INCR命令有INCRANDGET的原子操作;redis是单进程单线程架构,INCR命令不会有重复IDY);}HINCRBY命令其实为了存储更多关于序列号的信息,可以使用Redis的Hash数据结构,Redis也为Hash提供了HINCRBY命令来实现“INCRANDGET”原子操作//KEY_NAME是哈希结构对应的Key,FIELD_NAME是哈希结构的字段,INCR_BY_NUMBER是增量值redis127.0.0.1:6379>HINCRBYKEY_NAMEFIELD_NAMEINCR_BY_NUMBER宕机序列号恢复问题redis是内存数据库,如果RDB或者AOF持久化不开启,一旦宕机即使开启了RDB持久化也会丢失MachineID数据,因为最近一次快照的时间和最新一次HINCRBY命令的时间可能存在时间差,当数据出现redis宕机时set在宕机后通过RDB快照恢复序列号恢复方案使用关系数据库记录短时间内最大可用序列号MAX_ID。从redis获取ID时,只能取小于MAX_ID的序号。为了计算出最大值,需要定时任务周期性的计算ID消耗率RATE,存储在redis中。当客户端获取到CUR_ID、RATE和MAX_ID后,会根据ID消耗率RATE计算CUR_ID是否接近MAX_ID,如果接近则更新数据库的MAX_ID。Zookeeper分布式ID利用zookeeper持久化有序节点实现分布式ID自增,且zookeeper是高可用集群服务,提交成功消息持久化,不怕宕机或单机问题org.apache.curatorcurator-framework4.2.0org.apache.curatorcurator-recipes4.2.0示例RetryPolicyretryPolicy=newExponentialBackoffRetry(500,3);CuratorFrameworkclient=CuratorFrameworkFactory.builder().connectString("localhost:2181").connectionTimeoutMs(5000).sessionTimeoutMs(5000).retryPolicy(retryPolicy().bu);client.start();StringsequenceName="root/sequence/distributedId";DistributedAtomicLongdistAtomicLong=newDistributedAtomicLong(client,sequenceName,retryPolicy);//制作使用DistributedAtomicLong生成自增序列publicLongsequence()throwsException{AtomicValuesequence=this.distAtomicLong.increment();if(sequence.succeeded()){returnsequence.postValue();}else{returnull;}}UUID优缺点是基于数据库,redis,zookeeper的分布式ID高度依赖于一个外部服务。对于某些场景,如果这些外部服务不存在,如何生成分布式ID?JDK自带一个唯一的ID生成器。它具有全局唯一性,也就是UUID,但它是一个无意义的字符串,存储性能差,查询耗时。对于订单系统,不适合作为唯一ID。常见的优化方案是“转换为两个uint64整数存储”或者“减半存储”(减半后不能保证唯一性),但是对于日志系统,或者只是作为一个关联属性,可以唯一标识数据中的序号,你可以使用UUIDStringuuid=UUID.randomUUID().toString()。replaceAll("-","");Twitter的雪花算法生成分布式ID。与UUID一样,雪花算法不依赖于外部服务。雪花算法是Twitter内部分布式项目使用的ID生成算法。受到国内企业的广泛好评。依赖第三方服务,高效SnowflakeID组成结构:正数位(1位)+时间戳(41位)+机器ID(5位)+数据中心(5位)+自增(12位),由总共64位组成的Long类型。1:第一位(1bit):Java中long的最高位是符号位,代表正或负。正数为0,负数为1。一般生成的ID为正数,所以默认为0。2:时间戳部分(41bit):毫秒级时间,不建议保存当前时间戳,但是使用(当前时间戳-固定开始时间戳)的差值,使得生成的ID可以从一个较小的值开始3:workingmachineid(10bit):也叫workId,可以灵活配置,组合可以使用机房或机号。4:序号部分(12bit),自增支持同一个节点在同一毫秒内可以生成4096个ID//Twitter的SnowFlake算法,使用SnowFlake算法生成一个整数publicclassSnowFlakeShortUrl{//起始时间戳staticlongSTART_TIMESTAMP=1624698370256L;//每个部分占用的位数staticlongSEQUENCE_BIT=12;//序列号占用的位数staticlongMACHINE_BIT=5;//机器标识占用的位数staticlongDATA_CENTER_BIT=5;//序号占用的位数由数据中心//各部分的最大值staticlongMAX_SEQUENCE=-1L^(-1L<=0&&dataCenterId<=MAX_DATA_CENTER_NUM,"dataCenterIdisillegal!");Assert.isTrue(machineId>=0||machineId<=MAX_MACHINE_NUM,"machineIdisillegal!");this.dataCenterId=dataCenterId;this.machineId=machineId;}//生成下一个IDpublicsynchronizedlongnextId(){currTimeStamp=System.currentTimeMillis();Assert.isTrue(currTimeStamp>=lastTimeStamp,"Clockmovedbackwards");if(currTimeStamp==lastTimeStamp){//同一毫秒内,序号自动递增sequence=(sequence+1)&MAX_SEQUENCE;if(sequence==0L){//同一毫秒序号已经达到最大,获取下一毫秒currTimeStamp=getNextMill();}}else{sequence=0L;//不同毫秒,序号设置为0}lastTimeStamp=currTimeStamp;return(currTimeStamp-START_TIMESTAMP)<