package cn.iocoder.yudao.module.mes.dal.redis;
|
|
import cn.iocoder.yudao.framework.common.util.number.BizNoUtils;
|
import jakarta.annotation.Resource;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
import org.springframework.data.redis.core.script.RedisScript;
|
import org.springframework.stereotype.Repository;
|
|
import java.util.ArrayList;
|
import java.util.List;
|
|
/**
|
* MES 业务编码生成的 Redis DAO(实例码:袋码 / 托盘码 / 临时码等)。
|
*
|
* <p>需求:所有业务编码统一为「16 位纯数字」,且全系统唯一。
|
* 这里复用全系统统一的 Redis 自增 KEY,保证与其它模块的编码也不重复。
|
*/
|
@Repository
|
public class MesCodeRedisDAO {
|
|
/**
|
* 袋序号自增 KEY(全局袋序号,保证同一批/跨批生成时不重复)
|
*/
|
private static final String BAG_NO_SEQ_KEY = "mes:bag_no:seq";
|
|
/**
|
* 单次批量取码的最大个数
|
* <p>
|
* 受 Redis 单条命令参数规模与响应体大小约束,过大反而增加单次阻塞时间。
|
*/
|
public static final int MAX_BATCH_SIZE = 5000;
|
|
/**
|
* 批量写入并返回实际生效的编码。
|
* <p>
|
* 逐个 SADD 在同一脚本内完成:与调用方逐条 SADD 相比,网络往返从 N 次降为 1 次;
|
* 同时因为脚本原子执行,并发调用之间也不会产生重复编码。
|
*/
|
private static final RedisScript<List> ADD_UNIQUE_CODES_SCRIPT = new DefaultRedisScript<>(
|
"local res = {} "
|
+ "for i = 1, #ARGV do "
|
+ " if redis.call('SADD', KEYS[1], ARGV[i]) == 1 then "
|
+ " res[#res + 1] = ARGV[i] "
|
+ " end "
|
+ "end "
|
+ "return res",
|
List.class);
|
|
/**
|
* 原子分配袋序号区间的 Lua 脚本。
|
* <p>
|
* 先按库内最大序号校正基准值(Redis 首次使用、或库内数据被直接导入导致基准落后时),
|
* 再原子自增并返回自增后的值。整个「校正 + 自增」在 Redis 单线程内完成,避免并发下互相覆盖计数。
|
* <p>
|
* KEYS[1] = 序号 KEY;ARGV[1] = 本次需要的个数;ARGV[2] = 库内当前最大序号
|
*/
|
private static final RedisScript<Long> ALLOCATE_BAG_NO_SCRIPT = new DefaultRedisScript<>(
|
"local cur = redis.call('GET', KEYS[1]) "
|
+ "local base = tonumber(ARGV[2]) "
|
+ "if (not cur) or (tonumber(cur) < base) then redis.call('SET', KEYS[1], base) end "
|
+ "return redis.call('INCRBY', KEYS[1], ARGV[1])",
|
Long.class);
|
|
@Resource
|
private StringRedisTemplate stringRedisTemplate;
|
|
/**
|
* 生成一个 16 位纯数字业务编码(全系统唯一)
|
*
|
* @return 16 位纯数字编码
|
*/
|
public String generate() {
|
// 随机生成 16 位纯数字编码,并用 Redis 集合查重,保证全系统唯一(不使用自增/顺序)
|
for (int i = 0; i < BizNoUtils.MAX_RETRY; i++) {
|
String code = BizNoUtils.randomCode();
|
Long added = stringRedisTemplate.opsForSet().add(BizNoUtils.USED_SET_KEY, code);
|
if (added != null && added > 0) {
|
return code;
|
}
|
}
|
throw new IllegalStateException("生成唯一业务编码失败,请重试");
|
}
|
|
/**
|
* 批量生成 count 个唯一的 16 位纯数字业务编码(全系统唯一)
|
* <p>
|
* 大批量生成时不要循环调用 {@link #generate()}:那会产生 N 次 Redis 往返,
|
* 几十万条即可把接口拖到分钟级。这里每批最多 {@link #MAX_BATCH_SIZE} 个候选码
|
* 一次脚本写入,碰撞(概率极低)的缺额再补一批。
|
*
|
* @param count 需要的编码个数
|
* @return 唯一编码列表,顺序无保证但数量等于 count
|
*/
|
public List<String> generateBatch(int count) {
|
if (count <= 0) {
|
return List.of();
|
}
|
List<String> result = new ArrayList<>(Math.min(count, 1024));
|
// 16 位随机码空间为 10^16,正常情况下一轮即可全部写入;限制轮次只用于兜底异常
|
for (int round = 0; round < 100 && result.size() < count; round++) {
|
int batchSize = Math.min(count - result.size(), MAX_BATCH_SIZE);
|
List<String> candidates = new ArrayList<>(batchSize);
|
for (int i = 0; i < batchSize; i++) {
|
candidates.add(BizNoUtils.randomCode());
|
}
|
List<?> added = stringRedisTemplate.execute(ADD_UNIQUE_CODES_SCRIPT,
|
List.of(BizNoUtils.USED_SET_KEY), candidates.toArray(new String[0]));
|
if (added != null) {
|
added.forEach(code -> result.add(String.valueOf(code)));
|
}
|
}
|
if (result.size() < count) {
|
throw new IllegalStateException("批量生成唯一业务编码失败,请重试");
|
}
|
return result;
|
}
|
|
/**
|
* 原子分配一段连续的袋序号,返回区间起始值。
|
* <p>
|
* 袋序号原先由「查库内最大值再 +1」得到,并发生成时两个请求会读到同一个最大值,
|
* 从而产出重复的袋序号(袋序号会印在标签上)。此处改为 Redis 原子自增分配,
|
* 并用库内当前最大值做基准校正,保证序号既不重复也不回退。
|
*
|
* @param count 本次需要的连续序号个数(须大于 0)
|
* @param dbMaxBagNo 数据库当前最大袋序号(含已逻辑删除记录,传 null 视为 0)
|
* @return 区间起始序号(含),可用区间为 [start, start + count - 1]
|
*/
|
public int allocateBagNoStart(int count, Integer dbMaxBagNo) {
|
if (count <= 0) {
|
throw new IllegalArgumentException("袋序号分配个数必须大于 0");
|
}
|
Long end = stringRedisTemplate.execute(ALLOCATE_BAG_NO_SCRIPT, List.of(BAG_NO_SEQ_KEY),
|
String.valueOf(count), String.valueOf(dbMaxBagNo == null ? 0 : dbMaxBagNo));
|
if (end == null) {
|
throw new IllegalStateException("分配袋序号失败,请重试");
|
}
|
return (int) (end - count + 1);
|
}
|
|
}
|