All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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