feat: 新增 SequenceAllocator 号段预分配器

- 创建 SequenceAllocator 组件,支持号段预分配和分布式锁实时两种策略
- 使用 @Lock4j 注解实现分布式锁,通过自注入模式解决 Spring AOP 内部调用问题
- 本地 ConcurrentHashMap 缓存号段,AtomicLong 做内存 CAS 递增
- 补位格式化支持自定义补位字符和长度
- 重置维度支持 DAY/MONTH/YEAR/GLOBAL
- 添加 nl-spring-boot-starter-protection 依赖
This commit is contained in:
zhouz
2026-07-15 17:07:03 +08:00
parent c77bfd5eaf
commit 24e407585f
2 changed files with 196 additions and 0 deletions

View File

@@ -46,6 +46,10 @@
<groupId>cn.nl.cloud</groupId>
<artifactId>nl-spring-boot-starter-biz-ip</artifactId>
</dependency>
<dependency>
<groupId>cn.nl.cloud</groupId>
<artifactId>nl-spring-boot-starter-protection</artifactId>
</dependency>
<!-- 需要时候自行引入 -->
<!-- Web 相关 -->

View File

@@ -0,0 +1,192 @@
package cn.code.nl.module.base.service.codegen;
import cn.code.nl.module.base.dal.dataobject.codegen.CodeSequenceDO;
import cn.code.nl.module.base.dal.mysql.codegen.CodeSequenceMapper;
import cn.code.nl.module.base.service.codegen.dto.SequenceSegment;
import cn.code.nl.module.base.service.codegen.enums.ResetByEnum;
import cn.hutool.core.util.StrUtil;
import com.baomidou.lock.annotation.Lock4j;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.Year;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.code.nl.module.base.enums.ErrorCodeConstants.CODE_GEN_SEQ_EXCEED_MAX;
/**
* 序号分配器 —— 号段预分配 + 分布式锁实时两种策略
*
* @author zhouz
*/
@Slf4j
@Component
public class SequenceAllocator {
/**
* 默认号段预取步长
*/
private static final int DEFAULT_STEP = 100;
/**
* 本地号段缓存key = "ruleId:resetKey"value = 当前分配位置
* 配合 AtomicLong 做内存 CAS 递增
*/
private final ConcurrentHashMap<String, AtomicLong> localSegmentCache = new ConcurrentHashMap<>();
@Resource
private CodeSequenceMapper codeSequenceMapper;
/**
* 自注入,用于 Spring AOP 代理调用带 @Lock4j 的方法
*/
@Resource
@Lazy
private SequenceAllocator self;
/**
* 分配一个序号(号段预分配策略)
*
* @param ruleId 规则ID
* @param segment 序号段配置
* @return 格式化后的序号字符串
*/
public String allocateBySegment(Long ruleId, SequenceSegment segment) {
String resetKey = buildResetKey(segment.getResetBy());
String cacheKey = ruleId + ":" + resetKey;
// 从本地缓存取号段
AtomicLong current = localSegmentCache.get(cacheKey);
if (current != null) {
long seqValue = current.incrementAndGet();
// 检查号段是否还有效 - 从 DB 查当前号段末尾
CodeSequenceDO seqRecord = codeSequenceMapper.selectByRuleIdAndResetKey(ruleId, resetKey);
if (seqRecord != null && seqValue <= seqRecord.getCurrentValue()) {
return formatSequence(seqValue, segment);
}
// 号段耗尽,清理缓存
localSegmentCache.remove(cacheKey);
}
// 号段耗尽或不存在,通过代理调用带 @Lock4j 的方法从 DB 预取
String lockKey = "codegen:seq:segment:" + ruleId + ":" + resetKey;
long seqValue = self.fetchSegmentWithLock(lockKey, cacheKey, ruleId, resetKey, segment);
return formatSequence(seqValue, segment);
}
/**
* 分配一个序号(分布式锁实时策略,每次取一个号)
*
* @param ruleId 规则ID
* @param segment 序号段配置
* @return 格式化后的序号字符串
*/
public String allocateByLock(Long ruleId, SequenceSegment segment) {
String resetKey = buildResetKey(segment.getResetBy());
String lockKey = "codegen:seq:lock:" + ruleId + ":" + resetKey;
return self.allocateWithLock(lockKey, ruleId, resetKey, segment);
}
/**
* Lock4j 保护下从 DB 预取号段
*/
@Lock4j(keys = "#lockKey", acquireTimeout = 3000, expire = 5000)
public long fetchSegmentWithLock(String lockKey, String cacheKey, Long ruleId,
String resetKey, SequenceSegment segment) {
// double-check其他线程可能已经预取了
AtomicLong existing = localSegmentCache.get(cacheKey);
if (existing != null) {
return existing.incrementAndGet();
}
Long startAt = segment.getStartAt() != null ? segment.getStartAt() : 1L;
Long maxValue = segment.getMaxValue() != null ? segment.getMaxValue() : 9999L;
// DB 原子更新current_value = current_value + step
codeSequenceMapper.updateCurrentValueBySegment(ruleId, resetKey, DEFAULT_STEP, startAt, maxValue);
// 查回最新 current_value
CodeSequenceDO seq = codeSequenceMapper.selectByRuleIdAndResetKey(ruleId, resetKey);
Long newCurrentValue = (seq != null) ? seq.getCurrentValue() : (startAt + DEFAULT_STEP - 1);
long segmentStart = newCurrentValue - DEFAULT_STEP + 1;
// 检查上限
if (newCurrentValue > maxValue) {
log.error("序号超出最大值上限ruleId={}, resetKey={}, currentValue={}, maxValue={}",
ruleId, resetKey, newCurrentValue, maxValue);
throw exception(CODE_GEN_SEQ_EXCEED_MAX);
}
// 缓存号段到本地AtomicLong 从 segmentStart - 1 开始,首次递增得到 segmentStart
AtomicLong counter = new AtomicLong(segmentStart - 1);
localSegmentCache.put(cacheKey, counter);
log.info("预取号段成功ruleId={}, resetKey={}, 号段区间=[{}, {}]",
ruleId, resetKey, segmentStart, newCurrentValue);
return counter.incrementAndGet();
}
/**
* Lock4j 保护下实时分配序号
*/
@Lock4j(keys = "#lockKey", acquireTimeout = 3000, expire = 5000)
public String allocateWithLock(String lockKey, Long ruleId, String resetKey, SequenceSegment segment) {
Long startAt = segment.getStartAt() != null ? segment.getStartAt() : 1L;
// step=1 每次只取一个号
codeSequenceMapper.updateCurrentValueBySegment(ruleId, resetKey, 1, startAt, segment.getMaxValue());
// 查回当前值
CodeSequenceDO seq = codeSequenceMapper.selectByRuleIdAndResetKey(ruleId, resetKey);
Long value = (seq != null) ? seq.getCurrentValue() : startAt;
// 检查上限
if (segment.getMaxValue() != null && value > segment.getMaxValue()) {
log.error("序号超出最大值上限ruleId={}, resetKey={}, currentValue={}, maxValue={}",
ruleId, resetKey, value, segment.getMaxValue());
throw exception(CODE_GEN_SEQ_EXCEED_MAX);
}
return formatSequence(value, segment);
}
/**
* 根据重置维度构建 resetKey
*
* @param resetBy 重置维度
* @return 重置键
*/
public String buildResetKey(String resetBy) {
if (StrUtil.isBlank(resetBy)) {
return ResetByEnum.GLOBAL.getCode();
}
ResetByEnum resetByEnum = ResetByEnum.valueOf(resetBy);
return switch (resetByEnum) {
case DAY -> LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
case MONTH -> LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
case YEAR -> String.valueOf(Year.now().getValue());
case GLOBAL -> "GLOBAL";
};
}
/**
* 补位格式化序号
*
* @param value 序号值
* @param segment 序号段配置
* @return 格式化后的字符串
*/
public String formatSequence(long value, SequenceSegment segment) {
String raw = String.valueOf(value);
int paddingLen = segment.getPaddingLen() != null ? segment.getPaddingLen() : 0;
if (paddingLen <= 0 || raw.length() >= paddingLen) {
return raw;
}
String paddingChar = StrUtil.isNotBlank(segment.getPaddingChar())
? segment.getPaddingChar() : "0";
return paddingChar.repeat(paddingLen - raw.length()) + raw;
}
}