前言
我們的數據庫在設計時一般有兩個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 }
