自定义全局自增ID生成器(下)

本文涉及的产品
云原生内存数据库 Tair,内存型 2GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Redis 版,经济版 1GB 1个月
简介: 自定义全局自增ID生成器

初始化字段生成器

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的生成速率。还有比较大的优化空间。


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
存储 NoSQL 数据库
全局id生成方式
全局id生成方式
|
2月前
|
存储 缓存 算法
分布式全局id
分布式全局id
|
2月前
|
SQL 存储 Java
MyBatis【付诸实践 02】 mapper文件未编译+statementType使用+返回结果字段顺序不一致+获取自增ID+一个update标签批量更新记录
MyBatis【付诸实践 02】 mapper文件未编译+statementType使用+返回结果字段顺序不一致+获取自增ID+一个update标签批量更新记录
47 0
|
8月前
阿里云RPA这个继续循环的组件是不是会自动自增一呢
阿里云RPA这个继续循环的组件是不是会自动自增一呢
55 1
|
9月前
|
存储 算法 安全
场景应用:id全局唯一且自增,如何实现?
场景应用:id全局唯一且自增,如何实现?
125 0
|
10月前
|
存储 Rust 算法
有关'全局唯一id'
有关'全局唯一id'
60 0
|
11月前
|
存储 算法 安全
全局唯一ID(自增ID、UUID、雪花算法)
一、介绍 系统唯一id是我们在设计阶段常常遇到的问题。在复杂的分布式系统中,几乎都需要对大量的数据和消息进行唯一标识。在设计初期,我们需要考虑日后数据量的级别,如果可能会对数据进行分库分表,那么就需要有一个全局唯一id来标识一条数据或记录。生成唯一id的策略有多种,但是每种策略都有它的适用场景、优点以及局限性。
|
Java 数据库连接 数据库
Mybatis使用generatedKey在插入数据时返回自增id始终为1,自增id实际返回到原对象当中的问题排查...
Mybatis使用generatedKey在插入数据时返回自增id始终为1,自增id实际返回到原对象当中的问题排查...
138 0
|
Java 数据库
JPA通用策略生成器(@GeneratedValue 四种标准用法为TABLE, SEQUENCE, IDENTITY, AUTO)
JPA通用策略生成器(@GeneratedValue 四种标准用法为TABLE, SEQUENCE, IDENTITY, AUTO)
158 0