本文介绍一款在分布式环境下的自增编号生成器的设计与代码实现,可实现在分布式环境下的编号递增且唯一。 主要基于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()); // 生成单号
// ...
}
}