| | |
| | | import org.springframework.data.redis.core.script.RedisScript; |
| | | import org.springframework.stereotype.Repository; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | /** |
| | |
| | | * 袋序号自增 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 脚本。 |
| | |
| | | } |
| | | |
| | | /** |
| | | * 批量生成 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」得到,并发生成时两个请求会读到同一个最大值, |