[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()); // 生成单号
        // ...
    }
}
相关推荐
用户685453759776915 小时前
SQL优化完全指南:让你的数据库从"蜗牛"变"猎豹"!🐌➡️🐆
后端
大巨头15 小时前
豆包帮忙梳理知识点,真强大!
后端
ZhengEnCi15 小时前
JPA-SQL 语句使用完全指南-自动生成vs手动编写的智能选择策略
java·spring boot·sql
疯狂的程序猴15 小时前
Vue前端开发工具大全,从编码到调试的高效工作流指南
后端
渣哥15 小时前
别再乱用了!Spring AOP 与 AspectJ 的区别比你想的复杂
javascript·后端·面试
毕设源码-钟学长15 小时前
【开题答辩全过程】以 菜谱分享平台为例,包含答辩的问题和答案
java·eclipse
hui函数15 小时前
Python全栈(基础篇)——Day10:后端内容(map+reduce+filter+sorted+实战演示+每日一题)
后端·python
可DRAK鸦|・ω・`)15 小时前
docker后端jar包本地构建镜像
java·docker·容器·jar
hui函数15 小时前
Python全栈(基础篇)——Day13:后端内容(模块详解)
后端·python