|
|
|
|
公众号矩阵

事故复盘:订单ID被我们搞重复了!

在很多业务系统中,我们经常会遇到生成全局唯一的分布式ID的需求,如IM系统,订单系统等。那么生成全局唯一的分布式ID的方法有哪些呢?

作者: 李立敏 来源: Java识堂|2021-10-13 06:49

介绍

在很多业务系统中,我们经常会遇到生成全局唯一的分布式ID的需求,如IM系统,订单系统等。那么生成全局唯一的分布式ID的方法有哪些呢?

UUID

  1. // 3eece1c6-5b57-4bce-a306-6c49e44a1f90 
  2. UUID.randomUUID().toString() 

「本地生成,生成速度快,但识别性差,没有顺序性」

可以用来标识图片等,不能用作数据库主键

数据库自增主键

「我们原来刚开始做IM系统的时候就单独建了一个表来获取自增id作为消息的ID」,单独开一张表来获取自增id也不会影响对消息分库分表

Zookeeper

「每次要生成一个新Id时,创建一个持久顺序节点,创建操作返回的节点序号,即为新Id,然后把比自己节点小的删除即可」

这种方式能生成的Id比较少,因为数字位数比较少

Redis

「用incr命令即可实现」

设置一个key为userId,值为0,每次获取userId的时候,对userId加1再获取

  1. set userId 0 
  2. incr usrId //返回1 

每获取一次id都会和redis有一个网络交互的过程,因此可以改进为如下形式

直接获取一段userId的最大值,缓存到本地慢慢累加,快到了userId的最大值时,再去获取一段,一个用户服务宕机了,也顶多一小段userId没有用到

  1. set userId 0 
  2. incr usrId //返回1 
  3. incrby userId 1000 //返回10001 

雪花算法

「雪花算法是最常见的解决方案,满足全局唯一,趋势递增,因此可以用来作为数据库主键」

雪花算法是由Twitter公布的分布式主键生成算法,它能够保证不同进程主键的不重复性,以及相同进程主键的有序性。

在同一个进程中,它首先是通过时间位保证不重复,如果时间相同则是通过序列位保证。同时由于时间位是单调递增的,且各个服务器如果大体做了时间同步,那么生成的主键在分布式环境可以认为是总体有序的,这就保证了对索引字段的插入的高效性。例如MySQL的Innodb存储引擎的主键。

使用雪花算法生成的主键,二进制表示形式包含4部分,从高位到低位分表为:1bit符号位、41bit时间戳位、10bit工作进程位以及12bit序列号位。

「符号位(1bit)」

预留的符号位,恒为零。

「时间戳位(41bit)」

41位的时间戳可以容纳的毫秒数是2的41次幂,一年所使用的毫秒数是:365 * 24 * 60 * 60 * 1000。通过计算可知:

  1. Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L); 

结果约等于69.73年。ShardingSphere的雪花算法的时间纪元从2016年11月1日零点开始,可以使用到2086年,相信能满足绝大部分系统的要求。

「工作进程位(10bit)」

该标志在Java进程内是唯一的,如果是分布式应用部署应保证每个工作进程的id是不同的。该值默认为0,可通过属性设置。

「一般情况这10bit会拆分为2个5bit」

前5个bit代表机房id,最多代表 2 ^ 5 个机房(32 个机房) 后5个bit代表机器id,每个机房里可以代表 2 ^ 5 个机器(32 台机器)

「因此这个服务最多可以部署在 2^10 台机器上,也就是1024台机器」

「序列号位(12bit)」

该序列是用来在同一个毫秒内生成不同的ID。如果在这个毫秒内生成的数量超过4096(2的12次幂),那么生成器会等待到下个毫秒继续生成。

理解了实现思路,我们来把算法实现一遍

  1. public class SnowFlake { 
  2.  
  3.     /** 
  4.      * 起始的时间戳 
  5.      */ 
  6.     private final static long START_STMP = 1480166465631L; 
  7.  
  8.     /** 
  9.      * 每一部分占用的位数 
  10.      */ 
  11.     private final static long SEQUENCE_BIT = 12; //序列号占用的位数 
  12.     private final static long MACHINE_BIT = 5;   //机器标识占用的位数 
  13.     private final static long DATACENTER_BIT = 5;//数据中心占用的位数 
  14.  
  15.     /** 
  16.      * 每一部分的最大值 
  17.      */ 
  18.     private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT); 
  19.     private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT); 
  20.     private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT); 
  21.  
  22.     /** 
  23.      * 每一部分向左的位移 
  24.      */ 
  25.     private final static long MACHINE_LEFT = SEQUENCE_BIT; 
  26.     private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT; 
  27.     private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT; 
  28.  
  29.     private long datacenterId;  //数据中心 
  30.     private long machineId;     //机器标识 
  31.     private long sequence = 0L; //序列号 
  32.     private long lastStmp = -1L;//上一次时间戳 
  33.  
  34.     public SnowFlake(long datacenterId, long machineId) { 
  35.         if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) { 
  36.             throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0"); 
  37.         } 
  38.         if (machineId > MAX_MACHINE_NUM || machineId < 0) { 
  39.             throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0"); 
  40.         } 
  41.         this.datacenterId = datacenterId; 
  42.         this.machineId = machineId; 
  43.     } 
  44.  
  45.     /** 
  46.      * 产生下一个ID 
  47.      * 
  48.      * @return 
  49.      */ 
  50.     public synchronized long nextId() { 
  51.         long currStmp = getNewstmp(); 
  52.         // 发生时钟回拨 
  53.         if (currStmp < lastStmp) { 
  54.             throw new RuntimeException("Clock moved backwards.  Refusing to generate id"); 
  55.         } 
  56.  
  57.         if (currStmp == lastStmp) { 
  58.             //相同毫秒内,序列号自增 
  59.             sequence = (sequence + 1) & MAX_SEQUENCE; 
  60.             //同一毫秒的序列数已经达到最大 
  61.             if (sequence == 0L) { 
  62.                 currStmp = getNextMill(); 
  63.             } 
  64.         } else { 
  65.             //不同毫秒内,序列号置为0 
  66.             sequence = 0L; 
  67.         } 
  68.  
  69.         lastStmp = currStmp; 
  70.  
  71.         return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分 
  72.                 | datacenterId << DATACENTER_LEFT       //数据中心部分 
  73.                 | machineId << MACHINE_LEFT             //机器标识部分 
  74.                 | sequence;                             //序列号部分 
  75.     } 
  76.  
  77.     private long getNextMill() { 
  78.         long mill = getNewstmp(); 
  79.         while (mill <= lastStmp) { 
  80.             mill = getNewstmp(); 
  81.         } 
  82.         return mill; 
  83.     } 
  84.  
  85.     private long getNewstmp() { 
  86.         return System.currentTimeMillis(); 
  87.     } 
  88.  
  89.     public static void main(String[] args) { 
  90.         SnowFlake snowFlake = new SnowFlake(2, 3); 
  91.  
  92.         for (int i = 0; i < (1 << 12); i++) { 
  93.             System.out.println(snowFlake.nextId()); 
  94.         } 
  95.  
  96.     } 

「这端代码将workerid分为datacenterId和machineId,如果我们业务上不需要做区分的话,直接使用10位的workerid即可。」

workerid生成

我们可以通过zookeeper的有序节点保证id的全局唯一性,比如我通过以下命令创建一个永久有序节点

  1. # 创建一个根节点 
  2. create  /test '' 
  3. # 创建永久有序节点 
  4. create -s /test/ip-port- '' 
  5. # 返回 Created /test/ip-port-0000000000 

「ip和port可以为应用的ip和port,规则你来定,别重复就行」

其中/test/ip-port-0000000000中的0000000000就是我们的workerid

说一个我们原来生产环境遇到的一个workerid重复的问题,生成workid的方式那叫一个简洁

  1. // uid为zookeeper中的一个有序持久节点 
  2. List<String> pidListNode = zkClient.getChildren("uid"); 
  3. String workerId = String.valueOf(pidListNode.size()); 
  4. zkClient.create("uid", new byte[0], CreateMode.PERSISTENT_SEQUENTIAL); 

「你能看出来这段代码为什么会造成workid重复吗?」

它把uid子节点的数量作为workid,当2个应用同时执行到第一行代码时,子节点数量是一样的,得到的workerId就会重复。

有意思的是这段代码跑了好几年都没有问题,直到运维把应用的发版效率提高了一点,线上就开始报错了。因为刚开始应用是串行发版,后来改为并行发版

「当使用雪花算法的时候,有可能发生时钟回拨,建议使用开源的框架,如美团的Leaf。」

雪花算法在很多中间件中都被使用过,如seata用来生成全局唯一的事务id

本文转载自微信公众号「Java识堂」,作者李立敏。转载本文请联系Java识堂公众号。

【编辑推荐】

  1. 鸿蒙官方战略合作共建——HarmonyOS技术社区
  2. 大数据分析前景如何
  3. 预计将超过150亿美元隐私技术推动数据保护市场规模
  4. 大数据:阻止网络安全威胁的五种可行方法
  5. 哪些人员需要特别注意移动数据安全?
  6. 多方数据泄露成本是单方的26倍
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

带你轻松入门 RabbitMQ

带你轻松入门 RabbitMQ

轻松入门RabbitMQ
共4章 | loong576

50人订阅学习

数据湖与数据仓库的分析实践攻略

数据湖与数据仓库的分析实践攻略

助力现代化数据管理:数据湖与数据仓库的分析实践攻略
共3章 | 创世达人

14人订阅学习

云原生架构实践

云原生架构实践

新技术引领移动互联网进入急速赛道
共3章 | KaliArch

42人订阅学习

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微