背景
随着互联网的高速发展,企业系统采集的数据呈指数式上升,集成的业务也越来越多。于是,以分布式微服务为基础的项目架构逐渐走进人们的视野,在分布式项目中,对大量的数据、消息、http请求等要有唯一标识这时,传统的数据库自增主键已经不能满我们的需求。
- 全局唯一:不能出现重复ID。
- 高可用:ID生成系统是基础系统,被许多关键系统调用,一旦宕机,会造成严重影响。
以上两个特性,是分布式ID最基本的要求。
基于数据库的分布式ID
除了具备全局唯一性、高可用的特点外,还具备轻量级,不依赖第三方;高性能,与内存结合使用,减少了与数据库的交互;ID规则灵活,可扩展。
核心代码:
public class EntityIdHandler {
private final ReentrantLock lock = new ReentrantLock();
private static final int RETRY_COUNT = 5;
/**最后生成的id*/
private String lastGeneratedId = "";
/**主键,单据类型*/
private String idCode = "";
/**固定前缀*/
private String fixPrefix = "";
/**是否包含业务前缀*/
private boolean includeBizPrefix = false;
/**日期前缀*/
private String datePrefix = "";
/**日期格式*/
private String datePattern = "";
/**数字部分位数*/
private int numDigit = 0;
/**最大键值*/
private long maxValue = 0;
/**下一个键值*/
private long nextValule = 0;
private EntityIdConfService entityIdConfService;
public EntityIdHandler(String idCode, EntityIdConfService entityIdConfService) {
this.idCode = idCode.trim().toLowerCase();
this.entityIdConfService = entityIdConfService;
retrieveFromDB(RETRY_COUNT);
}
/**
* 更新数据库,刷新内存池。数据库中要记录两个值:nextValue,datePrefix,其中datePrefix只是表示当前的,并不是与nextValue对应,nextValue是未来使用值。
* 数据库要记录最后一次被更新时对应的日期前缀,以便当服务崩溃重启时,用它来和当前日期比较,判断数据库的nextValue是否回归poolSize+1。
* 如果是日期前缀变更,更新数据库nextValue为pooSize+1。数据库中管理的只是当前日期前缀段内的递增数量;
* 每次服务重启,要查询数据库中的日期前缀是否已过时,如果已过时,则准备更新数据库日期为当前。
* 如果是第一次初始化即系统第一次投入使用,置数据库NextBatchStartValue值为1,如果不是第一次启动,置数据库nextBatchStartValue=NextBatchStartValue+poolSize,
* 因此也有可能最多浪费一个poolsize的id,作为内存未和数据库及时通信的损失。
*/
private void retrieveFromDB(int retryCount) {
boolean success = false;
if (retryCount == 0) {return;}
try {
EntityIdConf entityIdConf = entityIdConfService.selectById(idCode);
String tempDatePrefix = getTempDatePrefix(entityIdConf.getDatePattern());
//如果日期前缀发生变更
if (!tempDatePrefix.trim().equalsIgnoreCase("") &&
!tempDatePrefix.equalsIgnoreCase(entityIdConf.getDatePrefix())) {
entityIdConf.setDatePrefix(tempDatePrefix);
entityIdConf.setNextBatchStartValue(entityIdConf.getPoolSize() + 1L);
} else {
//如果日期前缀没有变化,则增加步长,(在服务重启时这将损失id资源)
entityIdConf.setNextBatchStartValue(entityIdConf.getNextBatchStartValue() + entityIdConf.getPoolSize());
}
entityIdConf.setLastGeneratedId(this.lastGeneratedId);
//先更新数据库,再更新内存。
if (entityIdConfService.updateById(entityIdConf)) {
updateMemoryFieldFromDB(entityIdConf);
}
success = true;
} catch (Exception e) {
e.printStackTrace();
}
if (!success) {
// Call this method again, but sleep briefly to try to avoid thread contention.
try {
Thread.sleep(75);
} catch (InterruptedException ie) {
ie.printStackTrace();
}
retrieveFromDB(retryCount - 1);
}
}
/**
* 根据数据库更新内存值
*/
private void updateMemoryFieldFromDB(EntityIdConf entityIdConf) {
if (entityIdConf != null) {
this.fixPrefix = entityIdConf.getFixPrefix() == null ? "".trim() : entityIdConf.getFixPrefix().trim();
this.numDigit = entityIdConf.getNumDigit();
this.includeBizPrefix = entityIdConf.getIncludeBizPrefix() == null ? Boolean.FALSE : entityIdConf.getIncludeBizPrefix();
this.datePattern = entityIdConf.getDatePattern() == null ? "".trim() : entityIdConf.getDatePattern().trim();
this.datePrefix = entityIdConf.getDatePrefix() == null ? "" : entityIdConf.getDatePrefix().trim();
this.maxValue = entityIdConf.getNextBatchStartValue() - 1L;
this.nextValule = entityIdConf.getNextBatchStartValue() - entityIdConf.getPoolSize();
}
}
/**
* 获取单个ID
* @return
*/
public String getNextEntityId(String bizCode) {
lock.lock();
try {
String stringValue = getNextStringValue();
String nextEntityId = this.fixPrefix.trim()
+ (this.includeBizPrefix ? bizCode : "")
+ this.datePrefix.trim()
+ stringValue;
this.lastGeneratedId = nextEntityId;
return nextEntityId;
} finally {
lock.unlock();
}
}
/**
* 批量获取ID
* @return
*/
public List getNextEntityIds(String bizCode, Integer reqCount) {
lock.lock();
try {
List entityIds = new ArrayList<>();
for (int i = 0; i < reqCount; i++) {
entityIds.add(getNextEntityId(bizCode));
}
return entityIds;
} finally {
lock.unlock();
}
}
public String getNextStringValue() {
//如果日期前缀变化
String tempDatePrefix = getTempDatePrefix(this.datePattern);
if (!tempDatePrefix.trim().equalsIgnoreCase("") &&
!tempDatePrefix.equalsIgnoreCase(this.datePrefix)) {
retrieveFromDB(RETRY_COUNT);
}
//如果内存id资源耗光,需要同步数据库
if (nextValule > maxValue) {
retrieveFromDB(RETRY_COUNT);
}
//内存id正常增长
if (nextValule <= maxValue) {
nextValule += nextValule;
} else {
nextValule = -1L;
}
String nextVal = Long.toString(nextValule);
return StringUtils.leftPad(nextVal, this.numDigit, "0");
}
private String getTempDatePrefix(String datePattern) {
if (datePattern.trim().equalsIgnoreCase("")) {return "";}
LocalDate localDate = LocalDate.now();
return localDate.format(DateTimeFormatter.ofPattern(datePattern));
}
}
以上是实体类ID生成器的主要核心,原理是:开始获取下一个ID -> 判断日期是否变化 -> 是,将内存的下一个值(nextBatchStartValue)置为1,重新开始计数,更新内存,更新数据库 -> 否,判断下一个值是否超出内存最大键值 -> 是,更新nextBatchStartValue+=poolSize,同步数据库 -> 否,正常增长。
以下是测试单机qps和多线程环境下ID唯一性的测试代码: