自定义全局自增ID生成器

看了网上很多生成自增ID的策略,最终给出的都是雪花算法,leaf算法。但是却没有满足咱们对于自定义生成规则的需求。

在业务上有一部分ID往往是有规则的,比如某个产品的订单号往往是“产品标志+时间+n位流水”,类似这样的订单规则,使用雪花算法是满足不了业务需求的,所以我们得设计一套自己的自定义ID生成器。

“产品标志+时间+n位流水”规则中,难点无非在于n位流水号的生成,因为这个流水号需要保证在多次请求中不会产生重复的订单号。

首先,咱们根据业务需求,先制定对应的规则表达式:

id:
  generator:
    expressions:
        # 产品标志
      TEST:
        # 表达式,pid指的是产品标志
        exp: "$(pid)$(yearMonthDayHms)$(id:6:0)"
        # 字段名:初始值:最大值:最小数量:扩容数量:初始数量:增长步长
        initFields: ["id"]

表达式对应的实体类:

@Data
@Configuration
@ConfigurationProperties(prefix = "id.generator")
public class IDExpressionProperties {

    private Map<String, SerialIdConfig> expressions;

    @Data
    public static class SerialIdConfig {
        // 表达式
        private String exp;
        // 初始化字段
        private String[] initFields;
    }
}

通过spring解析获得对应的IDExpressionProperties实体类,拿到咱们自定义的配置

通过规则表达式可以看出,类似“$(pid)”这样的表达式,咱们可以抽象成接口自定义生成。比如咱们定义一个VariableGenerator接口或者抽象类,遇到pid就从spring ioc容器中调用pidVariableGenerator这个Bean的生成方法获取pid的值。遇到yearMonthDayHms就调用yearMonthDayHmsVariableGenerator这个Bean的生成方法获取yearMonthDayHms指定的值。

@Configuration
public class SerialConfig {

    @Autowired private IDExpressionProperties idExpressionProperties;
        /**
        ** 咱们要创建一个ID工厂类,专门用来生成ID的类
    ** 使用方法:
    ** @Autowired
    ** private IDFactory idFactory;
    ** String id = idFactory.get("产品标志");
    */
    @Bean(initMethod = "init")
    public IDFactory serialIdFactory() {
        return new IDFactory(idExpressionProperties.getExpressions());
    }
}

变量生成器

import org.apache.commons.lang3.StringUtils;

/**
 * 变量生成器
 * @className VariableGenerator
 * @date: 2021/2/18 下午2:53
 * @description:
 */
public abstract class VariableGenerator {

    public static final String COLON = ":";
   /**
        * apply是生成目标字符串的方法
        */
    protected abstract String apply(ExpressionElement e, Expression expression);

   /**
        * apply的后置处理方法,默认处理字符串不足的情况下,补足对应的填充数据
        */
    public String andThen(ExpressionElement e, Expression expression) {
        String variableValue = apply(e, expression);
        int count = e.getCount();
        String fillStringValue = e.getFillStringValue();
        if (StringUtils.isNotBlank(variableValue)) {
            if (count > 0) {
                variableValue = StringUtils.leftPad(variableValue, count, fillStringValue);
            } else {
                variableValue =
                        StringUtils.rightPad(variableValue, Math.abs(count), fillStringValue);
            }
        }
        return variableValue;
    }
}

ID生成器

  • 初始化的时候根据表达式配置确定是哪些字段需要初始化,根据初始化字段调用指定的Bean执行初始化
  • 通过get方法传入的参数key获取指定的规则表达式,根据指定的表达式调用对应的生成器Bean实例,调用指定的方法生成目标值,最后拼接出最终的ID
public class IDFactory {
    // 变量生成器
    @Autowired(required = false)
    private Map<String, VariableGenerator> variableGeneratorMap;
        // 字段初始化生成器
    @Autowired(required = false)
    private Map<String, InitFieldGenerator> initFieldGeneratorMap;
        // 构造函数,参数是生成规则
    public IDFactory(Map<String, IDExpressionProperties.SerialIdConfig> expressionMap) {
        this.expressionMap = expressionMap;
    }
        // 实例化后执行
    public void init() {
        // 如果没有规则表达式,那么直接就结束
        if (CollectionUtils.isEmpty(this.expressionMap)) {
            return;
        }
        for (Map.Entry<String, IDExpressionProperties.SerialIdConfig> e :
                this.expressionMap.entrySet()) {
            String key = e.getKey();
            // 规则表达式
            IDExpressionProperties.SerialIdConfig config = e.getValue();
            // 初始化字段参数
            String[] initFields = config.getInitFields();
            // 如果没有初始化字段生成器,直接结束
            if (CollectionUtils.isEmpty(initFieldGeneratorMap)) {
                return;
            }
            // 根据初始化规则,执行初始化操作
            for (String initField : initFields) {
                String fieldName = initField;
                // 获取初始化字段名称
                if (StringUtils.contains(initField, VariableGenerator.COLON)) {
                    fieldName = StringUtils.substringBefore(initField, VariableGenerator.COLON);
                }
                // 根据字段名称获取对应的初始化生成器的Bean实例
                InitFieldGenerator initFieldGenerator =
                        initFieldGeneratorMap.get(
                                fieldName + InitFieldGenerator.INIT_FIELD_GENERATOR);
                if (Objects.nonNull(initFieldGenerator)) {
                    // 执行字段初始化操作
                    initFieldGenerator.generator(key, initField);
                }
            }
        }
    }

    /**
     * 表达式
     *
     * <p>pid:expression格式
     */
    private Map<String, IDExpressionProperties.SerialIdConfig> expressionMap;
    /**
     * 根据指定的key规则生成id
     *
     * @param key
     * @return
     */
    public String get(String key) {
        // key为空直接抛异常
        if (StringUtils.isBlank(key)) {
            throw new IllegalArgumentException("无效的参数值:" + key);
        }
        // 获取规则表达式
        IDExpressionProperties.SerialIdConfig serialIdConfig = expressionMap.get(key);
        // 表达式字符串
        String expressionString = serialIdConfig.getExp();
        // 为空直接抛异常
        if (StringUtils.isBlank(expressionString)) {
            throw new IllegalArgumentException("没有找到对应的表达式");
        }
        // 解析指定的表达式
        Expression expression = parse(key, expressionString);
        // 匹配得出最终结果
        return matchExpression(expression);
    }
        // 生成器名称后缀
    private static final String VARIABLE_GENERATOR = "VariableGenerator";
  
        // 循环遍历表达式中所有的自定义变量,获取指定Bean实例,执行目标方法后得出最终ID
    private String matchExpression(Expression expression) {
        // 获取变量列表,例如pid,yearMonthDayHms等
        List<ExpressionElement> elements = expression.getElements();
        // 如果没有任何变量,那么直接返回原表达式,说明表达式是一个常量
        if (CollectionUtils.isEmpty(elements)) {
            return expression.getExpression();
        }
        // 获取原表达式,用来替换变量,生成最终的ID
        String expressionString = expression.getExpression();
        // 循环遍历变量列表
        for (ExpressionElement e : elements) {
            // 拼接Bean的名称
            String beanName = e.getVariableName() + VARIABLE_GENERATOR;
            // 从map中取出指定的Bean
            VariableGenerator variableGenerator = variableGeneratorMap.get(beanName);
            // 如果没有取到,那么直接忽略,说明没有创建该表达式对应的生成器
            if (Objects.isNull(variableGenerator)) {
                continue;
            }
            // 调用目标方法生成字符串
            String variableValue = variableGenerator.andThen(e, expression);
            // 如果不为空,就替换掉原表达式中的变量;就是用具体生成的值替换变量表达式
            // “$(pid)$(yearMonthDayHms)$(id:6:0)”会被替换成“TEST$(yearMonthDayHms)$(id:6:0)”
            if (StringUtils.isNotBlank(variableValue)) {
                expressionString =
                        StringUtils.replace(expressionString, e.getOriginString(), variableValue);
            }
        }
        // 返回最终结果
        return expressionString;
    }
        // 正则表达式,用来解析$(pid)$(yearMonthDayHms)$(id:6:0)表达式
    private static final Pattern EXPRESSION_PATTERN = Pattern.compile("\\$\\((.+?)\\)");

    private static final Map<String, Expression> EXPRESSION_MAP = Maps.newConcurrentMap();
    /**
     * 解析$(pid)$(yearMonthDayHms)$(id:6:0)
     *
     * @param expressionString
     * @return
     */
    private Expression parse(String key, String expressionString) {
        // 检查一下缓存中是否有解析号的表达式
        Expression expression = EXPRESSION_MAP.get(key);
        // 缓存不为空的话,直接返回
        if (Objects.nonNull(expression)) {
            return expression;
        }
        // 否则,直接解析
        synchronized (EXPRESSION_MAP) {
            // 双重检查,避免重复解析
            expression = EXPRESSION_MAP.get(key);
            if (Objects.nonNull(expression)) {
                return expression;
            }
            // 生成表达式对象
            expression = new Expression();
            expression.setKey(key);
            expression.setExpression(expressionString);
            List<ExpressionElement> expressionElements = Lists.newArrayList();
            Matcher matcher = EXPRESSION_PATTERN.matcher(expressionString);
            while (matcher.find()) {
                // 正则表达式,找出$()变量表达式,类似id:6:0
                String expressionVariable = matcher.group(1);
                // 表达式切割,分离出冒号分隔的参数
                String[] expressionVariables =
                        StringUtils.splitByWholeSeparatorPreserveAllTokens(
                                expressionVariable, VariableGenerator.COLON);
                ExpressionElement expre = new ExpressionElement();
                // 变量名称id
                expre.setVariableName(expressionVariables[0]);
                // 原生表达式$(id:6:0),便于后面直接替换
                expre.setOriginString(matcher.group());
                if (expressionVariables.length > 1) {
                    // 获取填充的最终长度
                    expre.setCount(CastUtil.castInt(expressionVariables[1]));
                }
                if (expressionVariables.length > 2) {
                    // 获取填充值
                    expre.setFillStringValue(expressionVariables[2]);
                }
                expressionElements.add(expre);
            }
            expression.setElements(expressionElements);
            // 放入本地缓存
            EXPRESSION_MAP.put(key, expression);
        }
        // 返回解析出来的表达式
        return expression;
    }
}
import lombok.Data;

import java.util.List;

/**
 * @className Expression
 * @date: 2021/2/18 下午2:53
 * @description: 解析$(pid)$(year)$(month)$(day)$(id:6:0)这种类型的表达式
 */
@Data
public class Expression {
    /** pid */
    private String key;
    /** 表达式 */
    private String expression;
    /** 解析结果 */
    private List<ExpressionElement> elements;
}

/**
 * @author zouwei
 * @className ExpressionElement
 * @date: 2021/2/18 下午2:56
 * @description: 解析${id:6:0}这种类型的标记
 */
@Data
public class ExpressionElement {
    // 原生变量表达式
    private String originString;
    // 变量名称
    private String variableName;
    // 总长度
    private int count;
        // 填充值,默认是空字符
    private String fillStringValue = StringUtils.SPACE;
}

初始化字段生成器

public abstract class InitFieldGenerator {

    public static final String INIT_FIELD_GENERATOR = "InitFieldGenerator";
    // 执行初始化操作
    public abstract String generator(String key, String initField);
}

以上代码咱们已经把整体的初始化、ID生成逻辑全部搞定,剩下的就是需要把对应的接口填充完毕就行。

针对表达式

$(pid)$(yearMonthDayHms)$(id:6:0)

咱们分别需要实现pidVariableGenerator、yearMonthDayHmsVariableGenerator、idVariableGenerator

@Bean
public VariableGenerator pidVariableGenerator() {
    return new VariableGenerator() {
        @Override
        public String apply(ExpressionElement e, Expression expression) {
            return expression.getKey();
        }
    };
}

private static final String YEAR_MONTH_DAY_HOUR_MINUTE_SECOND_FORMAT = "yyyyMMddHHmmss";

@Bean
public VariableGenerator yearMonthDayHmsVariableGenerator() {
    return new VariableGenerator() {
        @Override
        public String apply(ExpressionElement e, Expression expression) {
            return DateUtil.format(new DateTime(), YEAR_MONTH_DAY_HOUR_MINUTE_SECOND_FORMAT);
        }
    };
}

因为咱们的自增id是使用的redis的lua脚本实现的,所以会依赖redis。利用了redis执行lua脚本的原子性。

@Bean
public SerialIDVariableGenerator idVariableGenerator() {
    return new SerialIDVariableGenerator();
}

public class SerialIDVariableGenerator extends VariableGenerator {

    @Autowired private RedisTemplate redisTemplate;

    private InitParams initParams;
        // 构造函数
    public void initParams(String key, String initFields) {
        this.initParams = parse(key, initFields);
    }
    /**
     *  解析表达式   字段名:初始值:最大值:最小数量:扩容数量:初始数量:增长步长
     *
     * @param initField
     */
    private InitParams parse(String key, String initField) {
        InitParams initParams = new InitParams();
        if (StringUtils.contains(initField, COLON)) {
            String[] params = StringUtils.splitByWholeSeparatorPreserveAllTokens(initField, COLON);
            initParams.setFieldName(key + COLON + params[0]);
            initParams.setField(params);
        } else {
            initParams.setFieldName(key + COLON + initField);
            initParams.updateFields();
        }
        return initParams;
    }
        // 执行lua脚本,生成对应的自增id
    public String generate() {
        String fieldName = this.initParams.getFieldName();
        return executeLua(
                fieldName,
                initParams.getInitValue(),
                initParams.getMaxValue(),
                initParams.getMinCount(),
                initParams.getInitCount(),
                initParams.getExpansionStep(),
                initParams.getIncrStep());
    }
        // 执行生成函数
    @Override
    protected String apply(ExpressionElement e, Expression expression) {
        return generate();
    }
        // 执行lua脚本
    private String executeLua(
            String key,
            int initValue,
            int maxValue,
            int minCount,
            int initCount,
            int expansionStep,
            int incrStep) {
        // 执行lua脚本
        DefaultRedisScript<String> defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setResultType(String.class);
        defaultRedisScript.setScriptText(LUA_SCRIPT);
        RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
        String result =
                CastUtil.castString(
                        redisTemplate.execute(
                                defaultRedisScript,
                                serializer,
                                serializer,
                                Lists.newArrayList(key),
                                CastUtil.castString(initValue),
                                CastUtil.castString(maxValue),
                                CastUtil.castString(minCount),
                                CastUtil.castString(initCount),
                                CastUtil.castString(expansionStep),
                                CastUtil.castString(incrStep)));
        return result;
    }

    @Data
    private static class InitParams {

        /** 默认初始值 */
        private static final int DEFAULT_INIT_VALUE = 1;
        /** 默认最大值 */
        private static final int DEFAULT_MAX_VALUE = 9999;
        /** 默认最小数量 */
        private static final int DEFAULT_MIN_COUNT = 30;
        /** 默认初始数量 */
        private static final int DEFAULT_INIT_COUNT = 100;
        /** 默认扩容数量 */
        private static final int DEFAULT_EXPANSION_STEP = 50;
        /** 默认自增步长 */
        private static final int DEFAULT_INCR_STEP = 1;

        private final int[] params = {
            0,
            DEFAULT_INIT_VALUE,
            DEFAULT_MAX_VALUE,
            DEFAULT_MIN_COUNT,
            DEFAULT_EXPANSION_STEP,
            DEFAULT_INIT_COUNT,
            DEFAULT_INCR_STEP
        };
        /** 字段名称,其实就是key */
        private String fieldName;
        /** 初始值 */
        private int initValue;
        /** 最大值 */
        private int maxValue;
        /** 最小数量 */
        private int minCount;
        /** 扩容步长 */
        private int expansionStep;
        /** 初始数量 */
        private int initCount;
        /** 自增步长 */
        private int incrStep;

        public void setField(Object[] objects) {
            if (ArrayUtils.isEmpty(objects) || ArrayUtils.getLength(objects) < 2) {
                return;
            }
            for (int i = 1; i < objects.length; i++) {
                Object obj = objects[i];
                params[i] = CastUtil.castInt(obj);
            }
            updateFields();
        }

        public void updateFields() {
            this.initValue = params[1];
            this.maxValue = params[2];
            this.minCount = params[3];
            this.expansionStep = params[4];
            this.initCount = params[5];
            this.incrStep = params[6];
        }
    }
        // 该脚本的执行逻辑
    // 在redis中生成一个队列,指定初始化长度,第一个初始值,最大值,队列最小数量,每次扩容的数量,自增的步长
    // 1.如果队列不存在,就初始化队列,按照给定的初始化长度,初始值,自增步长,最大值等参数创建一个队列
    // 2.如果队列中值的数量超过队列最小数量,那么直接pop出一个值
    // 3.如果小于最小数量,那么直接循环生成指定步长的自增ID
    // 4.最终会pop出第一个数值
    // 5.如果是初始化的话,会返回success,否则就直接pop出第一个ID
    private static final String LUA_SCRIPT =
            "local key=KEYS[1]\nlocal initValue=tonumber(ARGV[1])\nlocal maxValue=tonumber(ARGV[2])\nlocal minCount=tonumber(ARGV[3])\nlocal initCount=tonumber(ARGV[4])\nlocal expansionStep=tonumber(ARGV[5])\nlocal incrStep=tonumber(ARGV[6])\nlocal len=redis.call('llen',key)\nlocal isInit=true\nlocal loop=initCount\nlocal nextValue=initValue\nif len>minCount\nthen\nreturn redis.call('lpop',key)\nend\nif len>0\nthen\nisInit=false\nloop=len+expansionStep\nnextValue=tonumber(redis.call('rpop',key))\nend\nwhile(len<loop)\ndo\nif nextValue>maxValue\nthen\nnextValue=initValue\nend\nredis.call('rpush',key,nextValue)\nnextValue=nextValue+incrStep\nlen=len+1\nend\nif isInit\nthen\nreturn 'success'\nend\nreturn redis.call('lpop',key)";
}

根据配置中的初始化字段的配置规则,咱们还需要一个idInitFieldGenerator初始化字段生成器

@Bean("idInitFieldGenerator")
public SerialIdInitFieldGenerator serialIdInitFieldGenerator() {
    return new SerialIdInitFieldGenerator(idVariableGenerator());
}

public class SerialIdInitFieldGenerator extends InitFieldGenerator {

    private SerialIDVariableGenerator serialIDVariableGenerator;

    public SerialIdInitFieldGenerator(SerialIDVariableGenerator serialIDVariableGenerator) {
        this.serialIDVariableGenerator = serialIDVariableGenerator;
    }
        
    // 利用了SerialIDVariableGenerator变量生成器的方法初始化
    @Override
    public String generator(String key, String initField) {
        serialIDVariableGenerator.initParams(key, initField);
        return serialIDVariableGenerator.generate();
    }
}

总结

1.核心生成逻辑还是利用了redis执行lua脚本的原子性

2.把表达式的生成逻辑拆分到具体的接口实现中去,方便规则的自定义扩展

目前粗略测试下来,线程并发的情况下大概1000个/s的生成速率。还有比较大的优化空间。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容