分布式下,获取单号有多种方式。
1.UUID 乱序,且很长,不利于数据库做索引查询 和 空间浪费
2.数据库自增序列,每次都要访问数据库,IO开销大,高并发时几乎不可用
3.Redis 自增,优点是速度快,缺点是持久化不可靠,有可能造成重复单号
本文介绍获取单号的特点是:
1.局部自增,前缀可添加业务属性的字符串
2.利用数据库持久化序列号值,保证可靠性
3.利用Redis 的 List 结构进行批量缓存序列号池 , 保证取号效率
优点: 兼顾了持久化和支持高并发
缺点: 当Redis序列号耗尽,需要加同步锁控制生成新的序列号缓存池,这里有一定的性能损失
- 数据库表结构
CREATE TABLE `my_sequence` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`seq_name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '序列名字',
`current_val` bigint(20) unsigned NOT NULL COMMENT '当前值',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='序列表';
- 持久化序列号的JAVA实体
/**
* 序列表
*
*/
@Data
@TableName("my_sequence")
public class MySequence {
private static final long serialVersionUID = 1L;
/**
* 自增主键
*/
@TableId
private Long id;
/**
* 序列名字
*/
private String seqName;
/**
* 当前值
*/
private Long currentVal;
}
- 工具类 (spring + RedisTemplate + Redission + mybatis(本例子用了mybatisPlus))
/**
* 序列号生成器
*/
@Component
@AllArgsConstructor
public class SequenceUtils {
private static final String PLATFORM_NO_LOCK_KEY = "PLATFORM_NO_LOCK_";
private static final String PLATFORM_NO_STACK_KEY = "PLATFORM_NO_STACK_KEY";
// 每次存进Redis 缓存池的序列号数量,根据业务量和单号长度设计进行合理安排
private static final Long SEQ_VAL_STEP_SIZE = 500L;
// 用于 操作 redis - 存取 序列号
private final RedisTemplate redisTemplate;
// 操作记录序列号数据库的 service
private final SequenceService sequenceService;
// Redission - 用于 分布式锁
private final DistributedLock distributedLock;
/**
* 从redis获取已经生成好的序号
* 不加入业务的事务,避免随业务回滚,序列号只增加,不回滚
*
* @param seqType 业务类型标志 - 数据库存了多个业务类型的序列号记录
* @return
*/
@Transactional(rollbackFor = Exception.class, propagation = Propagation.NOT_SUPPORTED)
public String getPlatformNoRedis(String seqType) {
Long nextSeqVal = (Long) redisTemplate.opsForList().leftPop(PLATFORM_NO_STACK_KEY);
if (null == nextSeqVal) {
nextSeqVal = pushAndGetSeqVal(seqType);
}
// 指定业务类型前缀,这里 D 为例子
return formatStrNo("D", String.valueOf(nextSeqVal), null);
}
/**
* 如果 redis 序号已经用完,则查库追加 指定步长数量的序号
*
* @param seqType 业务类型标志 - 数据库存了多个业务类型的序列号记录
* @return {@link Long}
*/
private Long pushAndGetSeqVal(String seqType) {
Long nextSeqVal;
// 第一个本地同步锁,防止分布式锁过多的争抢
synchronized (SequenceUtils.class) {
nextSeqVal = (Long) redisTemplate.opsForList().leftPop(PLATFORM_NO_STACK_KEY);
if (null == nextSeqVal) {
nextSeqVal = distributedLock.locked(PLATFORM_NO_LOCK_KEY + seqType, () -> {
Long nextPlatformNoTemp = (Long) redisTemplate.opsForList().leftPop(PLATFORM_NO_STACK_KEY);
if (null == nextPlatformNoTemp) {
// 取出指定的序列号持久化数据
// 这里用了 mybatisPlus 的模板代码,可换成自己的ORM取数据方式
MySequence sequence = sequenceService
.getOne(new LambdaQueryWrapper<MySequence>().eq(MySequence::getSeqName, seqType));
// 如果序列号还没有初始化,则进行初始化
MySequence lSequence = Optional.ofNullable(sequence).orElseGet(() -> {
MySequence mySequence = new MySequence();
mySequence.setSeqName(seqType);
mySequence.setCurrentVal(0L);
sequenceService.save(mySequence);
return mySequence;
});
Long oldVal = lSequence.getCurrentVal();
lSequence.setCurrentVal(oldVal + SEQ_VAL_STEP_SIZE);
// 先持久化性新的序列号值 - 这里必须能接受redis上传序列号失败造成的部分序列号浪费,
// 所以 SEQ_VAL_STEP_SIZE 步长不要太大,不然可能提前耗尽序列号
sequenceService.updateById(lSequence);
// 利用 redis 的管道批量上传序列号
redisTemplate.executePipelined(new SessionCallback<Long>() {
@Override
public <K, V> Long execute(RedisOperations<K, V> operations) throws DataAccessException {
RedisTemplate<String, Long> thisRedisTemplate = (RedisTemplate<String, Long>) operations;
// 在步长范围内,递增+1
for (int i = 1; i <= SEQ_VAL_STEP_SIZE; i++) {
thisRedisTemplate.opsForList().rightPush(PLATFORM_NO_STACK_KEY, oldVal + i);
}
return null;
}
});
// 上传之后,取出序列号
nextPlatformNoTemp = (Long) redisTemplate.opsForList().leftPop(PLATFORM_NO_STACK_KEY);
}
return nextPlatformNoTemp;
});
}
}
return nextSeqVal;
}
/**
* 格式化序列号
*
* @param type 业务类型
* @param sequence 自然数序列
* @param str 有值就是前缀+str+自然数五位,没有就是str用日期
* @return
*/
public String formatStrNo(String type, String sequence, String str) {
String pregfix = type.toUpperCase();
// 这里取了序列的指定长度 ,按照需求设置合理的值,不然有可能提前耗尽序列号
sequence = String.format("%05d", Integer.valueOf(sequence));
if (sequence.length() > 5) {
sequence = StrUtil.sub(sequence, sequence.length() - 5, sequence.length());
}
String result = LocalDate.now().format(DateTimeFormatter.ofPattern("yyMMdd"));
if (StrUtil.isNotEmpty(str)) {
result = str;
}
String template = "{}" + result + "{}";
return StrUtil.format(template, pregfix, sequence);
}
}
标签:String,sequence,单号,private,获取,Long,序列号,seqType,分布式
From: https://www.cnblogs.com/jicheng999/p/16841197.html