雪花算法生成ID


前言
我們的數據庫在設計時一般有兩個ID,自增的id為主鍵,還有一個業務ID使用UUID生成。自增id在需要分表的情況下做為業務主鍵不太理想,所以我們增加了uuid作為業務ID,有了業務id仍然還存在自增id的原因具體我也說不清楚,只知道和插入的性能以及db的要求有關。

我個人一直想將這兩個ID換成一個字段來處理,所以要求這個id是數字類似的,且是趨拋增長的,這樣mysql創建索引以及查詢時性能會比較好。於時網上找到了雪花算法.關於雪花算法大家可以看一下我后面引用的資料。

ID生成器代碼:
從網上抄的,自己改的,目前我還沒有應用到實際項目中,如需應用,請先進行嚴格自測

  1  
  2 import java.time.LocalDateTime;
  3 import java.time.ZoneOffset;
  4 import java.time.format.DateTimeFormatter;
  5  
  6 /**
  7  * <p>
  8  *  在雪花算法基礎生稍做改造生成Long Id
  9  *  https://www.jianshu.com/p/d3881a6a895e
 10  * </p>
 11  * 1 - 41位 - 10位 - 12位
 12  * 0 - 41位 - 10位 - 12位
 13  * <p>
 14  * <PRE>
 15  * <BR>    修改記錄
 16  * <BR>-----------------------------------------------
 17  * <BR>    修改日期         修改人          修改內容
 18  * </PRE>
 19  *
 20  * @author cuiyh9
 21  * @version 1.0
 22  * @Date Created in 2018年11月29日 20:46
 23  * @since 1.0
 24  */
 25 public final class ZfIdGenerator {
 26  
 27     /**
 28      * 起始的時間戳
 29      */
 30     private static final long START_TIME_MILLIS;
 31  
 32     /**
 33      * 每一部分占用的位數
 34      */
 35     private final static long SEQUENCE_BIT = 12; //序列號占用的位數
 36     private final static long WORKID_BIT = 10;   //機器標識占用的位數
 37  
 38     /**
 39      * 每一部分的最大值
 40      */
 41     private final static long MAX_WORK_NUM = -1L ^ (-1L << WORKID_BIT);
 42     private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
 43  
 44     /**
 45      * 每一部分向左的位移
 46      */
 47     private final static long WORKID_SHIFT = SEQUENCE_BIT;
 48     private final static long TIMESTMP_SHIFT = WORKID_SHIFT + WORKID_BIT;
 49  
 50     private long sequence = 0L; //序列號
 51     private long lastStmp = -1L;
 52  
 53     /** workId */
 54     private long workId;
 55  
 56     static {
 57         String startDate = "2018-01-01 00:00:00";
 58         DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
 59         LocalDateTime localDateTime = LocalDateTime.parse(startDate, df);
 60         START_TIME_MILLIS = localDateTime.toInstant(ZoneOffset.of("+8")).toEpochMilli();
 61  
 62     }
 63  
 64  
 65  
 66  
 67     /**
 68      * 獲取分部署式發號器
 69      * @param workId 每台服務需要傳一個服務id
 70      * @return
 71      */
 72     public static synchronized ZfIdGenerator getDistributedIdGenerator(long workId) {
 73         return new ZfIdGenerator(workId);
 74     }
 75  
 76     public static synchronized ZfIdGenerator getStandAloneIdGenerator() {
 77         long workId = MAX_WORK_NUM;
 78         return new ZfIdGenerator(workId);
 79     }
 80  
 81  
 82     private ZfIdGenerator(long workId) {
 83         if (workId > MAX_WORK_NUM || workId <= 0) {
 84             throw  new RuntimeException("workdId的值設置錯誤");
 85         }
 86         this.workId = workId;
 87     }
 88  
 89     /**
 90      * 生成id
 91      * @return
 92      */
 93     public synchronized long nextId() {
 94         long currStmp = System.currentTimeMillis();
 95         if (currStmp < START_TIME_MILLIS) {
 96             throw new RuntimeException("機器時間存在問題,請注意查看");
 97         }
 98  
 99         if (currStmp == lastStmp) {
100             sequence =  (sequence + 1) & MAX_SEQUENCE;
101             if (sequence == 0L) {
102                 currStmp = getNextMillis(currStmp);
103             }
104         } else {
105             sequence = 0L;
106         }
107         lastStmp = currStmp;
108  
109         return ((currStmp - START_TIME_MILLIS) << TIMESTMP_SHIFT)
110                 | (workId << WORKID_SHIFT)
111                 | (sequence);
112     }
113  
114     public long getNextMillis(long currStmp) {
115         long millis = System.currentTimeMillis();
116         while (millis <= currStmp) {
117             millis = System.currentTimeMillis();
118         }
119         return millis;
120     }
121  
122     /**
123      * 獲取最大的工作數量
124      * @return
125      */
126     public static long getMaxWorkNum() {
127         return MAX_WORK_NUM;
128     }
129  
130     public static void main(String[] args) {
131         ZfIdGenerator idGenerator1 = ZfIdGenerator.getDistributedIdGenerator(1);
132 //        ZfIdGenerator idGenerator2 = ZfIdGenerator.getDistributedIdGenerator(2);
133         for (int i = 0; i < 1000000; i++) {
134             System.out.println(idGenerator1.nextId());
135         }
136  
137 //        System.out.println(idGenerator2.nextId());
138  
139  
140  
141     }
142  
143 }

 

分布式情況
上面的ID生成器在單機情況下使用沒有問題,但如果在分布下使用,就需要分配不同的workId,如果workId相同,可能會導致生成的id相同。

解決方案:

1、使用java環境變量,人為通過-D預先設置workid.這種方案簡單,不會出現重復情況,但需要每個服務的啟動腳本不同.

2、使用sharding-jdbc中的算法,使用IP后幾位來做workId,這種方案也很簡單,不需要修改服務的啟動腳本,但在某些情況下會出現生成重復ID的情況,詳細見我下面的參考資料

3、使用zk,在啟動時給每個服務分配不同的workId,缺點:多了依賴,需要zk,優點:不會出現重復情況,且不需要修改服務的啟動腳本。這個是我個人使用的方案,實現思路為,系統啟動時創建一個永久性的結點(zookeeper保證原子性),然后在這個永久性的節點下,遍歷workId去zookeeper創建臨時結點,zookeeper會保證相同路徑只會有一個可能創建成功,如果創建失敗繼續遍歷即可。詳細可看一下代碼

實例化ID生成器如下(Spring boot項目):

 1  
 2 import lombok.extern.slf4j.Slf4j;
 3 import org.springframework.beans.factory.annotation.Autowired;
 4 import org.springframework.beans.factory.annotation.Value;
 5 import org.springframework.boot.SpringBootConfiguration;
 6 import org.springframework.context.annotation.Bean;
 7  
 8 /**
 9  * <p>TODO</p>
10  * <p>
11  * <PRE>
12  * <BR>    修改記錄
13  * <BR>-----------------------------------------------
14  * <BR>    修改日期         修改人          修改內容
15  * </PRE>
16  *
17  * @author cuiyh9
18  * @version 1.0
19  * @Date Created in 2018年11月30日 16:37
20  * @since 1.0
21  */
22 @Slf4j
23 @SpringBootConfiguration
24 public class IdGeneratorConfig {
25  
26     @Autowired
27     private ZkClient zkClient;
28  
29     @Value("${idgenerator.zookeeper.parent.path}")
30     private String IDGENERATOR_PARENT_PATH;
31  
32     @Bean
33     public ZfIdGenerator idGenerator() {
34         boolean flag = zkClient.createParent(IDGENERATOR_PARENT_PATH);
35         if (!flag) {
36             throw new RuntimeException("創建發號器父節點失敗");
37         }
38  
39         // 獲取workId
40         long workId = 0;
41         long maxWorkNum = ZfIdGenerator.getMaxWorkNum();
42         for (long i = 1; i < maxWorkNum; i++) {
43             String workPath = IDGENERATOR_PARENT_PATH + "/" + i;
44             flag = zkClient.createNotExistEphemeralNode(workPath);
45             if (flag) {
46                 workId =  i;
47                 break;
48             }
49         }
50  
51         if (workId == 0) {
52             throw new RuntimeException("獲取機器id失敗");
53         }
54         log.warn("idGenerator workId:{}", workId);
55         return ZfIdGenerator.getDistributedIdGenerator(workId);
56  
57     }
58 }

ZkClient代碼(基於apache curator)

注意apache curator版本,我最初使用的是4.x版本,程序執行到forPath()方法就會阻塞,后來查到是與zookeeper版本不匹配導致.

 1  
 2 import lombok.extern.slf4j.Slf4j;
 3 import org.apache.curator.framework.CuratorFramework;
 4 import org.apache.zookeeper.CreateMode;
 5 import org.apache.zookeeper.KeeperException;
 6 import org.springframework.beans.factory.annotation.Autowired;
 7 import org.springframework.stereotype.Component;
 8  
 9 /**
10  * <p>TODO</p>
11  * <p>
12  * <PRE>
13  * <BR>    修改記錄
14  * <BR>-----------------------------------------------
15  * <BR>    修改日期         修改人          修改內容
16  * </PRE>
17  *
18  * @author cuiyh9
19  * @version 1.0
20  * @Date Created in 2018年11月30日 16:36
21  * @since 1.0
22  */
23 @Slf4j
24 @Component
25 public class ZkClient {
26  
27     @Autowired
28     private CuratorFramework client;
29  
30     /**
31      * 創建父節點,創建成功或存在都返回成功
32      * @param path
33      * @return
34      */
35     public boolean createParent(String path) {
36         try {
37             client.create().creatingParentsIfNeeded().forPath(path);
38             return true;
39         } catch (KeeperException.NodeExistsException e) {
40             return true;
41         } catch (Exception e) {
42             log.error("createParent fail path:{}", path, e);
43         }
44         return false;
45     }
46  
47     /**
48      * 創建不存在的節點。如果存在或創建失敗,返回false
49      * @param path
50      * @throws Exception
51      */
52     public boolean createNotExistEphemeralNode(String path) {
53         try {
54             client.create().withMode(CreateMode.EPHEMERAL).forPath(path);
55             return true;
56         } catch (KeeperException.NodeExistsException e) {
57             return false;
58         }  catch (Exception e) {
59             log.error("createNotExistNode fail path:{}", path, e);
60         }
61         return false;
62     }
63 }

 


免責聲明!

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



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