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

本文介绍了如何基于 Spring Boot、Redis 和 Lua 实现一个序列号生成器服务,详细阐述了从搭建服务到封装成 Spring Boot Starter 的全过程,旨在为项目提供全局唯一ID解决方案。

0. 

31a89ed262640d31ca0279cb8837c15d.png

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

  • 项目结构一览

93208f7f1f1ae2ed43ef65be3af78565.png

  • 引入依赖

<?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 步长进行递增;以及 redis.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,控制台输出:

fdfc8bb9b6fcda23d10cfa1817930d52.png

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

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

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

351689d40f8dbdf4cd331810fbc08ba7.png

  • 添加依赖

<?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
  • 编译打包

2caa8f39911a03cacd0234ae07668198.png

3. 序列号生成器 starter 验证

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

cff32c76ccc94d05bfa00540b00d7dc2.png

  • 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。

9ba059f4a5806bed3f194fa6380765b3.png

一起聊技术、谈业务、喷架构,少走弯路,不踩大坑,会持续输出更多精彩分享,欢迎关注,敬请期待!

2c1322a2c2a58b6eb809c5c91a03eee8.jpeg

参考资料:

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值