介绍

在很多业务系统中,我们经常会遇到生成全局唯一的分布式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. setuserId 0 
  2. incr usrId //返回1 

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

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

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

「这端代码将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

标签: 事故 复盘 订单 ID