feat: 新增 SequenceAllocator 号段预分配器
- 创建 SequenceAllocator 组件,支持号段预分配和分布式锁实时两种策略 - 使用 @Lock4j 注解实现分布式锁,通过自注入模式解决 Spring AOP 内部调用问题 - 本地 ConcurrentHashMap 缓存号段,AtomicLong 做内存 CAS 递增 - 补位格式化支持自定义补位字符和长度 - 重置维度支持 DAY/MONTH/YEAR/GLOBAL - 添加 nl-spring-boot-starter-protection 依赖
This commit is contained in:
@@ -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 相关 -->
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user