雪花算法生成ID

时间:2022-12-22 00:12:15

前言
我们的数据库在设计时一般有两个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 }