实战全局唯一 ID 设计

Posted 程序员泥瓦匠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实战全局唯一 ID 设计相关的知识,希望对你有一定的参考价值。

一定要点蓝字关注我!!!

在分布式系统中,经常需要使用 全局唯一ID查找对应的数据。产生这种ID需要保证系统全局唯一,而且要高性能以及占用相对较少的空间。

全局唯一ID在数据库中一般会被设成主键,这样为了保证数据插入时索引的快速建立,还需要保持一个有序的趋势。

这样全局唯一ID就需要保证这两个需求:

  • 全局唯一

  • 趋势有序

全局ID产生的几种方式

数据库自增

当服务使用的数据库只有单库单表时,可以利用数据库的 auto_increment来生成全局唯一递增ID.

优势:

  • 简单,无需程序任何附加操作

  • 保持定长的增量

  • 在单表中能保持唯一性

劣势:

  • 高并发下性能不佳,主键产生的性能上限是数据库服务器单机的上限。

  • 水平扩展困难,在分布式数据库环境下,无法保证唯一性。

UUID

一般的语言中会自带UUID的实现,比如Java中UUID方式 UUID.randomUUID().toString(),可以通过服务程序本地产生,ID的生成不依赖数据库的实现。

优势:

  • 本地生成ID,不需要进行远程调用。

  • 全局唯一不重复。

  • 水平扩展能力非常好。

劣势:

  • ID有128 bits,占用的空间较大,需要存成字符串类型,索引效率极低。

  • 生成的ID中没有带Timestamp,无法保证趋势递增

Twitter Snowflake

snowflake是twitter开源的分布式ID生成算法,其核心思想是:产生一个long型的ID,使用其中41bit作为毫秒数,10bit作为机器编号,12bit作为毫秒内序列号。这个算法单机每秒内理论上最多可以生成 1000*(2^12)个,也就是大约 400W的ID,完全能满足业务的需求。

根据 snowflake算法的思想,我们可以根据自己的业务场景,产生自己的全局唯一ID。因为Java中 long类型的长度是64bits,所以我们设计的ID需要控制在64bits。

比如我们设计的ID包含以下信息:

 
   
   
 
  1. | 41 bits: Timestamp | 3 bits: 区域 | 10 bits: 机器编号 | 10 bits: 序列号 |

产生唯一ID的 Java代码:

 
   
   
 
  1. import java.security.SecureRandom;


  2. /**

  3. * 自定义 ID 生成器

  4. * ID 生成规则: ID长达 64 bits

  5. *

  6. * | 41 bits: Timestamp (毫秒) | 3 bits: 区域(机房) | 10 bits: 机器编号 | 10 bits: 序列号 |

  7. */

  8. public class CustomUUID {

  9. // 基准时间

  10. private long twepoch = 1288834974657L; //Thu, 04 Nov 2010 01:42:54 GMT

  11. // 区域标志位数

  12. private final static long regionIdBits = 3L;

  13. // 机器标识位数

  14. private final static long workerIdBits = 10L;

  15. // 序列号识位数

  16. private final static long sequenceBits = 10L;


  17. // 区域标志ID最大值

  18. private final static long maxRegionId = -1L ^ (-1L << regionIdBits);

  19. // 机器ID最大值

  20. private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);

  21. // 序列号ID最大值

  22. private final static long sequenceMask = -1L ^ (-1L << sequenceBits);


  23. // 机器ID偏左移10位

  24. private final static long workerIdShift = sequenceBits;

  25. // 业务ID偏左移20位

  26. private final static long regionIdShift = sequenceBits + workerIdBits;

  27. // 时间毫秒左移23位

  28. private final static long timestampLeftShift = sequenceBits + workerIdBits + regionIdBits;


  29. private static long lastTimestamp = -1L;


  30. private long sequence = 0L;

  31. private final long workerId;

  32. private final long regionId;


  33. public CustomUUID(long workerId, long regionId) {


  34. // 如果超出范围就抛出异常

  35. if (workerId > maxWorkerId || workerId < 0) {

  36. throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");

  37. }

  38. if (regionId > maxRegionId || regionId < 0) {

  39. throw new IllegalArgumentException("datacenter Id can't be greater than %d or less than 0");

  40. }


  41. this.workerId = workerId;

  42. this.regionId = regionId;

  43. }


  44. public CustomUUID(long workerId) {

  45. // 如果超出范围就抛出异常

  46. if (workerId > maxWorkerId || workerId < 0) {

  47. throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");

  48. }

  49. this.workerId = workerId;

  50. this.regionId = 0;

  51. }


  52. public long generate() {

  53. return this.nextId(false, 0);

  54. }


  55. /**

  56. * 实际产生代码的

  57. *

  58. * @param isPadding

  59. * @param busId

  60. * @return

  61. */

  62. private synchronized long nextId(boolean isPadding, long busId) {


  63. long timestamp = timeGen();

  64. long paddingnum = regionId;


  65. if (isPadding) {

  66. paddingnum = busId;

  67. }


  68. if (timestamp < lastTimestamp) {

  69. try {

  70. throw new Exception("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");

  71. } catch (Exception e) {

  72. e.printStackTrace();

  73. }

  74. }


  75. //如果上次生成时间和当前时间相同,在同一毫秒内

  76. if (lastTimestamp == timestamp) {

  77. //sequence自增,因为sequence只有10bit,所以和sequenceMask相与一下,去掉高位

  78. sequence = (sequence + 1) & sequenceMask;

  79. //判断是否溢出,也就是每毫秒内超过1024,当为1024时,与sequenceMask相与,sequence就等于0

  80. if (sequence == 0) {

  81. //自旋等待到下一毫秒

  82. timestamp = tailNextMillis(lastTimestamp);

  83. }

  84. } else {

  85. // 如果和上次生成时间不同,重置sequence,就是下一毫秒开始,sequence计数重新从0开始累加,

  86. // 为了保证尾数随机性更大一些,最后一位设置一个随机数

  87. sequence = new SecureRandom().nextInt(10);

  88. }


  89. lastTimestamp = timestamp;


  90. return ((timestamp - twepoch) << timestampLeftShift) | (paddingnum << regionIdShift) | (workerId << workerIdShift) | sequence;

  91. }


  92. // 防止产生的时间比之前的时间还要小(由于NTP回拨等问题),保持增量的趋势.

  93. private long tailNextMillis(final long lastTimestamp) {

  94. long timestamp = this.timeGen();

  95. while (timestamp <= lastTimestamp) {

  96. timestamp = this.timeGen();

  97. }

  98. return timestamp;

  99. }


  100. // 获取当前的时间戳

  101. protected long timeGen() {

  102. return System.currentTimeMillis();

  103. }

  104. }

使用自定义的这种方法需要注意的几点:

  • 为了保持增长的趋势,要避免有些服务器的时间早,有些服务器的时间晚,需要控制好所有服务器的时间,而且要避免 NTP时间服务器回拨服务器的时间。

  • 在跨毫秒时,序列号总是归0,会使得序列号为0的ID比较多,导致生成的ID取模后不均匀,所以序列号不是每次都归0,而是归一个0到9的随机数。

  • 使用这个 CustomUUID类,最好在一个系统中能保持单例模式运行。

python版本的 snowflake算法实现(感谢 @mailto1587 的python版本翻译):

 
   
   
 
  1. import sys

  2. import random

  3. import threading

  4. import time


  5. from concurrent import futures



  6. class Snowflake(object):

  7. region_id_bits = 2

  8. worker_id_bits = 10

  9. sequence_bits = 11


  10. MAX_REGION_ID = -1 ^ (-1 << region_id_bits)

  11. MAX_WORKER_ID = -1 ^ (-1 << worker_id_bits)

  12. SEQUENCE_MASK = -1 ^ (-1 << sequence_bits)


  13. WORKER_ID_SHIFT = sequence_bits

  14. REGION_ID_SHIFT = sequence_bits + worker_id_bits

  15. TIMESTAMP_LEFT_SHIFT = (sequence_bits + worker_id_bits + region_id_bits)


  16. def __init__(self, worker_id, region_id=0):

  17. self.twepoch = 1288834974657

  18. self.last_timestamp = -1

  19. self.sequence = 0


  20. assert 0 <= worker_id <= Snowflake.MAX_WORKER_ID

  21. assert 0 <= region_id <= Snowflake.MAX_REGION_ID


  22. self.worker_id = worker_id

  23. self.region_id = region_id


  24. self.lock = threading.Lock()


  25. def generate(self, bus_id=None):

  26. return self.next_id(

  27. True if bus_id is not None else False,

  28. bus_id if bus_id is not None else 0

  29. )


  30. def next_id(self, is_padding, bus_id):

  31. with self.lock:

  32. timestamp = self.get_time()

  33. padding_num = self.region_id



  34. if is_padding:

  35. padding_num = bus_id


  36. if timestamp < self.last_timestamp:

  37. try:

  38. raise ValueError(

  39. 'Clock moved backwards. Refusing to'

  40. 'generate id for {0} milliseconds.'.format(

  41. self.last_timestamp - timestamp

  42. )

  43. )

  44. except ValueError:

  45. print(sys.exc_info[2])


  46. if timestamp == self.last_timestamp:

  47. self.sequence = (self.sequence + 1) & Snowflake.SEQUENCE_MASK

  48. if self.sequence == 0:

  49. timestamp = self.tail_next_millis(self.last_timestamp)

  50. else:

  51. self.sequence = random.randint(0, 9)


  52. self.last_timestamp = timestamp


  53. return (

  54. (timestamp - self.twepoch) << Snowflake.TIMESTAMP_LEFT_SHIFT |

  55. (padding_num << Snowflake.REGION_ID_SHIFT) |

  56. (self.worker_id << Snowflake.WORKER_ID_SHIFT) |

  57. self.sequence

  58. )


  59. def tail_next_millis(self, last_timestamp):

  60. timestamp = self.get_time()

  61. while timestamp <= last_timestamp:

  62. timestamp = self.get_time()

  63. return timestamp


  64. def get_time(self):

  65. return int(time.time() * 1000)



  66. def main():

  67. id_set = set()

  68. snowflake = Snowflake(1)


  69. def gen_id():

  70. try:

  71. _id = snowflake.generate()

  72. except Exception as e:

  73. print(e)

  74. else:

  75. assert _id not in id_set

  76. id_set.add(_id)


  77. with futures.ThreadPoolExecutor(max_workers=16) as executor:

  78. futs = [executor.submit(gen_id) for _ in range(100)]


  79. print('{0} IDs in the set'.format(len(id_set)))


  80. if __name__ == '__main__':

  81. main()

- The End -

号外:为读者持续几份最新教程,覆盖了 Spring Boot、Spring Cloud、微服务架构等。



 热门文章:



长按二维码,扫扫关注哦

关注即可得 Spring Boot Cloud、微服务等干货


签到打卡

↓↓↓↓

以上是关于实战全局唯一 ID 设计的主要内容,如果未能解决你的问题,请参考以下文章

高并发之 - 全局有序唯一id Snowflake 应用实战

Redis实战9-全局唯一ID

高并发之 - 全局有序唯一id Snowflake 应用实战

干货技术实战:聊一聊分布式系统全局唯一ID的几种实现方式

干货技术实战:聊一聊分布式系统全局唯一ID的几种实现方式

全局唯一ID设计