雪花算法生成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 }