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

2022-05-31 09:47:01 浏览数 (2)

0.

0.0. 历史文章整理

玩转 Spring Boot 入门篇

玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP)

玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持)

玩转 Spring Boot 集成篇(Redis)

玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin)

玩转 Spring Boot 集成篇(RabbitMQ)

玩转 Spring Boot 集成篇(@Scheduled、静态、动态定时任务)

玩转 Spring Boot 集成篇(任务动态管理代码篇)

玩转 Spring Boot 集成篇(定时任务框架Quartz)

玩转 Spring Boot 原理篇(源码环境搭建)

玩转 Spring Boot 原理篇(核心注解知多少)

玩转 Spring Boot 原理篇(自动装配前凑之自定义Starter)

玩转 Spring Boot 原理篇(自动装配源码剖析)

玩转 Spring Boot 原理篇(启动机制源码剖析)

玩转 Spring Boot 原理篇(内嵌Tomcat实现原理&优雅停机源码剖析)

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

玩转 Spring Boot 应用篇(解决菜菜店铺商品超卖问题)

玩转 Spring Boot 应用篇(引入Redis解决店铺高并发读的问题)

玩转 Spring Boot 应用篇(引入RabbitMQ解决店铺下单峰值问题)

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 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 步长进行递增;以及 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;

@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)
代码语言: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;

@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)
代码语言: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;

@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,项目整体结构如下。

  • 添加依赖
代码语言: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 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 以及实现
代码语言: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;

@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 自动配置类
代码语言: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复制
@SpringBootTest
class 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。

参考资料:

https://spring.io/

https://start.spring.io/

https://spring.io/projects/spring-boot

https://github.com/spring-projects/spring-boot

https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/

https://stackoverflow.com/questions/tagged/spring-boot

0 人点赞