diff --git a/nl-module-base/nl-module-base-server/pom.xml b/nl-module-base/nl-module-base-server/pom.xml index ef5da7d2..01eee04e 100644 --- a/nl-module-base/nl-module-base-server/pom.xml +++ b/nl-module-base/nl-module-base-server/pom.xml @@ -46,6 +46,10 @@ cn.nl.cloud nl-spring-boot-starter-biz-ip + + cn.nl.cloud + nl-spring-boot-starter-protection + diff --git a/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/SequenceAllocator.java b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/SequenceAllocator.java new file mode 100644 index 00000000..852dab90 --- /dev/null +++ b/nl-module-base/nl-module-base-server/src/main/java/cn/code/nl/module/base/service/codegen/SequenceAllocator.java @@ -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 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; + } + +}