玩转 Spring Boot 应用篇(序列号生成器服务实现)
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;
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 millisecond
if count == step then
redis.call('PEXPIRE', seqKey, 1);
end
local 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;
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 millisecond
if count == step then
redis.call('PEXPIRE', seqKey, 1);
end
local 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 应用篇(序列号生成器服务实现)相关推荐
- 玩转 Spring Boot 应用篇(搭建菜菜的店铺)
0. 0.0. 历史文章整理 玩转 Spring Boot 入门篇 玩转 Spring Boot 集成篇(MySQL.Druid.HikariCP) 玩转 Spring Boot 集成篇(MyBat ...
- 玩转spring boot——开篇
很久没写博客了,而这一转眼就是7年.这段时间并不是我没学习东西,而是园友们的技术提高的非常快,这反而让我不知道该写些什么.我做程序已经有十几年之久了,可以说是彻彻底底的"程序老炮" ...
- 玩转spring boot——结合阿里云持续交付
前言 在互联网项目中,项目测试.部署往往需要花费大量时间.传统方式是在本地打包.测试完毕程序,然后通过ftp上传至服务器,再把测试的配置文件修改为生产环境的配置文件,最后重新运行服务.这一过程如果交给 ...
- 玩转spring boot——结合redis
前言 Redis 是一个高性能的key-value数据库. redis的出现,很大程度补偿了memcached这类key/value存储的不足,在部 分场合可以对关系数据库起到很好的补充作用.它提供了 ...
- (转)构建微服务:Spring boot 入门篇
转自: Spring Boot(一):入门篇 - 纯洁的微笑 - 博客园 : 什么是Spring Boot Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 S ...
- 玩转spring boot——国际化
前言 在项目开发中,可能遇到国际化的问题,而支持国际化却是一件很头疼的事.但spring boot给出了一个非常理想和方便的方案. 一.准备工作 pom.xml: <?xml version=& ...
- 玩转spring boot——ajax跨域
前言 java语言在多数时,会作为一个后端语言,为前端的php,node.js等提供API接口.前端通过ajax请求去调用java的API服务.今天以node.js为例,介绍两种跨域方式:Cross ...
- 玩转spring boot——结合docker
前言 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 liunx机器上,也可以实现虚拟化.容器是完全使用沙箱机制,相互之间不会有 ...
- 使用Spring Boot和Kubernetes构建微服务架构
"我喜欢编写身份验证和授权代码." 〜从来没有Java开发人员. 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证. 在本教程 ...
- 如何使用消息队列,Spring Boot和Kubernetes扩展微服务
by Daniele Polencic 由Daniele Polencic 如何使用消息队列,Spring Boot和Kubernetes扩展微服务 (How to scale Microservic ...
最新文章
- ***正传——著名网络安全人士郭鑫成长经历
- 朱俊彦团队最新论文:用GAN监督学习给左晃右晃的猫狗加表情,很丝滑很贴合...
- 数据报套接字服务端和客户端
- mybatis使用过程遇到的一些问题及解决方法
- linux - mysql 异常:Ignoring query to other database
- 信号与信息处理专业属于计算机类吗,信号与信息处理
- 第一章 路径规划算法概述
- 2022年 - 2023年 最新计算机毕业设计 本科 选题大全 汇总
- Could not find method jackOptions() for arguments
- Smart Beta是什么?
- 2021/8/12 网络机顶盒
- 一起来玩U3D之基础物理引擎
- c语言程序设计实验13文件,第13章_文件---《C语言程序设计》实验指导.ppt
- 数据库里这是怎么回事啊?
- PHP+Redis令牌桶算法 接口限流
- TeamTalk源码分析(二) —— 服务器端的程序的编译与部署
- c语言read有什么作用,c语言read函数读到什么结束
- 苹果新专利:用AR眼镜来控制iPhone屏幕输出,保护隐私
- 在VMware8.0下安装crux2.6
- Android 点击查看大图(长按保存图片)