获取单号的几种方式:
- UUID 乱序,且很长,不利于数据库做索引查询 和 空间浪费。
- 数据库自增序列,每次都要访问数据库,IO开销大,高并发时几乎不可用。
- Redis 自增,优点是速度快,缺点是持久化不可靠,有可能造成重复单号。
- 结合 数据库 和 redis 可以获得持久化和性能的双重优势,缺点是依赖服务器时间,如果服务器时间回调,会出现重复单号,另外依赖了额外的中间件。这是本文想要具体介绍的一种方式。
示例环境:
中间件: MySQL + Redis Java 工具(必要): 分布式锁(示例中是使用了封装后的Redission,实际可自己实现分布式锁或使用Redission)
思路流程图:
代码:
- 数据库表结构
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;
}
- service类 (spring + RedisTemplate + Redission + mybatis(本例子用了mybatisPlus))
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
/** 这里省略了一些内部包,不影响代码逻辑,根据自己的业务替换 **/
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
@Service
public class SequenceServiceImpl extends ServiceImpl<MySequenceMapper, MySequence> implements SequenceService {
private static final String PLATFORM_NO_LOCK_KEY = "PLATFORM_NO_LOCK_";
private static final String PLATFORM_NO_STACK_KEY = "PLATFORM_NO_STACK_KEY";
private static final Long SEQ_VAL_STEP_SIZE = 500L;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private DistributedLock distributedLock;
/**
* 从redis获取已经生成好的序号
* 不加入业务的事务,避免随业务回滚,序列号只增加,不回滚
* @param c CommDeclType枚举类
* @return
*/
@Transactional(rollbackFor = Exception.class, propagation = Propagation.NOT_SUPPORTED)
@Override
public String getPlatformNoRedis(CommDeclType c) {
Long nextSeqVal = (Long) redisTemplate.opsForList().leftPop(PLATFORM_NO_STACK_KEY+ seqType);
if (null == nextSeqVal) {
nextSeqVal = pushAndGetSeqVal(c);
}
return formatStrNo(c.getRemarks(), 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+ seqType);
if (null == nextSeqVal) {
nextSeqVal = distributedLock.locked(PLATFORM_NO_LOCK_KEY + seqType, () -> {
Long nextPlatformNoTemp = (Long) redisTemplate.opsForList().leftPop(PLATFORM_NO_STACK_KEY+ seqType);
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+ seqType, oldVal + i);
}
return null;
}
});
// 上传之后,取出序列号
nextPlatformNoTemp = (Long) redisTemplate.opsForList().leftPop(PLATFORM_NO_STACK_KEY+ seqType);
}
return nextPlatformNoTemp;
});
}
}
return nextSeqVal;
}
/**
* 格式化序列号
*
* @param type 业务类型
* @param sequence 自然数序列
* @param str 有值就是前缀+str+自然数五位,没有就是str用日期
* @return
*/
public static 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);
}
}