
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 taglocal tag = KEYS[1];if tag == nil thentag = 'default';end-- if user do not pass shardId, default partition is 0.local partitionif KEYS[2] == nil thenpartition = 0;elsepartition = KEYS[2] % 4096;endlocal seqKey = 'idgenerator_' .. tag .. '_' .. partition;local step = 1;local count;repeatcount = tonumber(redis.call('INCRBY', seqKey, step));until count < (1024 - step)-- count how many seq are generated in one millisecondif count == step thenredis.call('PEXPIRE', seqKey, 1);endlocal now = redis.call('TIME');-- second, microSecond, partition, seqreturn { 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;@Configurationpublic class ScriptConfiguration {@Beanpublic 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;@Servicepublic class RedisIdGenService implements IdGenService {private Logger logger = LoggerFactory.getLogger(RedisIdGenService.class);@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate 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;@RestControllerpublic class IdGenController {private Logger logger = LoggerFactory.getLogger(IdGenController.class);@Autowiredprivate 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 taglocal tag = KEYS[1];if tag == nil thentag = 'default';end-- if user do not pass shardId, default partition is 0.local partitionif KEYS[2] == nil thenpartition = 0;elsepartition = KEYS[2] % 4096;endlocal seqKey = 'idgenerator_' .. tag .. '_' .. partition;local step = 1;local count;repeatcount = tonumber(redis.call('INCRBY', seqKey, step));until count < (1024 - step)-- count how many seq are generated in one millisecondif count == step thenredis.call('PEXPIRE', seqKey, 1);endlocal now = redis.call('TIME');-- second, microSecond, partition, seqreturn { 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;@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 自动配置类
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>
-
编写测试类
@SpringBootTestclass DemoIdApplicationTests {@Autowiredprivate IdGenService idGenService;@Testpublic void idGenTest() {System.out.println("调用自定义序列号生成器 starter 生成的序列号为:" + idGenService.next());}}
执行后控制台输出如下:
调用自定义序列号生成器 starter 生成的序列号为:6919868765123379201
至此,自定义序列号生成器 starter 就验证通过了,收工。
4. 例行回顾
本文主要是基于 Spring Boot 封装一个序列号生成器服务 + Starter ,只需通过封装的 Starter,就可以很轻松的在项目中生成全局唯一的序列 ID 。










