前言
我们的数据库在设计时一般有两个ID,自增的id为主键,还有一个业务ID使用UUID生成。自增id在需要分表的情况下做为业务主键不太理想,所以我们增加了uuid作为业务ID,有了业务id仍然还存在自增id的原因具体我也说不清楚,只知道和插入的性能以及db的要求有关。
我个人一直想将这两个ID换成一个字段来处理,所以要求这个id是数字类似的,且是趋抛增长的,这样mysql创建索引以及查询时性能会比较好。于时网上找到了雪花算法.关于雪花算法大家可以看一下我后面引用的资料。
ID生成器代码:
从网上抄的,自己改的,目前我还没有应用到实际项目中,如需应用,请先进行严格自测
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
/**
* <p>
* 在雪花算法基础生稍做改造生成Long Id
* https://www.jianshu.com/p/d3881a6a895e
* </p>
* 1 - 41位 - 10位 - 12位
* 0 - 41位 - 10位 - 12位
* <p>
* <PRE>
* <BR> 修改记录
* <BR>-----------------------------------------------
* <BR> 修改日期 修改人 修改内容
* </PRE>
*
* @author cuiyh9
* @version 1.0
* @Date Created in 2018年11月29日 20:46
* @since 1.0
*/
public final class ZfIdGenerator {
/**
* 起始的时间戳
*/
private static final long START_TIME_MILLIS;
/**
* 每一部分占用的位数
*/
private final static long SEQUENCE_BIT = 12; //序列号占用的位数
private final static long WORKID_BIT = 10; //机器标识占用的位数
/**
* 每一部分的最大值
*/
private final static long MAX_WORK_NUM = -1L ^ (-1L << WORKID_BIT);
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
/**
* 每一部分向左的位移
*/
private final static long WORKID_SHIFT = SEQUENCE_BIT;
private final static long TIMESTMP_SHIFT = WORKID_SHIFT + WORKID_BIT;
private long sequence = 0L; //序列号
private long lastStmp = -1L;
/** workId */
private long workId;
static {
String startDate = "2018-01-01 00:00:00";
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.parse(startDate, df);
START_TIME_MILLIS = localDateTime.toInstant(ZoneOffset.of("+8")).toEpochMilli();
}
/**
* 获取分部署式发号器
* @param workId 每台服务需要传一个服务id
* @return
*/
public static synchronized ZfIdGenerator getDistributedIdGenerator(long workId) {
return new ZfIdGenerator(workId);
}
public static synchronized ZfIdGenerator getStandAloneIdGenerator() {
long workId = MAX_WORK_NUM;
return new ZfIdGenerator(workId);
}
private ZfIdGenerator(long workId) {
if (workId > MAX_WORK_NUM || workId <= 0) {
throw new RuntimeException("workdId的值设置错误");
}
this.workId = workId;
}
/**
* 生成id
* @return
*/
public synchronized long nextId() {
long currStmp = System.currentTimeMillis();
if (currStmp < START_TIME_MILLIS) {
throw new RuntimeException("机器时间存在问题,请注意查看");
}
if (currStmp == lastStmp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0L) {
currStmp = getNextMillis(currStmp);
}
} else {
sequence = 0L;
}
lastStmp = currStmp;
return ((currStmp - START_TIME_MILLIS) << TIMESTMP_SHIFT)
| (workId << WORKID_SHIFT)
| (sequence);
}
public long getNextMillis(long currStmp) {
long millis = System.currentTimeMillis();
while (millis <= currStmp) {
millis = System.currentTimeMillis();
}
return millis;
}
/**
* 获取最大的工作数量
* @return
*/
public static long getMaxWorkNum() {
return MAX_WORK_NUM;
}
public static void main(String[] args) {
ZfIdGenerator idGenerator1 = ZfIdGenerator.getDistributedIdGenerator(1);
// ZfIdGenerator idGenerator2 = ZfIdGenerator.getDistributedIdGenerator(2);
for (int i = 0; i < 1000000; i++) {
System.out.println(idGenerator1.nextId());
}
// System.out.println(idGenerator2.nextId());
}
}
分布式情况
上面的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项目):
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
/**
* <p>TODO</p>
* <p>
* <PRE>
* <BR> 修改记录
* <BR>-----------------------------------------------
* <BR> 修改日期 修改人 修改内容
* </PRE>
*
* @author cuiyh9
* @version 1.0
* @Date Created in 2018年11月30日 16:37
* @since 1.0
*/
@Slf4j
@SpringBootConfiguration
public class IdGeneratorConfig {
@Autowired
private ZkClient zkClient;
@Value("${idgenerator.zookeeper.parent.path}")
private String IDGENERATOR_PARENT_PATH;
@Bean
public ZfIdGenerator idGenerator() {
boolean flag = zkClient.createParent(IDGENERATOR_PARENT_PATH);
if (!flag) {
throw new RuntimeException("创建发号器父节点失败");
}
// 获取workId
long workId = 0;
long maxWorkNum = ZfIdGenerator.getMaxWorkNum();
for (long i = 1; i < maxWorkNum; i++) {
String workPath = IDGENERATOR_PARENT_PATH + "/" + i;
flag = zkClient.createNotExistEphemeralNode(workPath);
if (flag) {
workId = i;
break;
}
}
if (workId == 0) {
throw new RuntimeException("获取机器id失败");
}
log.warn("idGenerator workId:{}", workId);
return ZfIdGenerator.getDistributedIdGenerator(workId);
}
}
ZkClient代码(基于apache curator)
注意apache curator版本,我最初使用的是4.x版本,程序执行到forPath()方法就会阻塞,后来查到是与zookeeper版本不匹配导致.
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* <p>TODO</p>
* <p>
* <PRE>
* <BR> 修改记录
* <BR>-----------------------------------------------
* <BR> 修改日期 修改人 修改内容
* </PRE>
*
* @author cuiyh9
* @version 1.0
* @Date Created in 2018年11月30日 16:36
* @since 1.0
*/
@Slf4j
@Component
public class ZkClient {
@Autowired
private CuratorFramework client;
/**
* 创建父节点,创建成功或存在都返回成功
* @param path
* @return
*/
public boolean createParent(String path) {
try {
client.create().creatingParentsIfNeeded().forPath(path);
return true;
} catch (KeeperException.NodeExistsException e) {
return true;
} catch (Exception e) {
log.error("createParent fail path:{}", path, e);
}
return false;
}
/**
* 创建不存在的节点。如果存在或创建失败,返回false
* @param path
* @throws Exception
*/
public boolean createNotExistEphemeralNode(String path) {
try {
client.create().withMode(CreateMode.EPHEMERAL).forPath(path);
return true;
} catch (KeeperException.NodeExistsException e) {
return false;
} catch (Exception e) {
log.error("createNotExistNode fail path:{}", path, e);
}
return false;
}
}
参考资料:
1、https://blog.csdn.net/x5fnncxzq4/article/details/79549514
2、https://segmentfault.com/a/1190000011282426#articleHeader2
3、https://www.cnblogs.com/relucent/p/4955340.html
4、https://blog.csdn.net/boshuzhang/article/details/72677454
5、https://www.jianshu.com/p/d3881a6a895e
6、https://blog.csdn.net/u012557538/article/details/53318436
7、https://segmentfault.com/a/1190000011282426#articleHeader2