先看整体效果
把简单的东西 "傻瓜化" 是软件开发追求的目标之一. 请看下图:
左边是在 application.YAML 里配置了 3 个生成器, 右边可以直接注入到代码中使用, 注意, 不用写任何代码. 这酸爽.
下面请看效果:
上面是 3 个生成器生成的第一个序号. 哎吆, 还不错哦.
慢慢学会分析
序列号大家都非常熟悉, 无非就是一个初始值, 步长, 有时还有最大值. 这只是最基本的信息, 还可以按需添加其他的.
很容易抽象出一个接口, 如下代码:
- /**
- * 序列号生成器
- * @author lixinjie
- * @since 2019-04-04
- */
- public interface SnGenerator {
- /** 名称, 根据实际情况使用 */
- String getName();
- /** 注册到容器中的 bean 名称 */
- String getBeanName();
- /** 初始值 */
- long getInitNum();
- /** 步长 */ long getStep();
- /** 获取下一个序列号 */
- long nextNum(); /** 最大值 */
- long getMaxNum();
- }
剩下的就是下面这三个问题了:
接口有了之后, 自然就是基于 Redis 的实现了, 这是遇到第一个问题.
还需要根据配置动态向容器中注册 bean 定义, 这是第二个问题.
自然需要从配置文件中读出这些配置信息, 供上一步使用, 这是第三个问题.
再来学会实现
接口实现时主要用到 Redis 的 INCRBY 命令, 这是个原子命令. 即使你有多个节点, 每个节点有多个线程同时来调用, 也是 OK 的.
而且 key 不存在时, 首次调用时还会将 key 设置为 0. 这样带来的好处就是, 程序不用考虑 key 是否存在, 直接调用自增就可以了.
唯一不爽的就是 key 只会被设置为 0, 如果初始值不是从 0 开始的, 就真有些不爽, 最简单的办法就是在程序中把初始值作为偏移量叠加上去.
当然还有另一个办法, 就是主动设置 key 的初始值. 因为存在并发, 自然要使用 SETNX 命令. 这样已经完全 OK, 但还是会有人在心理上觉得不安全. 那就在应用启动阶段执行该命令, 此时肯定不会有调用的. 请看下面源码:
- /**
- * 基于 Redis 的实现
- * @author lixinjie
- * @since 2019-04-04
- */
- public class RedisSnGenerator implements SnGenerator {
- @Autowired
- private StringRedisTemplate stringRedisTemplate;
- private String name;
- private String beanName;
- private long initNum;
- private long step;
- private long maxNum;
- public RedisSnGenerator(String name, String beanName, long initNum, long step, long maxNum) {
- this.name = name;
- this.beanName = beanName;
- this.initNum = initNum;
- this.step = step;
- this.maxNum = maxNum;
- } @PostConstruct
- public void init() {
- if (!stringRedisTemplate.hasKey(getName())) {
- stringRedisTemplate.opsForValue().setIfAbsent(getName(), String.valueOf(getInitNum()));
- } }
- @Override
- public String getName() {
- return name; }
- public String getBeanName() {
- return beanName;
- }
- @Override
- public long getInitNum() {
- return initNum;
- }
- @Override
- public long getStep() {
- return step; }
- @Override
- public long nextNum() {
- return stringRedisTemplate.opsForValue().increment(getName(), getStep());
- }
- @Override
- public long getMaxNum() {
- return maxNum; }
- }
要动态注册 bean 定义, Spring 框架提供了专用接口, BeanDefinitionRegistryPostProcessor:
- public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
- /**
- * @param registry the bean definition registry used by the application context
- * @throws org.springframework.beans.BeansException in case of errors
- */
- void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
- }
它是一个 bean 工厂后处理器, 应用启动时框架会自动检测并应用该类型的实现类.
要使用配置文件中的内容, Spring 框架提供了专门的接口, EnvironmentAware:
- public interface EnvironmentAware extends Aware {
- /**
- * Set the {@code Environment} that this component runs in.
- */
- void setEnvironment(Environment environment);
- }
它是一个 Aware 接口, 应用启动时框架会自动检测该类型的接口并把你需要的给你 set 进去.
这块源码有点多, 我们就只看两部分吧. 下面是从配置文件中读出配置信息的:
- private SnGeneInfo[] parseSnGeneInfos() {
- String prefix = "sngenerator";
- String[] generators = environment.getProperty(prefix + ".generators", String[].class);
- SnGeneInfo[] infos = new SnGeneInfo[generators.length];
- for (int i = 0; i <generators.length; i++) {
- infos[i] = buildSnGeneInfo(prefix, generators[i]);
- } return infos; }
- private SnGeneInfo buildSnGeneInfo(String prefix, String generator) {
- return new SnGeneInfo(
- prefix + ":" + generator,
- environment.getProperty(prefix + "." + generator + ".bean-name"),
- environment.getProperty(prefix + "." + generator + ".init-num", long.class),
- environment.getProperty(prefix + "." + generator + ".step", long.class),
- environment.getProperty(prefix + "." + generator + ".max-num", long.class)
- ); }
下面是注册 bean 定义的:
- @Override
- public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
- SnGeneInfo[] geneInfos = parseSnGeneInfos();
- System.out.println(logInfo(geneInfos));
- for (SnGeneInfo geneInfo : geneInfos) {
- BeanDefinitionBuilder bdb = BeanDefinitionBuilder.genericBeanDefinition(RedisSnGenerator.class);
- bdb.addConstructorArgValue(geneInfo.getKeyName());
- bdb.addConstructorArgValue(geneInfo.getBeanName());
- bdb.addConstructorArgValue(geneInfo.getInitNum());
- bdb.addConstructorArgValue(geneInfo.getStep());
- bdb.addConstructorArgValue(geneInfo.getMaxNum());
- registry.registerBeanDefinition(geneInfo.getBeanName(), bdb.getBeanDefinition());
- } }
需要很多序列的话?
如果需要非常多的序列生成器的话, 上面的方法不可取. 可以采用 "分组" 序列生成器, 每一组内可以有足够多的序列, 且组与组之间互不影响.
下面请看使用方法, 还是一样的简单:
下面请看效果:
每一组内部的 key, 就是上面的 f1/f2/f3 等, 不用配置, 程序按需直接传入即可.
此时获取下一个序列号的方法需要带一个参数, 就是用来传这个 key 的. 它是基于 Redis 的哈希 (Hash) 实现的.
- /**
- * 分组序列号生成器
- * @author lixinjie
- * @since 2019-04-04
- */
- public interface GroupSnGenerator {
- /** 名称, 根据实际情况使用 */
- String getName();
- /** 注册到容器中的 bean 名称 */
- String getBeanName();
- /** 初始值 */
- long getInitNum();
- /** 步长 */ long getStep();
- /** 获取下一个序列号 */
- long nextNum(String identifier);
- /** 最大值 */
- long getMaxNum();
- }
此时也会遇到给 hashkey 设置初始值的问题, 肯定和上面不一样, 请阅读源码吧.
- /**
- * 基于 Redis 的实现
- * @author lixinjie
- * @since 2019-04-04
- */
- public class RedisGroupSnGenerator implements GroupSnGenerator {
- private Map<String, Boolean> existHashKeys = new ConcurrentHashMap<>();
- @Autowired
- private StringRedisTemplate stringRedisTemplate;
- private String name;
- private String beanName;
- private long initNum;
- private long step;
- private long maxNum;
- public RedisGroupSnGenerator(String name, String beanName, long initNum, long step, long maxNum) {
- this.name = name;
- this.beanName = beanName;
- this.initNum = initNum;
- this.step = step;
- this.maxNum = maxNum;
- } @PostConstruct
- public void init() {
- if (!stringRedisTemplate.hasKey(getName())) {
- stringRedisTemplate.opsForHash().putIfAbsent(getName(), "flag", String.valueOf(getInitNum()));
- } }
- @Override
- public String getName() {
- return name; }
- public String getBeanName() {
- return beanName;
- }
- @Override
- public long getInitNum() {
- return initNum;
- }
- @Override
- public long getStep() {
- return step; }
- @Override
- public long nextNum(String identifier) {
- if (!existHashKeys.containsKey(identifier)) {
- stringRedisTemplate.opsForHash().putIfAbsent(getName(), identifier, String.valueOf(getInitNum()));
- existHashKeys.putIfAbsent(identifier, Boolean.TRUE);
- }
- return stringRedisTemplate.opsForHash().increment(getName(), identifier, getStep());
- }
- @Override
- public long getMaxNum() {
- return maxNum; }
- }
源码地址: https://github.com/coding-new-talking/cnt-springboot.git
(END)
编程新说, 本号由工作 10 年的
架构师维护, 洞察技术本质,
生动幽默有趣, 欢迎关注!
来源: https://www.cnblogs.com/lixinjie/p/a-easy-use-serial-number-generator.html