微信号:AndroidChinaNet

介绍:这里有资讯,干货,技术,源码,精彩内容不容错过!

全局唯一ID设计

2016-04-23 21:29 Android开发中文站

在分布式系统中,经常需要使用全局唯一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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import java.security.SecureRandom;
 
/**
  * 自定义 ID 生成器
  * ID 生成规则: ID长达 64 bits
  *
  * | 41 bits: Timestamp (毫秒) | 3 bits: 区域(机房) | 10 bits: 机器编号 | 10 bits: 序列号 |
  */
public class CustomUUID {
     // 基准时间
     private long twepoch = 1288834974657L;  //Thu, 04 Nov 2010 01:42:54 GMT
     // 区域标志位数
     private final static long regionIdBits = 3L;
     // 机器标识位数
     private final static long workerIdBits = 10L;
     // 序列号识位数
     private final static long sequenceBits = 10L;
 
     // 区域标志ID最大值
     private final static long maxRegionId = -1L ^ (-1L << regionIdBits);
     // 机器ID最大值
     private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
     // 序列号ID最大值
     private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
 
     // 机器ID偏左移10位
     private final static long workerIdShift = sequenceBits;
     // 业务ID偏左移20位
     private final static long regionIdShift = sequenceBits + workerIdBits;
     // 时间毫秒左移23位
     private final static long timestampLeftShift = sequenceBits + workerIdBits + regionIdBits;
 
     private static long lastTimestamp = -1L;
 
     private long sequence = 0L;
     private final long workerId;
     private final long regionId;
 
     public CustomUUID( long workerId,  long regionId) {
 
         // 如果超出范围就抛出异常
         if (workerId > maxWorkerId || workerId <  0 ) {
             throw new IllegalArgumentException( "worker Id can't be greater than %d or less than 0" );
         }
         if (regionId > maxRegionId || regionId <  0 ) {
             throw new IllegalArgumentException( "datacenter Id can't be greater than %d or less than 0" );
         }
 
         this .workerId = workerId;
         this .regionId = regionId;
     }
 
     public CustomUUID( long workerId) {
         // 如果超出范围就抛出异常
         if (workerId > maxWorkerId || workerId <  0 ) {
             throw new IllegalArgumentException( "worker Id can't be greater than %d or less than 0" );
         }
         this .workerId = workerId;
         this .regionId =  0 ;
     }
 
     public long generate() {
         return this .nextId( false 0 );
     }
 
     /**
      * 实际产生代码的
      *
      * @param isPadding
      * @param busId
      * @return
      */
     private synchronized long nextId( boolean isPadding,  long busId) {
 
         long timestamp = timeGen();
         long paddingnum = regionId;
 
         if (isPadding) {
             paddingnum = busId;
         }
 
         if (timestamp < lastTimestamp) {
             try {
                 throw new Exception( "Clock moved backwards.  Refusing to generate id for " + (lastTimestamp - timestamp) +  " milliseconds" );
             catch (Exception e) {
                 e.printStackTrace();
             }
         }
 
         //如果上次生成时间和当前时间相同,在同一毫秒内
         if (lastTimestamp == timestamp) {
             //sequence自增,因为sequence只有10bit,所以和sequenceMask相与一下,去掉高位
             sequence = (sequence +  1 ) & sequenceMask;
             //判断是否溢出,也就是每毫秒内超过1024,当为1024时,与sequenceMask相与,sequence就等于0
             if (sequence ==  0 ) {
                 //自旋等待到下一毫秒
                 timestamp = tailNextMillis(lastTimestamp);
             }
         else {
             // 如果和上次生成时间不同,重置sequence,就是下一毫秒开始,sequence计数重新从0开始累加,
             // 为了保证尾数随机性更大一些,最后一位设置一个随机数
             sequence =  new SecureRandom().nextInt( 10 );
         }
 
         lastTimestamp = timestamp;
 
         return ((timestamp - twepoch) << timestampLeftShift) | (paddingnum << regionIdShift) | (workerId << workerIdShift) | sequence;
     }
 
     // 防止产生的时间比之前的时间还要小(由于NTP回拨等问题),保持增量的趋势.
     private long tailNextMillis( final long lastTimestamp) {
         long timestamp =  this .timeGen();
         while (timestamp <= lastTimestamp) {
             timestamp =  this .timeGen();
         }
         return timestamp;
     }
 
     // 获取当前的时间戳
     protected long timeGen() {
         return System.currentTimeMillis();
     }
}


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


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

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

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



 
Android开发中文站 更多文章 Android的前世今生 Android开发工具介绍(一) Android开发工具介绍(二) Android开发工具介绍(三) Android开发者必备的六个工具
猜您喜欢 [django]难解决的问题往往是最傻逼的 每天学点C++知识:不要去做编译器的工作 【原译】webpack 2和babel 6的tree-shaking 入门必备:自学编程,如何做到无师自通? 程序员常有,优秀程序员不常有