0.1. 背景

在微服务盛行的当下,模块拆分粒度越来越细,若排查问题时,就需要一个能贯穿始终的 全局唯一的 ID ;在支付场景中的订单编号,银行流水号等生成均需要依赖序列号生成的工具。

本次基于 Spring Boot + Redis + Lua 来实现一个序列号生成器服务,并尝试包装成 Spring Boot Starter 进而彻底解决项目中序列号生成的难题。

  • 技术栈:Spring Boot 2.6.3 + Redis + Lua

  • 环境依赖:  JDK 1.8 + Maven 3.6.3

1.  搭建序列号生成服务

  • 项目结构一览

  • 引入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>idgenerator</artifactId>
    <version>0.0.1</version>
    <name>idgenerator</name>
    <description>Id generator for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.21.0</version>
                <configuration>
                    <!--默认关掉单元测试 -->
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  • 添加 Redis 相关配置

在 application.properties 文件中加入 redis 相关配置。

### Redis 缓存配置信息
# 主机名称
spring.redis.host=127.0.0.1
# 端口号
spring.redis.port=6379
# 认证密码
spring.redis.password=
# 连接超时时间
spring.redis.timeout=500
# 默认数据库
spring.redis.database=0
  • 编写 Lua 脚本

在 resources 目录下创建 redis-script-single.lua 文件,内容如下。

-- moudle tag
local tag = KEYS[1];
if tag == nil then
    tag = 'default';
end
-- if user do not pass shardId, default partition is 0.
local partition
if KEYS[2] == nil then
    partition = 0;
else
    partition = KEYS[2] % 4096;
endlocal seqKey = 'idgenerator_' .. tag .. '_' .. partition;
local step = 1;local count;
repeat
    count = tonumber(redis.call('INCRBY', seqKey, step));
until count < (1024 - step)-- count how many seq are generated in one millisecond
if count == step then
    redis.call('PEXPIRE', seqKey, 1);
endlocal now = redis.call('TIME');
-- second, microSecond, partition, seq
return { tonumber(now[1]), tonumber(now[2]), partition, count }

重点关注 redis.call('INCRBY', seqKey, step)  作用是对 seqKey 按照 step 步长进行递增;以及  re dis.call('PEXPIRE ', seqKey, 1); 设置 seqKey 的失效时间,可依据需求是否需要。

  • Redis 脚本支持类定义( ScriptConfiguration.java )

创建 RedisScript 的子类 DefaultRedisScript 对象,内部设置了 lua 文件的位置以及脚本返回格式。

package com.example.idgenerator.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.scripting.support.ResourceScriptSource;import java.util.List;@Configuration
public class ScriptConfiguration {    @Bean
    public RedisScript<List> redisScript() {
        Resource resource = new ResourceScriptSource(new ClassPathResource("redis-script-single.lua")).getResource();
        return RedisScript.of(resource, List.class);
    }
}
  • 定义序列号 Service(IdGenService.java)

package com.example.idgenerator.service;/**
 * 序列号生成器 Service
 */
public interface IdGenService {
    String next();
}
  • 定义序列号 Service 实现( RedisIdGenService.java )

package com.example.idgenerator.service.impl;import com.example.idgenerator.service.IdGenService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;@Service
public class RedisIdGenService implements IdGenService {    private Logger logger = LoggerFactory.getLogger(RedisIdGenService.class);    @Autowired
    private StringRedisTemplate stringRedisTemplate;    @Autowired
    private RedisScript<List> redisScript;    public String next() {
        List<String> keys = new ArrayList<>();
        //keys.add("USER_MOUDLE");
        //keys.add("1");
        List<Long> result = stringRedisTemplate.execute(redisScript, keys);
        long id = buildId(result.get(0), result.get(1), result.get(2), result.get(3));
        logger.info("序列号:" + id);
        return String.valueOf(id);
    }    public long buildId(long second, long microSecond, long shardId, long seq) {
        long miliSecond = second * 1000L + microSecond / 1000L;
        return (miliSecond << 22) + (shardId << 10) + seq;
    }
}
  • 定义序列号 API( IdGenController.java )

package com.example.idgenerator.controller;import com.example.idgenerator.service.IdGenService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class IdGenController {    private Logger logger = LoggerFactory.getLogger(IdGenController.class);    @Autowired
    private IdGenService idGenService;    @GetMapping("/getId")
    public String getId() {
        String seq = idGenService.next();
        logger.info("生成序列号:" + seq);
        return seq;
    }
}
  • 启动服务验证

启动服务,浏览器访问  http://localhost:8080/getId,控制台输出:

至此,一个基于 Spring Boot 的序列号生成器服务就完成了,可以直接集成到项目中去使用,不过是提供 HTTP 的服务,若不直接提供 WEB 服务,考虑到使用方便,是否可以考虑封装成 starter 呢?

2.  包装成序列号生成器 starter

考虑到直观,直接新建项目,项目名:idgenerator-spring-boot-starter,项目整体结构如下。

  • 添加依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.idgenerator</groupId>
    <artifactId>idgenerator-spring-boot-starter</artifactId>
    <version>0.0.1</version>
    <name>idgenerator-spring-boot-starter</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>
  • 添加 Redis 相关配置

 ### Redis 缓存配置信息
# 主机名称
spring.redis.host=127.0.0.1
# 端口号
spring.redis.port=6379
# 认证密码
spring.redis.password=
# 连接超时时间
spring.redis.timeout=500
# 默认数据库
spring.redis.database=0
  • 编写 Lua 脚本

-- moudle tag
local tag = KEYS[1];
if tag == nil then
    tag = 'default';
end
-- if user do not pass shardId, default partition is 0.
local partition
if KEYS[2] == nil then
    partition = 0;
else
    partition = KEYS[2] % 4096;
endlocal seqKey = 'idgenerator_' .. tag .. '_' .. partition;
local step = 1;local count;
repeat
    count = tonumber(redis.call('INCRBY', seqKey, step));
until count < (1024 - step)-- count how many seq are generated in one millisecond
if count == step then
    redis.call('PEXPIRE', seqKey, 1);
endlocal now = redis.call('TIME');
-- second, microSecond, partition, seq
return { tonumber(now[1]), tonumber(now[2]), partition, count }
  • 编写 Service 以及实现

package org.idgenerator.service;/**
 * 序列号生成器 Service
 */
public interface IdGenService {
    String next();
}
package org.idgenerator.service.impl;import org.idgenerator.service.IdGenService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;@Service
public class RedisIdGenService implements IdGenService {    private Logger logger = LoggerFactory.getLogger(RedisIdGenService.class);    private StringRedisTemplate stringRedisTemplate;    private RedisScript<List> redisScript;    public RedisIdGenService(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
        Resource luaResource = new ResourceScriptSource(new ClassPathResource("redis-script-single.lua")).getResource();
        RedisScript<List> redisScript = RedisScript.of(luaResource,List.class);
        this.redisScript = redisScript;
    }    public String next() {
        List<String> keys = new ArrayList<>();
        //keys.add("USER_MOUDLE");
        //keys.add("1");
        List<Long> result = stringRedisTemplate.execute(redisScript, keys);
        long id = buildId(result.get(0), result.get(1), result.get(2), result.get(3));
        logger.info("序列号:" + id);
        return String.valueOf(id);
    }    public long buildId(long second, long microSecond, long shardId, long seq) {
        long miliSecond = second * 1000L + microSecond / 1000L;
        return (miliSecond << 22) + (shardId << 10) + seq;
    }
}
  • 定义 IdGenAutoConfiguration 自动配置类

package org.idgenerator.autoconfigure;import org.idgenerator.service.IdGenService;
import org.idgenerator.service.impl.RedisIdGenService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;@Configuration
@ConditionalOnClass({StringRedisTemplate.class})
public class IdGenAutoConfiguration {    @Bean
    @ConditionalOnMissingBean(IdGenService.class)
    public IdGenService idGen(StringRedisTemplate stringRedisTemplate) {
        return new RedisIdGenService(stringRedisTemplate);
    }
}
  • 定义 spring.factories 文件

在 resources 目录下创建 META-INF 文件夹,然后创建 spring.factories 文件,文件内容如下。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.idgenerator.autoconfigure.IdGenAutoConfiguration
  • 编译打包

3.  序列号生成器 starter 验证

创建 ToyApp 项目,并引入第 2 步编译之后的序列号生成器 starter。

  • pom.xml 详细内容。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>ToyApp</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>ToyApp</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>        <dependency>
            <groupId>org.idgenerator</groupId>
            <artifactId>idgenerator-spring-boot-starter</artifactId>
            <systemPath>
                ${project.basedir}/lib/idgenerator-spring-boot-starter-0.0.1.jar
            </systemPath>
            <scope>system</scope>
            <version>0.0.1</version>
        </dependency>
    </dependencies>    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  • 编写测试类

@SpringBootTest
class DemoIdApplicationTests {    @Autowired
    private IdGenService idGenService;    @Test
    public void idGenTest() {
        System.out.println("调用自定义序列号生成器 starter 生成的序列号为:" + idGenService.next());
    }
}

执行后控制台输出如下:

调用自定义序列号生成器 starter 生成的序列号为:6919868765123379201

至此,自定义序列号生成器 starter 就验证通过了,收工。

4.  例行回顾

本文主要是基于 Spring Boot 封装一个序列号生成器服务 + Starter ,只需通过封装的 Starter,就可以很轻松的在项目中生成全局唯一的序列 ID 。

玩转 Spring Boot 应用篇(序列号生成器服务实现)相关推荐

  1. 玩转 Spring Boot 应用篇(搭建菜菜的店铺)

    0.  0.0. 历史文章整理 玩转 Spring Boot 入门篇 玩转 Spring Boot 集成篇(MySQL.Druid.HikariCP) 玩转 Spring Boot 集成篇(MyBat ...

  2. 玩转spring boot——开篇

    很久没写博客了,而这一转眼就是7年.这段时间并不是我没学习东西,而是园友们的技术提高的非常快,这反而让我不知道该写些什么.我做程序已经有十几年之久了,可以说是彻彻底底的"程序老炮" ...

  3. 玩转spring boot——结合阿里云持续交付

    前言 在互联网项目中,项目测试.部署往往需要花费大量时间.传统方式是在本地打包.测试完毕程序,然后通过ftp上传至服务器,再把测试的配置文件修改为生产环境的配置文件,最后重新运行服务.这一过程如果交给 ...

  4. 玩转spring boot——结合redis

    前言 Redis 是一个高性能的key-value数据库. redis的出现,很大程度补偿了memcached这类key/value存储的不足,在部 分场合可以对关系数据库起到很好的补充作用.它提供了 ...

  5. (转)构建微服务:Spring boot 入门篇

    转自: Spring Boot(一):入门篇 - 纯洁的微笑 - 博客园 : 什么是Spring Boot Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 S ...

  6. 玩转spring boot——国际化

    前言 在项目开发中,可能遇到国际化的问题,而支持国际化却是一件很头疼的事.但spring boot给出了一个非常理想和方便的方案. 一.准备工作 pom.xml: <?xml version=& ...

  7. 玩转spring boot——ajax跨域

    前言  java语言在多数时,会作为一个后端语言,为前端的php,node.js等提供API接口.前端通过ajax请求去调用java的API服务.今天以node.js为例,介绍两种跨域方式:Cross ...

  8. 玩转spring boot——结合docker

    前言 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 liunx机器上,也可以实现虚拟化.容器是完全使用沙箱机制,相互之间不会有 ...

  9. 使用Spring Boot和Kubernetes构建微服务架构

    "我喜欢编写身份验证和授权代码." 〜从来没有Java开发人员. 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证. 在本教程 ...

  10. 如何使用消息队列,Spring Boot和Kubernetes扩展微服务

    by Daniele Polencic 由Daniele Polencic 如何使用消息队列,Spring Boot和Kubernetes扩展微服务 (How to scale Microservic ...

最新文章

  1. ***正传——著名网络安全人士郭鑫成长经历
  2. 朱俊彦团队最新论文:用GAN监督学习给左晃右晃的猫狗加表情,很丝滑很贴合...
  3. 数据报套接字服务端和客户端
  4. mybatis使用过程遇到的一些问题及解决方法
  5. linux - mysql 异常:Ignoring query to other database
  6. 信号与信息处理专业属于计算机类吗,信号与信息处理
  7. 第一章 路径规划算法概述
  8. 2022年 - 2023年 最新计算机毕业设计 本科 选题大全 汇总
  9. Could not find method jackOptions() for arguments
  10. Smart Beta是什么?
  11. 2021/8/12 网络机顶盒
  12. 一起来玩U3D之基础物理引擎
  13. c语言程序设计实验13文件,第13章_文件---《C语言程序设计》实验指导.ppt
  14. 数据库里这是怎么回事啊?
  15. PHP+Redis令牌桶算法 接口限流
  16. TeamTalk源码分析(二) —— 服务器端的程序的编译与部署
  17. c语言read有什么作用,c语言read函数读到什么结束
  18. 苹果新专利:用AR眼镜来控制iPhone屏幕输出,保护隐私
  19. 在VMware8.0下安装crux2.6
  20. Android 点击查看大图(长按保存图片)

热门文章

  1. 【Scala】镜像网站下载,速度直接起飞
  2. 游戏开发中的脚本语言
  3. 【NLP】一文了解词性标注CRF模型
  4. 打开UG10 C语言错误,修复UG软件在win10中出现运行乱码的错误方法
  5. 分享180个动态模板PPT模板,总有一款适合你
  6. 线性调频信号的脉冲压缩(匹配滤波)
  7. mysql官网 源码下载_mysql官网源码包下载
  8. 【JSP】测试Ajax
  9. MIKE水动力笔记6_如何自己制作实测数据的时间序列文件dfs0
  10. tomcat乱码怎么解决