分布式ID生成策略

Posted 无虑的小猪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式ID生成策略相关的知识,希望对你有一定的参考价值。

  在分布式系统中,肯定避免不了获取全局唯一ID,用于业务主键,本节主要学习分布式ID常用的生成方法。

一、UUID

  UUID (Universally Unique Identifier),通用唯一识别码。UUID是基于当前时间、计数器(counter)和硬件标识(通常为无线网卡的MAC地址)等数据计算生成的。

  UUID是JDK提供的工具类,在util包,UUID的生成方式:

for (int i = 0; i < 3; i++) 
    TimeUnit.MILLISECONDS.sleep(10);
    UUID uuid = UUID.randomUUID();
    System.out.println(uuid.toString() + "----" + uuid.toString().length());

  生成的UUID示例:

9b896468-1f9b-4b04-84e7-adedc078f368----36
e772633c-fb6a-4aa3-90cd-52d86e43a0c1----36
45d74ae0-8025-4570-a8b7-fea6d0c94177----36

UUID通过当前日期与时间、时钟序列、全局唯一的机器识别号生成,是一组32位数的16进制数字,以连字号分隔的五组来显示,总共有 36个字符。

UUID做分布式ID的优缺点:

优点

生成简单,性能好

缺点

UUID是无序的,无法保证趋势递增;

存储在数据库中查询效率低下影响性能;

占用空间大,传输效率低

  分布式id一般作为业务主键存储在数据库中,一般业务主键都包含索引,mysql的索引通过B+树实现,UUID数据插入数据库,数据库为优化查询会对索引底层的b+树进行修改,由于UUID是无序的,每次UUID数据的插入都会对主键生成的b+树做很大的调整,影响性能,所以UUID一般不用做分布式ID的生成。

二、数据库主键自增ID

2.1、单数据库场景

  创建数据库表结构时,可以给对应表的主键设置自动递增。

  

  单数据库场景,使用自增主键可满足基本要求。

2.2、分库分表场景

  数据库主键自增方式仅适用于单个数据库的场景,若在分库分表的场景中,直接利用单数据库主键自增无法保证ID唯一。这种场景下,可以将主键独立出来单独维护,创建主键维护表。

主键维护表结构:

 CREATE TABLE `id_generator` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \'主键id\',
   `value` varchar(2) NOT NULL,
   PRIMARY KEY (`id`) ,
   UNIQUE KEY `uk_id_generator_value` (`value`) USING BTREE
 ) ENGINE=InnoDB AUTO_INCREMENT 1 DEFAULT CHARSET=utf8mb4;

  在插入数据之前,从主键维护表中获取对应的主键值保证ID的唯一性。

 BEGIN;
 REPLACE INTO id_generator (`value`) values (\'id\') ;
 SELECT LAST_INSERT_ID() as resultId from dual;
 COMMIT;

  Replace into 语法特点:表中旧行与主键或唯一索引列具有相同的值,则插入新行之前删除该旧行。

2.3、多主模式

  在数据的双主模式集群中,每个数据库实例都可生成自增的ID。此时控制每个数据库实例主键维护表中的主键初始值及自增步长,可以保证ID的唯一。

  查看主键自增属性

show variables like \'%increment%\'

详情如下:  

  

  auto_increment_increment:表示自增步长

假设有双主模式中数据库A、B,在A、B数据库中的操作演示如下:

  A数据库,设置初始值为1,步长为2:

  主键维护表设置初始值为1, 详情如下:

 CREATE TABLE `id_generator` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \'主键id\',
   `value` varchar(2) NOT NULL,
   PRIMARY KEY (`id`) ,
   UNIQUE KEY `uk_id_generator_value` (`value`) USING BTREE
 ) ENGINE=InnoDB AUTO_INCREMENT 1 DEFAULT CHARSET=utf8mb4;

  调整步长为2

set auto_increment_increment = 2

B数据库,设置初始值为2,步长为2:

  主键维护表设置初始值为2, 详情如下:

 CREATE TABLE `id_generator` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \'主键id\',
   `value` varchar(2) NOT NULL,
   PRIMARY KEY (`id`) ,
   UNIQUE KEY `uk_id_generator_value` (`value`) USING BTREE
 ) ENGINE=InnoDB AUTO_INCREMENT 2 DEFAULT CHARSET=utf8mb4;

  调整步长为2:

set auto_increment_increment = 2

  最终各数据库实例获取ID详情如下:

  

2.4、号段模式

  号段模式是分布式ID生成常用方式之一,号段模式原理是基于业务服务标识,从数据库中批量获取自增ID,每次从数据库中取出一个范围数值,将获取的范围值添加到系统内存中。

 CREATE TABLE `id_generator` (
   `id` int(10) NOT NULL,
   `max_id` bigint(20) NOT NULL COMMENT \'当前最大id\',
   `step` int(8) NOT NULL COMMENT \'号段的步长\',
   `busi_type` tinyint(1) NOT NULL COMMENT \'业务类型 0 - 订单;1 - 商品;2-物流\',
   `version` int(8) NOT NULL COMMENT \'版本号\',
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

  version 作为一个乐观锁,每次都更新version,保证并发时数据的正确性。等号段ID用完,再次向数据库申请新号段,对max_id做一次更新操作, update max_id= max_id + step,update成功则说明新号段获取成功,新的号段范围是(max_id ,max_id +step]。

   

  由于多业务端可能同时操作,所以采用版本号version乐观锁方式更新,这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。

  缺点:服务器重启,单点故障会造成ID不连续。可做如下优化:将查询到的号段信息存储在Redis中做缓存,而非系统内存中。

2.5、总结

  基于数据库自增主键获取分布式ID,在高并发的场景下,性能会成为瓶颈,不易拓展。

三、Redis的INCR命令生成分布式ID

  基于全局唯一ID的特性,可以通过Redis的INCR命令来生成全局唯一ID。基于Redis的INCR指令生成分布式ID的优缺点:

优点

1、不依赖于数据库,性能优于数据库

2、数字ID有序,对分页处理和排序都很友好

缺点

1、需要引入redis,增加系统复杂度

2、生成ID持久化、单节点宕机故障问题

  针对故障问题可以通过Redis集群来处理,比如有三个Redis的Master节点。可以初始化每台Redis的值分别是1,2,3,然后分别把分布式ID的KEY用Hash Tags固定每一个master节点,步长就是master节点的个数。各个Redis生成的ID为:A:1,4,7;B:2,5,8;C:3,6,9。

  Redis分布式ID的简单demo:

 @Autowired
 private StringRedisTemplate redisTemplate;
 @Test
 public void testRedisId() 
     nextId("order");
 
 /**
  * 根据业务类型获取分布式ID
  * @param busiType  业务类型
  * @return
  */
 public long nextId(String busiType) 
     return redisTemplate.opsForValue().increment("disId:" + busiType);
 

四、雪花算法

4.1、雪花算法简介

  雪花算法(Snowflake)是由Twitter开源的分布式ID生成算法,用于在不同的机器上生成唯一ID的算法。雪花算法会生成一个64bit的数字作为分布式ID,保证这个ID自增并且全局唯一。在Java中64Bit为的整数为Long类型,Snowflake算法生成的分布式ID在Java中用long类型来储存。生成的64位ID结构如下:

  

  ·占用1bit,第一位为符号位,不使用;

  ·41位的时间戳,41bit位可以表示2^41个数,每个数代表的是毫秒,雪花算法的时间年限是(2^41)/(1000×60×60×24×365)=69年;

  ·10bit表示是机器数。其中5位datacenterId,5位workerId。最大部署机器数: 2^ 10 = 1024台机器。在实际集群部署中,通常为每个集群节点配置不同的workerId、datacenterId来解决生成的分布式ID冲突问题。

  ·12bit位是自增序列,表示2^12=4096个数,每个节点同一毫秒内可以生成4096个ID,若达到这个最大值,通过while循环阻塞到下一个毫秒,直到获得新的时间戳。

4.2、雪花算法实现

 /**
  * Twitter_Snowflake
  * SnowFlake的结构如下(每部分用-分开):
  * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
  * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
  * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
  * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
  * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId
  * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号
  * 加起来刚好64位,为一个Long型。
  * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
  */
 public class SnowflakeIdWorker 
 
     // ==============================Fields===========================================
     /**
      * 开始时间截 (2020-11-03,一旦确定不可更改,否则时间被回调,或者改变,可能会造成id重复或冲突)
      */
     private final long twepoch = 1604374294980L;
 
     /**
      * 机器id所占的位数
      */
     private final long workerIdBits = 5L;
 
     /**
      * 数据标识id所占的位数
      */
     private final long datacenterIdBits = 5L;
 
     /**
      * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
      */
     private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
 
     /**
      * 支持的最大数据标识id,结果是31
      */
     private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
 
     /**
      * 序列在id中占的位数
      */
     private final long sequenceBits = 12L;
 
     /**
      * 机器ID向左移12位
      */
     private final long workerIdShift = sequenceBits;
 
     /**
      * 数据标识id向左移17位(12+5)
      */
     private final long datacenterIdShift = sequenceBits + workerIdBits;
 
     /**
      * 时间截向左移22位(5+5+12)
      */
     private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
 
     /**
      * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
      */
     private final long sequenceMask = -1L ^ (-1L << sequenceBits);
 
     /**
      * 工作机器ID(0~31)
      */
     private long workerId;
 
     /**
      * 数据中心ID(0~31)
      */
     private long datacenterId;
 
     /**
      * 毫秒内序列(0~4095)
      */
     private long sequence = 0L;
 
     /**
      * 上次生成ID的时间截
      */
     private long lastTimestamp = -1L;
 
     //==============================Constructors=====================================
 
     /**
      * 构造函数
      *
      */
     public SnowflakeIdWorker() 
         this.workerId = 0L;
         this.datacenterId = 0L;
     
 
     /**
      * 构造函数
      *
      * @param workerId     工作ID (0~31)
      * @param datacenterId 数据中心ID (0~31)
      */
     public SnowflakeIdWorker(long workerId, long datacenterId) 
         if (workerId > maxWorkerId || workerId < 0) 
             throw new IllegalArgumentException(String.format("worker Id can\'t be greater than %d or less than 0", maxWorkerId));
         
         if (datacenterId > maxDatacenterId || datacenterId < 0) 
             throw new IllegalArgumentException(String.format("datacenter Id can\'t be greater than %d or less than 0", maxDatacenterId));
         
         this.workerId = workerId;
         this.datacenterId = datacenterId;
     
 
     // ==============================Methods==========================================
 
     /**
      * 获得下一个ID (该方法是线程安全的)
      *
      * @return SnowflakeId
      */
     public synchronized long nextId() 
         long timestamp = timeGen();
 
         //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
         if (timestamp < lastTimestamp) 
             throw new RuntimeException(
                     String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
         
 
         //如果是同一时间生成的,则进行毫秒内序列
         if (lastTimestamp == timestamp) 
             sequence = (sequence + 1) & sequenceMask;
             //毫秒内序列溢出
             if (sequence == 0) 
                 //阻塞到下一个毫秒,获得新的时间戳
                 timestamp = tilNextMillis(lastTimestamp);
             
         
         //时间戳改变,毫秒内序列重置
         else 
             sequence = 0L;
         
 
         //上次生成ID的时间截
         lastTimestamp = timestamp;
 
         //移位并通过或运算拼到一起组成64位的ID
         return ((timestamp - twepoch) << timestampLeftShift) //
                 | (datacenterId << datacenterIdShift) //
                 | (workerId << workerIdShift) //
                 | sequence;
     
 
     /**
      * 阻塞到下一个毫秒,直到获得新的时间戳
      *
      * @param lastTimestamp 上次生成ID的时间截
      * @return 当前时间戳
      */
     protected long tilNextMillis(long lastTimestamp) 
         long timestamp = timeGen();
         while (timestamp <= lastTimestamp) 
             timestamp = timeGen();
         
         return timestamp;
     
 
     /**
      * 返回以毫秒为单位的当前时间
      *
      * @return 当前时间(毫秒)
      */
     protected long timeGen() 
         return System.currentTimeMillis();
     
 
     /**
      * 随机id生成,使用雪花算法
      *
      * @return
      */
     public static String getSnowId() 
         SnowflakeIdWorker sf = new SnowflakeIdWorker();
         String id = String.valueOf(sf.nextId());
         return id;
     
 
     //=========================================Test=========================================
 
     /**
      * 测试
      */
     public static void main(String[] args) 
         SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
         for (int i = 0; i < 1000; i++) 
             long id = idWorker.nextId();
             System.out.println(id);
         
     
 

雪花算法优点:

  适合高并发场景,每秒可生成百万个ID;不依赖第三方库或中间件;基于时间戳,生成的ID趋势递增,并且有序。

雪花算法缺点

  雪花算法生成分布式ID依赖于系统时间,如果服务器时钟回拨可能会生成重复id。

4.3、雪花算法原理

  

   在雪花算法的实现中,41位比特位是用当前系统时间减去设置的初始时间戳值,可根据需求执行定义,只要不大于系统时间即可。也可根据规划的系统运营生命周期,适当的缩减64Bit中时间戳所占用的比特数,用来提高每毫秒生成分布式ID的数量。

五、基于雪花算法的拓展

5.1、Uidgenerator(百度)

  UidGenerator是Java实现的, 基于Snowflake算法的唯一ID生成器。UidGenerator以组件形式工作在应用项目中, 支持自定义workerId位数和初始化策略。 UidGenerator通过借用未来时间来解决雪花算法中sequence天然存在的并发限制; 采用RingBuffer来缓存已生成的UID, 并行化UID的生产和消费, 同时对CacheLine补齐,避免了由RingBuffer带来的硬件级「伪共享」问题。

  Snowflake算法描述:指定机器 & 同一时刻 & 某一并发序列,是唯一的。据此可生成一个64 bits的唯一ID(long)。Uidgenerator对64Bit的结构定义做了调整,调整后结构如下:

默认采用上图字节分配方式:

  sign(1bit) :固定1bit符号标识,即生成的UID为正数。

  delta seconds (28 bits) :当前时间,相对于时间基点"2016-05-20"的增量值,单位:秒,最多可支持约8.7年

  worker id (22 bits) :机器id,最多可支持约420w次机器启动。内置实现为在启动时由数据库分配,默认分配策略为用后即弃,后续可提供复用策略。

  sequence (13 bits) :每秒下的并发序列,13 bits可支持每秒8192个并发。

false sharing 伪共享:

  两个CPU不断争夺缓存行控制权,不断使对方的缓存行失效,写数据回内存的行为导致性能下降。这种行为就叫做cache伪共享。

  关于伪共享概念参考博客:https://blog.csdn.net/qq_28119741/article/details/102815659

  

  源码地址:https://github.com/baidu/uid-generator

  中文文档地址:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md

5,2、美团(Leaf)

  由美团开发,开源项目链接:https://github.com/Meituan-Dianping/Leaf

  Leaf同时支持号段模式和snowflake算法模式,可以切换使用。ID号码是趋势递增的8byte的64位数字,满足上述数据库存储的主键要求。

  Leaf的snowflake模式依赖于ZooKeeper,不同于原始snowflake算法也主要是在workId的生成上,Leaf中workId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId。

  Leaf的号段模式是对直接用数据库自增ID充当分布式ID的一种优化,减少对数据库的频率操作。相当于从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,业务服务将号段在本地生成1~1000的自增ID并加载到内存。

特性:

  1)全局唯一,绝对不会出现重复的ID,且ID整体趋势递增。

  2)高可用,服务完全基于分布式架构,即使MySQL宕机,也能容忍一段时间的数据库不可用。

  3)高并发低延时,在CentOS 4C8G的虚拟机上,远程调用QPS可达5W+,TP99在1ms内。

  4)接入简单,直接通过公司RPC服务或者HTTP调用即可接入。

  Leaf采用双buffer的方式,它的服务内部有两个号段缓存区segment。当前号段已消耗10%时,还没能拿到下一个号段,则会另启一个更新线程去更新下一个号段。

  简而言之就是Leaf保证了总是会多缓存两个号段,即便哪一时刻数据库挂了,也会保证发号服务可以正常工作一段时间。

5.3、滴滴(TinyID)

  由滴滴开发,开源项目链接:https://github.com/didi/tinyid

  Tinyid是在美团(Leaf)的leaf-segment算法基础上升级而来,不仅支持了数据库多主节点模式,还提供了tinyid-client客户端的接入方式,使用起来更加方便。但和美团(Leaf)不同的是,Tinyid只支持号段一种模式不支持雪花模式。Tinyid提供了两种调用方式,一种基于Tinyid-server提供的http方式,另一种Tinyid-client客户端方式。每个服务获取一个号段(1000,2000]、(2000,3000]、(3000,4000]

特性:

  1)全局唯一的long型ID

  2)趋势递增的id

  3)提供 http 和 java-client 方式接入

  4)支持批量获取ID

  5)支持生成1,3,5,7,9...序列的ID

  6)支持多个db的配置

适用场景:只关心ID是数字,趋势递增的系统,可以容忍ID不连续,可以容忍ID的浪费

不适用场景:像类似于订单ID的业务,因生成的ID大部分是连续的,容易被扫库、或者推算出订单量等信息

 

分布式全局唯一ID生成策略?

一、背景

分布式系统中我们会对一些数据量大的业务进行分拆,如:用户表,订单表。因为数据量巨大一张表无法承接,就会对其进行分库分表。
但一旦涉及到分库分表,就会引申出分布式系统中唯一主键ID的生成问题。

1.1 唯一ID的特性

  1. 整个系统ID唯一;
  2. ID是数字类型,而且是趋势递增;
  3. ID简短,查询效率快。

1.2 递增与趋势递增

递增 趋势递增
第一次生成的ID为12,下一次生成的ID是13,再下一次生成的ID是14。 什么是?如:在一段时间内,生成的ID是递增的趋势。如:再一段时间内生成的ID在【0,1000】之间,过段时间生成的ID在【1000,2000】之间。但在【0-1000】区间内的时候,ID生成有可能第一次是12,第二次是10,第三次是14。

二、方案

2.1 UUID

UUID全称:Universally Unique Identifier。标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例:9628f6e9-70ca-45aa-9f7c-77afe0d26e05

  • 优点:
  1. 代码实现简单;
  2. 本机生成,没有性能问题;
  3. 因为是全球唯一的ID,所以迁移数据容易。
  • 缺点:
  1. 每次生成的ID是无序的,无法保证趋势递增;
  2. UUID的字符串存储,查询效率慢;
  3. 存储空间大;
  4. ID本身无业务含义,不可读。
  • 应用场景:
  1. 类似生成token令牌的场景;
  2. 不适用一些要求有趋势递增的ID场景,不适合作为高性能需求的场景下的数据库主键。

也有在线生成UUID的网站,如果你的项目上用到了UUID,可以用来生成临时的测试数据。https://www.uuidgenerator.net/

2.2 MySQL主键自增

利用了MySQL的主键自增auto_increment,默认每次ID1

优点:

  1. 数字化,ID递增;
  2. 查询效率高;
  3. 具有一定的业务可读。
  • 缺点:
  1. 存在单点问题,如果MySQL挂了,就没法生成ID了;
  2. 数据库压力大,高并发抗不住。

2.3 MySQL多实例主键自增

这个方案就是解决MySQL的单点问题,在auto_increment基本上面,设置step步长
技术图片

如上,每台的初始值分别为1,2,3...N,步长为N(这个案例步长为4

  • 优点:解决了单点问题;
  • 缺点:一旦把步长定好后,就无法扩容;而且单个数据库的压力大,数据库自身性能无法满足高并发。
  • 应用场景:数据不需要扩容的场景。

2.4 基于Redis实现

  • 单机:Redisincr函数在单机上是原子操作,可以保证唯一且递增。

  • 集群:单机Redis可能无法支撑高并发。集群情况下,可以使用步长的方式。比如有5个Redis节点组成的集群,它们生成的ID分别为:

A: 1,6,11,16,21
B: 2,7,12,17,22
C: 3,8,13,18,23
D: 4,9,14,19,24
E: 5,10,15,20,25
  • 优点:有序递增,可读性强。
  • 缺点:占用带宽,每次要向Redis进行请求。

三、优化方案

3.1、改造数据库主键自增

数据库的自增主键的特性,可以实现分布式ID,适合做userId,正好符合如何永不迁移数据和避免热点? 但这个方案有严重的问题:

  1. 一旦步长定下来,不容易扩容;
  2. 数据库压力山大。
  • 为什么压力大?

因为我们每次获取ID的时候,都要去数据库请求一次。那我们可以不可以不要每次去取?

可以请求数据库得到ID的时候,可设计成获得的ID是一个ID区间段。
技术图片

  • 上图ID规则表含义:
  1. id表示为主键,无业务含义;
  2. biz_tag为了表示业务,因为整体系统中会有很多业务需要生成ID,这样可以共用一张表维护;
  3. max_id表示现在整体系统中已经分配的最大ID;
  4. desc描述;
  5. update_time表示每次取的ID时间;
  • 整体流程:
  1. 【用户服务】在注册一个用户时,需要一个用户ID;会请求【生成ID服务(是独立的应用)】的接口;
  2. 【生成ID服务】会去查询数据库,找到user_tagid,现在的max_id0step=1000;
  3. 【生成ID服务】把max_idstep返回给【用户服务】;并且把max_id更新为max_id = max_id + step,即更新为1000;
  4. 【用户服务】获得max_id=0step=1000;
  5. 这个用户服务可以用ID=【max_id + 1,max_id+step】区间的ID,即为【1,1000】;
  6. 【用户服务】会把这个区间保存到jvm中;
  7. 【用户服务】需要用到ID的时候,在区间【1,1000】中依次获取ID,可采用AtomicLong中的getAndIncrement方法;
  8. 如果把区间的值用完了,再去请求【生产ID服务】接口,获取到max_id1000,即可以用【max_id + 1,max_id+step】区间的ID,即为【1001,2000】

  9. 该方案就非常完美的解决了数据库自增的问题,而且可以自行定义max_id的起点,和step步长,非常方便扩容;
  10. 也解决了数据库压力的问题,因为在一段区间内,是在jvm内存中获取的,而不需要每次请求数据库。即使数据库宕机了,系统也不受影响,ID还能维持一段时间。

3.2 竞争问题

以上方案中,如果是多个用户服务,同时获取ID,同时去请求【ID服务】,在获取max_id的时候会存在并发问题。如:

用户服务A,取到的max_id=1000 ;用户服务B取到的也是max_id=1000,那就出现了问题,ID重复了。

解决方案是:加分布式锁,保证同一时刻只有一个用户服务获取max_id

3.3 突发阻塞问题

技术图片

因为竞争问题,所有只有一个用户服务去操作数据库,其他二个会被阻塞。出现的现象就是一会儿突然系统耗时变长,怎么去解决?

  • buffer方案

技术图片

流程如下:

  1. 当前获取IDbuffer1中,每次获取IDbuffer1中获取;
  2. buffer1中的ID已经使用到了100,也就是达到区间的10%;
  3. 达到了10%,先判断buffer2中有没有去获取过,如果没有就立即发起请求获取ID线程,此线程把获取到的ID,设置到buffer2中;
  4. 如果buffer1用完了,会自动切换到buffer2;
  5. buffer2用到10%了,也会启动线程再次获取,设置到buffer1中;
  6. 依次往返。

3.4 总结

  1. buffer的方案就达到了业务场景用的ID,都是在jvm内存中获得的,从此不需要到数据库中获取了,数据库宕机时长长点儿也没太大影响了。
  2. 因为会有一个线程,会观察什么时候去自动获取。两个buffer之间自行切换使用,就解决了突发阻塞的问题。

四、其他方式

还有一些其他的ID生成方案,比如:

  1. 滴滴:时间+起点编号+车牌号;
  2. 淘宝订单:时间戳+用户ID
  3. 其他电商:时间戳+下单渠道+用户ID,有的会加上订单第一个商品的ID;
  4. MongoDBID:通过时间+机器码+pid+inc共12个字节,4+3+2+3的方式最终标识成一个24长度的十六进制字符。

以上是关于分布式ID生成策略的主要内容,如果未能解决你的问题,请参考以下文章

分布式全局唯一ID生成策略?

分布式ID生成策略

常见分布式全局唯一ID生成策略

常见分布式全局唯一ID生成策略

分布式高并发下全局ID生成策略

几种常见的分布式全局唯一ID生成策略