
com.luoshu.open.id.JdbcIdGenerate Maven / Gradle / Ivy
package com.luoshu.open.id;
import com.luoshu.open.id.exception.IdException;
import com.luoshu.open.id.ui.model.RangeInfo;
import lombok.extern.slf4j.Slf4j;
import javax.sql.DataSource;
import java.math.BigDecimal;
import java.text.MessageFormat;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
@Slf4j
public class JdbcIdGenerate implements IdGenerate{
/**
* 这个是 js 的最大值 ,超过这个,意味着 js 会产生异常,数字会溢出
*/
private static final long SYSTEM_MAX_ID = 9007199254740991L;
/**
* 当到达了这个ID时,会触发警告,目的也是为了提前整改,避免在 js 前端溢出
*
* 提前两万亿触发报警,留有足够的时间处理
*/
private static final long WARN_ID = 9005199254740991L;
private String category;
private DataSource dataSource;
private JdbcIdDao jdbcIdDao;
private JdbcIdConfig config;
/**
* 查询 db 的间隔时间。单位秒
*
* 主要用于在自动设置步长的情况,会自动将频率调整成为每XX秒,查询一次数据库
*/
private final int queryDbIntervalS = 60;
/**
* 最大缓存数量。 是指从数据库取一个步长,然后取出的范围,从内存中进行分配,这里是可以分配到的最大值,是 大于等于
* 如果当前分配值,达到了当前值,那说明缓存中的号段全部用光,需要重新从数据库中获取一个号段
*/
private Long maxCacheNum;
/**
* 当前已分配到的值, 不能大于 maxCacheNum
*/
private AtomicLong currentReadNum;
/**
* 数据库中设置的当前序列的最大值,如果数据库未配置,那么它会等于系统最大值
*/
private long maxNum = SYSTEM_MAX_ID;
/**
* 上一次从 db 中读取序列值的时间
*/
private Long lastReadDbTime;
/**
* 当前的序列值
*/
private int currentStep;
public JdbcIdGenerate(String category, DataSource dataSource , JdbcIdConfig config) {
this.category = category;
this.dataSource = dataSource;
this.config = config;
this.jdbcIdDao = new JdbcIdDao(dataSource);
}
/**
* 开发者提醒:这里曾经遇到过一个问题需要注意。就是当同一个数据源时,在事务中调用这个方法,会导致线程池的锁,与这个方法的同步锁,产生死锁。
* 目前没有该问题
* @return
*/
@Override
public synchronized long next() {
if(maxCacheNum == null || currentReadNum == null || currentReadNum.get() > maxCacheNum){
// 需要再取一个范围到缓存中进行分配
log.info("luoshu-id load next step range for category : " + category);
RangeInfo rangeInfo = readNextRange();
if(rangeInfo == null){
throw new IdException("luoshu-id load table setp to memory error , category : " + category);
}
// 将更新的区间,刷新到内存中
flushMemory(rangeInfo);
}
if(maxCacheNum == null || currentReadNum == null || currentReadNum.get() > maxCacheNum){
String msg = MessageFormat.format("next id error , currentReadNum : {0} , maxCacheNum : {1}",
currentReadNum, maxCacheNum);
throw new IdException(msg);
}
long value = currentReadNum.getAndIncrement();
if(value > WARN_ID){
// 是否达到报警值
log.warn("luoshu-id category : " + category + " is out of : " + WARN_ID + " , " +
"please check you system and make a solution , the system max id is : " + SYSTEM_MAX_ID);
}
if(value > SYSTEM_MAX_ID){
// 是否达到系统最大值 , 这里如果溢出,会导致前端 js 的数值丢失精度,是不被允许的
throw new IdException("luoshu-id category : " + category + " is out of system max id : " + SYSTEM_MAX_ID + " . it will make javascript number error");
}
if(value > this.maxNum){
// 达到在数据库预设的最大值,抛出异常
throw new IdException("luoshu-id category : " + category + " is out of " + this.maxNum + " on database setting , please check you database");
}
return value;
}
/**
* 获取下一个区间
* @return 如果为 不为空 , 则代表拉取成功
*/
protected RangeInfo readNextRange(){
return readNextRange(1 , -1);
}
/**
* 获取下一个可用的区间。并且会更新数据库中的区间值
* @param step 指定的步长
* @return
*/
public RangeInfo readRangeByFixStep(int step){
if(step <= 0){
throw new IllegalArgumentException("step must > 0");
}
return readNextRange(1 , step);
}
/**
* 获取下一个区间
* @param retryTime 当前是第几次重试
* @param step 步长,也就是读取这个区间的长度,如果为-1,则会自动获取步长
* @return 如果为 不为空 , 则代表拉取成功
*/
protected RangeInfo readNextRange(int retryTime , int step){
if(retryTime > config.getRetryTime()){
log.error("luoshu-id update num of category : " + category + " is over max retry time : " + config.getRetryTime());
return null;
}
// 从数据库获取该实体,如果获取不到,则会创建一个新的
IdPO idPO = getOrCreate();
// 进入到这里 , 数据库一定要有数据了
verify(idPO);
// 如果外部系统没有传一个可用的步长,则自动获取
if(step <= 0){
// 向数据库中写入获取的步长
step = getStep();
}
// 需要标记在缓存中的区间最大值
long markerIndex = idPO.getNum() + step;
// 如果 r 为 false , 则代从数据库获取一个步长的区间失败
boolean r = jdbcIdDao.updateSquenceNum(category, markerIndex + 1, idPO.getVersion(), randVersion());
if(!r){
int newRetryTime = retryTime + 1;
log.info("luoshu-id update num of category : " + category + " fail , retry time : " + newRetryTime);
// 再次尝试
return readNextRange(newRetryTime , step);
}else{
RangeInfo rangeInfo = new RangeInfo();
rangeInfo.setCategory(this.category);
rangeInfo.setRangeStartNum(idPO.getNum());
rangeInfo.setRangeEndNum(markerIndex);
rangeInfo.setIdLimitMaxNum(idPO.getMaxNum());
return rangeInfo;
}
}
/**
* 更新本地的缓存信息
* @param rangeInfo
*/
protected synchronized void flushMemory(RangeInfo rangeInfo){
// 更新内存中的当前分配id , 与步长可更新的最大值
if(this.currentReadNum == null){
this.currentReadNum = new AtomicLong();
}
this.currentReadNum.set(rangeInfo.getRangeStartNum());
// 更新缓存区间的最大值
this.maxCacheNum = rangeInfo.getRangeEndNum();
// 设置序列最大值,这个最大值是为了防止数字越界
if(rangeInfo.getIdLimitMaxNum() != null){
this.maxNum = rangeInfo.getIdLimitMaxNum();
}
log.info("luoshu-id category : " + category + " load range from database success , start : " + this.currentReadNum.get() + " " +
", step to : " + this.maxCacheNum + " , end : " + maxNum);
}
/**
* 获取或者创建一个数据库实体
* 首先会从数据库中查询,如果查询不到,则会插入一个
* @return
*/
protected synchronized IdPO getOrCreate(){
IdPO idPO = jdbcIdDao.findByCategory(category);
if(idPO == null){
// 数据库没有, 插入一条
idPO = new IdPO();
idPO.setNum(1L);
idPO.setVersion(randVersion());
idPO.setCategory(category);
try {
jdbcIdDao.insertCategory(idPO);
} catch (Exception e) {
// 插入失败了,有可能是并发情况,数据库中已经有一条数据了
log.error(e.getMessage() , e);
// 这个时候重新查一下数据库 ,如果还是为空,则证明是数据库有问题
idPO = jdbcIdDao.findByCategory(category);
}
}
return idPO;
}
/**
* 进入到这里 , 数据库一定要有数据了
* 用于验证该数据是否合法
* @param idPO
*/
protected void verify(IdPO idPO){
if(idPO == null){
throw new IdException("category is empty in database : " + category);
}
if(idPO.getNum() == null){
throw new IdException("num of category is null : " + category);
}
if(idPO.getVersion() == null || "".equals(idPO.getVersion().trim())){
throw new IdException("version of category is null : " + category);
}
}
/**
* 获取向数据库中拉取的步长。
* 控制在一分钟查询一次数据库
* @return
*/
protected int getStep(){
if(this.config.getStep() < -1 || this.config.getStep() == 0){
throw new IdException("config step error : " + config.getStep());
}
if(this.config.getStep() > 0){
// 说明程序设定了步长,可以直接返回
return this.config.getStep();
}
// 智能设置步长
if(this.lastReadDbTime == null){
// 需要初始化一个默认值
this.currentStep = 2;
this.lastReadDbTime = System.currentTimeMillis();
return this.currentStep;
}else{
long now = System.currentTimeMillis();
int step = -1;
if(now - this.lastReadDbTime == 0){
// 如果上一次读数据库的时间,与这次读取的时间相差不到1毫秒,那说明并发非常高,先初始化500条
step = 500;
}else if((now - this.lastReadDbTime) > (queryDbIntervalS * 1000L)){
// 如果大于指定的间隔时间,那么说明并发量不高,设置步长为1
step = 1;
}else{
// 按照公式计算
BigDecimal s = new BigDecimal((now - lastReadDbTime)).divide(new BigDecimal(1000), 3, BigDecimal.ROUND_HALF_UP);
double m = new BigDecimal(this.queryDbIntervalS).divide(s, 2, BigDecimal.ROUND_HALF_UP).doubleValue();
step = (int) (this.currentStep * m);
if(step <= 0){
step = 1;
}else if(step > 1000){
// 避免快速消耗
step = 1000;
}
}
log.info("luoshu-id category : " + category + " update step . " + this.currentStep + " -> " + step);
this.currentStep = step;
this.lastReadDbTime = now;
return this.currentStep;
}
}
/**
* 随机生成版本号,其实就是一串 uuid , 之所以不用时间,是担心并发情况重复
* @return
*/
protected String randVersion(){
String str = UUID.randomUUID().toString().replace("-", "");
return str.substring(0 , 18);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy