[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()); // 生成单号
        // ...
    }
}
相关推荐
哈哈哈笑什么20 小时前
Spring Cloud分布式高并发系统下,订单数据(离线设备→云端)“同步不丢、不重、有序”的完整落地方案
后端
即将进化成人机20 小时前
Spring Boot入门
java·spring boot·后端
嘻哈baby20 小时前
微服务本地联调不再痛苦:多服务开发调试完整方案
后端
苏打水com20 小时前
HTML/CSS 核心考点详解(字节跳动 ToB 中台场景)
java·前端·javascript
-大头.20 小时前
Spring批处理与任务管理全解析
java·linux·spring
哈哈哈笑什么20 小时前
订单状态实时通知的生产级完整方案
后端
action191620 小时前
Nano Banana2API国内接入神方案!0.1元/次稳到哭
后端
无限进步_20 小时前
C++从入门到类和对象完全指南
开发语言·c++·windows·git·后端·github·visual studio
科普瑞传感仪器20 小时前
基于六维力传感器的机器人柔性装配,如何提升发动机零部件装配质量?
java·前端·人工智能·机器人·无人机