前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >玩转 Spring Boot 应用篇(序列号生成器服务实现)(十九)

玩转 Spring Boot 应用篇(序列号生成器服务实现)(十九)

作者头像
botkenni
发布2022-09-23 20:50:15
3220
发布2022-09-23 20:50:15
举报
文章被收录于专栏:IT码农IT码农

0. 

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. 搭建序列号生成服务

  • 项目结构一览
  • 引入依赖
代码语言:javascript
复制
<?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 相关配置。

代码语言:javascript
复制
### 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 文件,内容如下。

代码语言:javascript
复制
-- moudle taglocal tag = KEYS[1];if tag == nil then    tag = 'default';end-- if user do not pass shardId, default partition is 0.local partitionif KEYS[2] == nil then    partition = 0;else    partition = KEYS[2] % 4096;end
local 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 millisecondif count == step then    redis.call('PEXPIRE', seqKey, 1);end
local now = redis.call('TIME');-- second, microSecond, partition, seqreturn { tonumber(now[1]), tonumber(now[2]), partition, count }

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

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

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

代码语言:javascript
复制
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;
@Configurationpublic 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)
代码语言:javascript
复制
package com.example.idgenerator.service;
/** * 序列号生成器 Service */public interface IdGenService {    String next();}
  • 定义序列号 Service 实现(RedisIdGenService.java)
代码语言:javascript
复制
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;
@Servicepublic 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)
代码语言:javascript
复制
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;
@RestControllerpublic 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,项目整体结构如下。

  • 添加依赖
代码语言:javascript
复制
<?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 相关配置
代码语言:javascript
复制
 ### Redis 缓存配置信息# 主机名称spring.redis.host=127.0.0.1# 端口号spring.redis.port=6379# 认证密码spring.redis.password=# 连接超时时间spring.redis.timeout=500# 默认数据库spring.redis.database=0
  • 编写 Lua 脚本
代码语言:javascript
复制
-- moudle taglocal tag = KEYS[1];if tag == nil then    tag = 'default';end-- if user do not pass shardId, default partition is 0.local partitionif KEYS[2] == nil then    partition = 0;else    partition = KEYS[2] % 4096;end
local 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 millisecondif count == step then    redis.call('PEXPIRE', seqKey, 1);end
local now = redis.call('TIME');-- second, microSecond, partition, seqreturn { tonumber(now[1]), tonumber(now[2]), partition, count }
  • 编写 Service 以及实现
代码语言:javascript
复制
package org.idgenerator.service;
/** * 序列号生成器 Service */public interface IdGenService {    String next();}
代码语言:javascript
复制
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;
@Servicepublic 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 自动配置类
代码语言:javascript
复制
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 文件,文件内容如下。

代码语言:javascript
复制
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.idgenerator.autoconfigure.IdGenAutoConfiguration
  • 编译打包

3. 序列号生成器 starter 验证

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

  • pom.xml 详细内容。
代码语言:javascript
复制
<?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>
  • 编写测试类
代码语言:javascript
复制
@SpringBootTestclass DemoIdApplicationTests {
    @Autowired    private IdGenService idGenService;
    @Test    public void idGenTest() {        System.out.println("调用自定义序列号生成器 starter 生成的序列号为:" + idGenService.next());    }}

执行后控制台输出如下:

代码语言:javascript
复制
调用自定义序列号生成器 starter 生成的序列号为:6919868765123379201

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

4. 例行回顾

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-09-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档