背景
项目需要序号服务,该序号由标志字母+日期+自增数字。所以,考虑使用redis和mysql的方式去实现它。获取序号通过redis,可以保证线程安全。然后号段通过mysql去操作,对mysql的操作通过redisson分布式锁保证分布式一致性。
1,所需环境
- redis和redisson分布式锁
- mysql
- mybatisPlus
2,数据库表
CREATE TABLE `t_redis_sequence` (
`biz_tag` varchar(64) NOT NULL COMMENT '业务标签',
`prefix_abc` varchar(16) NOT NULL DEFAULT '' COMMENT '最开头 字母前缀',
`prefix_date` varchar(16) NOT NULL DEFAULT '' COMMENT '日期前缀',
`length` int(11) NOT NULL DEFAULT '7' COMMENT '字段长度',
`current_id` bigint(20) NOT NULL DEFAULT '1' COMMENT '当前编号',
`refresh` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否每日刷新,1 刷新 0 不刷新',
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='redis取流水号表'
3,业务实现代码
@Component
@Slf4j
public class RedisSeqGenerator {
@Autowired
private TmsRedisUtils tmsRedisUtils;
@Autowired
private RedisSequenceMapper redisSequenceMapper;
@Autowired
private RedissonClient redisson;
/**
* 每次缓存的数量
*/
private static final int SECTION_WIDTH = 10000;
/**
* 获取流水号
*/
public String getNextSeq(RedisSeqEnums redisSeqEnums) {
String bizTag = redisSeqEnums.getBizTag();
String id =tmsRedisUtils.listleftPop(RedisKeyConstant.TMS_SEQUENCE_PREFIX + bizTag);
if(id == null){
Long current = 0L;
RedisSequence seq = null;
RLock lock = redisson.getLock(RedisKeyConstant.TMS_REDIS_LOCK_PREFIX + bizTag);
try{
lock.lock();
//二次 判断
id = tmsRedisUtils.listleftPop(RedisKeyConstant.TMS_SEQUENCE_PREFIX + bizTag);
if(id != null){
return id;
}
//从数据库获取seq
seq = redisSequenceMapper.selectById(bizTag);
if(seq == null){
seq = new RedisSequence();
seq.setCurrentId(1L);
seq.setBizTag(bizTag);
seq.setPrefixAbc(redisSeqEnums.getPrefixAbc());
seq.setLength(redisSeqEnums.getLength());
seq.setRefresh(redisSeqEnums.getRefresh());
seq.setPrefixDate(LocalDateTime.now().format(TimeConstant.FORMATTER));
redisSequenceMapper.insert(seq);
}
current = seq.getCurrentId();
seq.setCurrentId(seq.getCurrentId() + SECTION_WIDTH);
redisSequenceMapper.updateById(seq);
//存入redis缓存
cache(bizTag, seq,current);
id = tmsRedisUtils.listleftPop(RedisKeyConstant.TMS_SEQUENCE_PREFIX + bizTag);
}finally {
lock.unlock();
}
}
return id;
}
/** 存入redis缓存 */
private void cache(String bizTag, RedisSequence seq,Long current) {
List<String> ids = new ArrayList<>();
for(long i = current;i<current + SECTION_WIDTH ;i++){
ids.add(seq.getPrefixAbc() + seq.getPrefixDate()+ addZeroToSeq(i,seq.getLength()));
}
tmsRedisUtils.listRightPushAll(RedisKeyConstant.TMS_SEQUENCE_PREFIX + bizTag,ids);
}
/** 在前面加0 */
private String addZeroToSeq(long currentId, int length){
String c = String.valueOf(currentId);
length = length - c.length();
if(length > 0){
for(int i = 0; i< length;i++){
c = "0" + c;
}
}
return c;
}
}
4,所依赖的枚举类,实体类
@Getter
public enum RedisSeqEnums {
订单号流水号("orderNoSeq", "T",7,1),
运单号流水号("waybillNoSeq", "W",8,1),
承运商流水号("carrierNoSeq", "SUP",3,1),
短信验证码流水号("smsReqIdSeq", "",12,1)
;
/**业务类型*/
private String bizTag;
/**最开头 字母前缀*/
private String prefixAbc;
/**长度*/
private Integer length;
/**是否每日刷新,1 刷新 0 不刷新*/
private Integer refresh;
RedisSeqEnums(String bizTag, String prefixAbc, Integer length, Integer refresh) {
this.bizTag = bizTag;
this.prefixAbc = prefixAbc;
this.length = length;
this.refresh = refresh;
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_redis_sequence")
@Accessors(chain = true)
public class RedisSequence implements Serializable {
/**业务标签*/
@TableId
private String bizTag;
/**最开头 字母前缀*/
private String prefixAbc;
/**日期前缀*/
private String prefixDate;
/**当前编号*/
private Long currentId;
/**长度*/
private Integer length;
/**是否每日刷新,1 刷新 0 不刷新*/
private Integer refresh;
}
5,序号的每日刷新
我通过定时任务的方式去刷新日期字符串,大家可以用的定时组件不一致,我这边只提供代码
//修改数据库中的时间日期字符串
String dateStr = LocalDateTime.now().format(TimeConstant.FORMATTER);
List<RedisSequence> list = redisSequenceMapper.selectCurrentIdByRefresh();
if(list != null && list.size()>0){
list.forEach(v ->{
v.setCurrentId(1L);
v.setPrefixDate(dateStr);
redisSequenceMapper.updateById(v);
});
}
//清空redis缓存的流水号
for (RedisSeqEnums value : RedisSeqEnums.values()) {
tmsRedisUtils.lTrim(RedisKeyConstant.TMS_SEQUENCE_PREFIX + value.getBizTag(),1,0);
}
总结
我这边只是提供了一种实现方式,大家可以参考。