[Java]基于Redis的分布式环境下的自增编号生成器

本文介绍一款在分布式环境下的自增编号生成器的设计与代码实现,可实现在分布式环境下的编号递增且唯一。 主要基于Redis+分布式锁实现。

1 设计与实现

此编号生成器可生成格式为:{前缀}-{年份}-{自增整数}的编号。如:Ticket-2025-0001,当跨年后自增整数会重置,并可配置自增整数的最少补全位数。

代码如下:

java 复制代码
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.time.LocalDateTime;
import java.time.Year;
import java.util.Objects;
import java.util.function.Supplier;

/**
 * 自增编号生成器
 */
public class NumberGenerator {
    // 年份的redis key
    private static final String YEAR_KEY = "numberGenerateYear";

    private final StringRedisTemplate stringRedisTemplate;

    private final RedissonClient redissonClient;

    // 自增整数的redis key
    private final String numberKey;

    // 前缀
    private final String prefix;

    // 编号格式化字符串
    private final String format;

    /**
     * 初始化编号生成器
     * <pre>
     *     编号生成格式:{prefix}-{当前年份}-{自增整数};
     *     当自增整数不足fillNum位时,会前补零,补齐到fillNum位;
     *     举例:(1)前缀为Ticket,当前为2023年,自增整数为12,fillNum为5时,
     *          生成编号:Ticket-2023-00012;
     *
     *          (2)前缀为Ticket,当前为2023年,自增整数d为121121,fillNum为5时,
     *          生成编号:Ticket-2023-121121
     * </pre>
     *
     * @param prefix              编号前缀
     * @param fillNum             自增整数的最少补全位数
     * @param initNumberSupplier  初始整数值提供器
     * @param stringRedisTemplate StringRedisTemplate bean依赖注入
     * @param redissonClient      RedissonClient bean依赖注入
     */
    public NumberGenerator(String prefix, int fillNum, Supplier<Integer> initNumberSupplier, StringRedisTemplate stringRedisTemplate, RedissonClient redissonClient) {
        this.prefix = prefix;
        this.format = "%s-%d-%0" + fillNum + "d";
        this.numberKey = "numberGenerate:" + prefix;

        // 注入依赖bean
        this.stringRedisTemplate = stringRedisTemplate;
        this.redissonClient = redissonClient;

        // 初始化自增整数
        String numberValue = stringRedisTemplate.opsForValue().get(numberKey);
        if (numberValue == null) {
            Integer initNumber = initNumberSupplier.get();
            initNumber = initNumber == null ? 0 : initNumber;
            // 保存到redis
            stringRedisTemplate.opsForValue().set(numberKey, String.valueOf(initNumber));
        }

        // 初始化年份
        stringRedisTemplate.opsForValue().setIfAbsent(YEAR_KEY, Year.now().toString());
    }

    /**
     * 获取下一个编号
     *
     * @return 编号
     */
    public String nextNumber() {
        // 加分布式锁
        RLock lock = redissonClient.getLock("lock:numberGenerate:" + prefix);
        lock.lock();
        try {
            return doNextNumber();
        } finally {
            if (lock.isLocked()) {
                lock.unlock();
            }
        }
    }

    private String doNextNumber() {
        int year = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get(YEAR_KEY)));
        int currentYear = LocalDateTime.now().getYear();
        if (currentYear > year) { // 跨年的情况
            year = currentYear;
            // 刷新redis缓存
            stringRedisTemplate.opsForValue().set(YEAR_KEY, String.valueOf(currentYear));
            stringRedisTemplate.opsForValue().set(numberKey, "0"); // 重新递增
        }
        Long number = stringRedisTemplate.opsForValue().increment(numberKey); // 编号递增 ++i
        return String.format(format, prefix, year, number);
    }
}

2 使用示例

创建一个审核单编号生成器,前缀为AUDIT,最少补全位数为4,将其注册为Spring Bean,在需要的地方依赖注入即可使用。

代码如下:

java 复制代码
import com.example.mapper.AuditMapper;

import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.Year;

import javax.annotation.Resource;

/**
 * <h2>审核单编号生成器</h2>
 */
@Component
public class AuditNoGenerator implements InitializingBean {
    // 编号生成器
    private NumberGenerator numberGenerator;

    // 审核单记录表Mapper
    @Resource
    private AuditMapper auditMapper;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redissonClient;

    private AuditNoGenerator() {
    }

    /**
     * Bean初始化完成后执行
     */
    @Override
    public void afterPropertiesSet() {
        initNumberGenerator();
    }

    /**
     * 生成下一个编号
     *
     * @return 编号
     */
    public String nextNo() {
        return numberGenerator.nextNumber();
    }

    /**
     * 初始化编号生成器
     */
    private void initNumberGenerator() {
        numberGenerator = new NumberGenerator("AUDIT", 4,
            () -> auditMapper.selectMaxAuditNumByYear(Year.now().getValue()), stringRedisTemplate, redissonClient);
    }
}

auditMapper.selectMaxAuditNumByYear(Year.now().getValue())是自定义的数据库查询方法,用于获取某个年份的审核单号的最大值,比如:今年(2025)的最大编号为AUDIT-2025-1201,则返回1201。

selectMaxAuditNumByYear的方法定义:

java 复制代码
/**
 * 获取某个年份的审核单号的最大值
 * <p>
 * 比如:2023年的最大编号为AUDIT-2023-1986,则返回1986
 *
 * @param year 年份
 * @return 某个年份的审核单号的最大值
 */
Integer selectMaxAuditNumByYear(int year);

对应的Mybatis mapper xml代码如下:

xml 复制代码
<select id="selectMaxAuditNumByYear" resultType="java.lang.Integer">
    <!-- 截取AUDIT-yyyy-xxxx 的 xxxx -->
    select MAX(CONVERT(SUBSTR(audit_no, 12), UNSIGNED))
    from t_audit
    where audit_no like concat('AUDIT-', #{year}, '%')
</select>

NumberGenerator的initNumberSupplier可以根据自身情况配置,本例使用数据库查询获取

使用方式

使用方式如下:

java 复制代码
@Service
public class AuditService {
    @Resource // 注入依赖
    private AuditNoGenerator auditNoGenerator;
    
    public void createAudit() {
        // 模拟创建审核单
        AuditEntity auditEntity = new AuditEntity();
        auditEntity.setAuditNo(auditNoGenerator.nextNo()); // 生成单号
        // ...
    }
}
相关推荐
又是忙碌的一天21 分钟前
Java IO流
java·开发语言
程序员buddha23 分钟前
springboot-mvc项目示例代码
java·spring boot·mvc
不懂英语的程序猿1 小时前
【Java 工具类】Java通过 TCP/IP 调用斑马打印机(完整实现)
java
你的人类朋友2 小时前
✍️记录自己的git分支管理实践
前端·git·后端
像风一样自由20202 小时前
Go语言入门指南-从零开始的奇妙之旅
开发语言·后端·golang
多多*2 小时前
分布式系统中的CAP理论和BASE理论
java·数据结构·算法·log4j·maven
sg_knight3 小时前
Docker 实战:如何限制容器的内存使用大小
java·spring boot·spring·spring cloud·docker·容器·eureka
合作小小程序员小小店3 小时前
web网页开发,在线考勤管理系统,基于Idea,html,css,vue,java,springboot,mysql
java·前端·vue.js·后端·intellij-idea·springboot
间彧4 小时前
SpringBoot + MyBatis-Plus + Dynamic-Datasource 读写分离完整指南
数据库·后端
间彧4 小时前
数据库读写分离下如何解决主从同步延迟问题
后端