分布式系統-主鍵唯一id,訂單編號生成-雪花算法-SnowFlake


  • 分布式系統下 我們每台設備(分布式系統-獨立的應用空間-或者docker環境)
    * SnowFlake的優點是,整體上按照時間自增排序,並且整個分布式系統內不會產生ID碰撞(由數據中心ID和機器ID作區分),並且效率較高,經測試,SnowFlake每秒能夠產生26萬ID左右。
  • 所以我們可以為分布式系統下:分庫分表主鍵,分庫,多庫的情況下的訂單編號使用這種方式進行唯一number操作

 

  • 雖然這種方法正常情況下還是可以湊合用的,但是假如設備出現時間差,在極度大的並發情況下,還是會出現問題的,設備掩碼4095,
  • 為這個方案所支持的最小划分粒度是「毫秒 * 線程」,單線程(Snowflake 里對應的概念是 Worker)的每秒容量是12-bit,也就是接近4096.
  • 當時鍾回撥則可能出現服務掛起,處於不可用狀態,有哪些時間會發生呢,比如遇到閏秒時間等等情況.

  當然有些其他的辦法:像專門使用數據庫生成唯一id,加入需要多N台設備,每一台設備的起始值不同,步長則為N,例如:要部署N台機器,步長需設置為N,每台的初始值依次為0,1,2...N-1,

 

  但是這種有很大缺點,一開始就要整合好:缺點如下:

  • 系統水平擴展比較困難,比如定義好了步長和機器台數之后,如果要添加機器該怎么做?假設現在只有一台機器發號是1,2,3,4,5(步長是1),這個時候需要擴容機器一台。可以這樣做:把第二台機器的初始值設置得比第一台超過很多,比如14(假設在擴容時間之內第一台不可能發到14),同時設置步長為2,那么這台機器下發的號碼都是14以后的偶數。然后摘掉第一台,把ID值保留為奇數,比如7,然后修改第一台的步長為2。讓它符合我們定義的號段標准,對於這個例子來說就是讓第一台以后只能產生奇數。擴容方案看起來復雜嗎?貌似還好,現在想象一下如果我們線上有100台機器,這個時候要擴容該怎么做?簡直是噩夢。所以系統水平擴展方案復雜難以實現。
  • ID沒有了單調遞增的特性,只能趨勢遞增,這個缺點對於一般業務需求不是很重要,可以容忍。
  • 數據庫壓力還是很大,每次獲取ID都得讀寫一次數據庫,只能靠堆機器來提高性能。

  

  Leaf方案實現

  Leaf這個名字是來自德國哲學家、數學家萊布尼茨的一句話:There are no two identical leaves in the world "世界上沒有兩片相同的樹葉"

  綜合對比上述幾種方案,每種方案都不完全符合我們的要求。所以Leaf分別在上述第二種和第三種方案上做了相應的優化,實現了Leaf-segment和Leaf-snowflake方案。

  1.Leaf-segment數據庫方案 2. Leaf-snowflake方案  具體可參考搜索. 也可去搜索一下 zookeeper的分布式唯一

 

 

 

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型。

唯一的缺點是設備時間要穩定,不能出現回退.

下面就是我的代碼: 沒有代碼不就是白瞎了?

  1 package test;
  2 
  3 /**
  4  * @Title: SnowFlakeUtils.java
  5  * @Package com.cn.alasga.common.core.util.wechat
  6  * @Description: 雪花算法生成不重復的序列號 可用作訂單編號
  7  * @author LiJing
  8  * @date 2018/12/6 13:46
  9  * @version v.3.0
 10  */
 11 
 12 import org.apache.curator.shaded.com.google.common.util.concurrent.ThreadFactoryBuilder;
 13 
 14 import java.util.concurrent.*;
 15 
 16 /**
 17  * Twitter_Snowflake<br>
 18  * SnowFlake的結構如下(每部分用-分開):<br>
 19  * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
 20  * 1位標識,由於long基本類型在Java中是帶符號的,最高位是符號位,正數是0,負數是1,所以id一般是正數,最高位是0<br>
 21  * 41位時間截(毫秒級),注意,41位時間截不是存儲當前時間的時間截,而是存儲時間截的差值(當前時間截 - 開始時間截)
 22  * 得到的值),這里的的開始時間截,一般是我們的id生成器開始使用的時間,由我們程序來指定的(如下下面程序IdWorker類的startTime屬性)。41位的時間截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
 23  * 10位的數據機器位,可以部署在1024個節點,包括5位datacenterId和5位workerId<br>
 24  * 12位序列,毫秒內的計數,12位的計數順序號支持每個節點每毫秒(同一機器,同一時間截)產生4096個ID序號<br>
 25  * 加起來剛好64位,為一個Long型。<br>
 26  * SnowFlake的優點是,整體上按照時間自增排序,並且整個分布式系統內不會產生ID碰撞(由數據中心ID和機器ID作區分),並且效率較高,經測試,SnowFlake每秒能夠產生26萬ID左右。
 27  */
 28 public class SnowflakeUtils {
 29 
 30     private static SnowflakeUtils idGenerater;
 31 
 32     static {
 33         idGenerater = new SnowflakeUtils(1, 2);
 34     }
 35 
 36     public synchronized static long getOrderNo() {
 37         return idGenerater.nextId();
 38     }
 39 
 40     // ==============================Fields===========================================
 41     /**
 42      * 開始時間截 (2015-01-01)
 43      */
 44     private final long twepoch = 1420041600000L;
 45 
 46     /**
 47      * 機器id所占的位數
 48      */
 49     private final long workerIdBits = 5L;
 50 
 51     /**
 52      * 數據標識id所占的位數
 53      */
 54     private final long datacenterIdBits = 5L;
 55 
 56     /**
 57      * 支持的最大機器id,結果是31 (這個移位算法可以很快的計算出幾位二進制數所能表示的最大十進制數)
 58      */
 59     private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
 60 
 61     /**
 62      * 支持的最大數據標識id,結果是31
 63      */
 64     private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
 65 
 66     /**
 67      * 序列在id中占的位數
 68      */
 69     private final long sequenceBits = 12L;
 70 
 71     /**
 72      * 機器ID向左移12位
 73      */
 74     private final long workerIdShift = sequenceBits;
 75 
 76     /**
 77      * 數據標識id向左移17位(12+5)
 78      */
 79     private final long datacenterIdShift = sequenceBits + workerIdBits;
 80 
 81     /**
 82      * 時間截向左移22位(5+5+12)
 83      */
 84     private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
 85 
 86     /**
 87      * 生成序列的掩碼,這里為4095 (0b111111111111=0xfff=4095)
 88      */
 89     private final long sequenceMask = -1L ^ (-1L << sequenceBits);
 90 
 91     /**
 92      * 工作機器ID(0~31)
 93      */
 94     private long workerId;
 95 
 96     /**
 97      * 數據中心ID(0~31)
 98      */
 99     private long datacenterId;
100 
101     /**
102      * 毫秒內序列(0~4095)
103      */
104     private long sequence = 0L;
105 
106     /**
107      * 上次生成ID的時間截
108      */
109     private long lastTimestamp = -1L;
110 
111     //==============================Constructors=====================================
112 
113     /**
114      * 構造函數
115      *
116      * @param workerId     工作ID (0~31)
117      * @param datacenterId 數據中心ID (0~31)
118      */
119     public SnowflakeUtils(long workerId, long datacenterId) {
120         if (workerId > maxWorkerId || workerId < 0) {
121             throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
122         }
123         if (datacenterId > maxDatacenterId || datacenterId < 0) {
124             throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
125         }
126         this.workerId = workerId;
127         this.datacenterId = datacenterId;
128     }
129 
130     // ==============================Methods==========================================
131 
132     /**
133      * 獲得下一個ID (該方法是可以加注 synchronized 線程安全)
134      *
135      * @return SnowflakeId
136      */
137     private long nextId() {
138         long timestamp = timeGen();
139 
140         //如果當前時間小於上一次ID生成的時間戳,說明系統時鍾回退過這個時候應當拋出異常
141         if (timestamp < lastTimestamp) {
142             throw new RuntimeException(
143                     String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
144         }
145 
146         //如果是同一時間生成的,則進行毫秒內序列
147         if (lastTimestamp == timestamp) {
148             sequence = (sequence + 1) & sequenceMask;
149             //毫秒內序列溢出
150             if (sequence == 0) {
151                 //阻塞到下一個毫秒,獲得新的時間戳
152                 timestamp = tilNextMillis(lastTimestamp);
153             }
154         }
155         //時間戳改變,毫秒內序列重置
156         else {
157             sequence = 0L;
158         }
159 
160         //上次生成ID的時間截
161         lastTimestamp = timestamp;
162 
163         //移位並通過或運算拼到一起組成64位的ID
164         return ((timestamp - twepoch) << timestampLeftShift)
165                 | (datacenterId << datacenterIdShift)
166                 | (workerId << workerIdShift)
167                 | sequence;
168     }
169 
170     /**
171      * 阻塞到下一個毫秒,直到獲得新的時間戳
172      *
173      * @param lastTimestamp 上次生成ID的時間截
174      * @return 當前時間戳
175      */
176     protected long tilNextMillis(long lastTimestamp) {
177         long timestamp = timeGen();
178         while (timestamp <= lastTimestamp) {
179             timestamp = timeGen();
180         }
181         return timestamp;
182     }
183 
184     /**
185      * 返回以毫秒為單位的當前時間
186      *
187      * @return 當前時間(毫秒)
188      */
189     protected long timeGen() {
190         return System.currentTimeMillis();
191     }
192 
193     //==============================Test=============================================
194 
195     /**
196      * 測試
197      */
198     public static void main(String[] args) throws Exception {
199         // 線程數量
200         final int threadCount = 100;
201         // 每個線程生成的 ID 數量
202         final int idCountPerThread = 1000;
203         // 用於等待所有線程啟動完成
204         CountDownLatch threadLatch = new CountDownLatch(threadCount);
205 
206         final int coreThread = 5;
207         final int maxThread = 50;
208         final long keepAliveTime = 0L;
209         final int queueCapacity = 1024;
210 
211 
212         ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
213 
214         //Common Thread Pool
215         ExecutorService pool = new ThreadPoolExecutor(coreThread, maxThread, keepAliveTime, TimeUnit.MILLISECONDS,
216                 new LinkedBlockingQueue<Runnable>(queueCapacity), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
217 
218 
219         ConcurrentSkipListSet<Long> ids = new ConcurrentSkipListSet<>();
220         for (int i = 0; i < threadCount; ++i) {
221             final int n = i;
222             pool.execute(() -> {
223                 // 等待所有線程都運行到這里,然后都繼續運行,差不多同時生成 id
224                 final String threadNum = Thread.currentThread().getName() + "-" + n + "號線程";
225                 try {
226                     threadLatch.await();
227                 } catch (InterruptedException e) {
228                     e.printStackTrace();
229                 }
230 //                System.out.println(threadNum+"繼續執行");
231                 for (int j = 0; j < idCountPerThread; ++j) {
232                     long id = SnowflakeUtils.getOrderNo();
233                     ids.add(id);
234                     System.out.println(id);
235                 }
236             });
237             threadLatch.countDown();
238         }
239         pool.shutdown();
240         // 等待 id 生成完成,生成不同數量的 id 時需要調整
241         Thread.sleep(2000);
242 //        System.out.println(ids.size());
243         //System.out.println(ids);
244     }
245 }

  

現在來解釋一下:

 

代碼中測試用到的 ConcurrentSkipListSet<Long> idListSet = new ConcurrentSkipListSet<>(); ConcurrentSkipListSet是線程安全的有序的集合,適用於高並發的場景。 ConcurrentSkipListSet和TreeSet,它們雖然都是有序的集合。 但是,第一,它們的線程安全機制不同,TreeSet是非線程安全的,而ConcurrentSkipListSet是線程安全的。 第二,ConcurrentSkipListSet是通過ConcurrentSkipListMap實現的,而TreeSet是通過TreeMap實現的。
說明:
(01) ConcurrentSkipListSet繼承於AbstractSet。因此,它本質上是一個集合。
(02) ConcurrentSkipListSet實現了NavigableSet接口。因此,ConcurrentSkipListSet是一個有序的集合。
(03) ConcurrentSkipListSet是通過ConcurrentSkipListMap實現的。它包含一個ConcurrentNavigableMap對象m,而m對象實際上是ConcurrentNavigableMap的實現類ConcurrentSkipListMap的實例。ConcurrentSkipListMap中的元素是key-value鍵值對;而ConcurrentSkipListSet是集合,它只用到了ConcurrentSkipListMap中的key!
(04) 里面不能有重復的數據,而且是有序的,還支持並發,很強大啊!!

但是::::

查看add()方法的javadoc,其注釋為:
如果此 set 中不包含指定元素,則添加指定元素。更確切地講,如果此 set 不包含滿足 e.equals(e2) 的元素 
e2,則向 set 中添加指定的元素 e。如果此 set 已經包含該元素,則調用不更改該 set 並返回 
false。



根據注釋,只要元素的equals()方法判斷不相等就能加入到Set中,可調試發現不是這么回事:

Java代碼  
1 public boolean add(E e) {
2     return m.putIfAbsent(e, Boolean.TRUE) == null;
3 }

m為 ConcurrentSkipListMap,它的putIfAbsent()方法實現是:

 
        
Java代碼  
  
 1     public V putIfAbsent(K key, V value) {
 2         if (value == null)
 3             throw new NullPointerException();
 4         return doPut(key, value, true);
 5     }
 6 
 7     private V doPut(K kkey, V value, boolean onlyIfAbsent) {
 8         Comparable super K&gt; key = comparable(kkey);
 9         for (;;) {
10             Node b = findPredecessor(key);
11             Node n = b.next;
12             for (;;) {
13                 if (n != null) {
14                     Node f = n.next;
15                     if (n != b.next)               // inconsistent read
16                         break;;
17                     Object v = n.value;
18                     if (v == null) {               // n is deleted
19                         n.helpDelete(b, f);
20                         break;
21                     }
22                     if (v == n || b.value == null) // b is deleted
23                         break;
24                     int c = key.compareTo(n.key);
25                     if (c &gt; 0) {
26                         b = n;
27                         n = f;
28                         continue;
29                     }
30                     if (c == 0) {
31                         if (onlyIfAbsent || n.casValue(v, value))
32                             return (V)v;
33                         else
34                             break; // restart if lost race to replace value
35                     }
36                     // else c  z = new Node(kkey, value, n);
37                 if (!b.casNext(n, z))
38                     break;         // restart if lost race to append to b
39                 int level = randomLevel();
40                 if (level &gt; 0)
41                     insertIndex(z, level);
42                 return null;
43             }
44         }
45     }

 

  實際上Set中的添加是根據元素的compareTo()方法在比較!如果compareTo()返回0,就認為2個元素相等,跟equals()方法根本沒有關系!JAVA SDK 的JavaDoc也不靠譜啊~~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM