你做过的系统里一定有这个场景,生成一个业务单号,这个业务编号要求全局唯一,由前缀+日期+5位数字递增组成
方案一:缓存一天时间,不需要查询数据库
@Getter
@AllArgsConstructor
public enum BusinessTypeEnum {
TRANSPORT_ENTER("yyyyMMdd", "TYJ", "hmy-tms:transport-enter-no:"),
TRANSPORT_EXIT("yyyyMMdd", "TYT", "hmy-tms:transport-exit-no:"),
RECEIVE_ADJUST("yyyyMMdd", "YSTZ", "hmy-finance:receive-adjust-no:");
/**
* 日期格式
*/
private final String pattern;
/**
* 前缀
*/
private final String prefix;
/**
* 缓存key
*/
private final String cacheKey;
}
package com.hmy.generator.service;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import com.hmy.generator.common.enums.BusinessTypeEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.support.atomic.RedisAtomicLong;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Objects;
/**
* 编码生成器
*/
@Service
public class CommonNoGenerator {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 获取单号
*
* @param enums 业务枚举
* @return 业务单号
*/
public String getNextNo(BusinessTypeEnum enums) {
String currentDate = DateUtil.format(new Date(), DatePattern.createFormatter(enums.getPattern()));
RedisAtomicLong aLong = new RedisAtomicLong(enums.getCacheKey() + currentDate,
Objects.requireNonNull(redisTemplate.getConnectionFactory()));
long andIncrement = aLong.getAndIncrement();
//当天过期
aLong.expireAt(DateUtil.endOfDay(new Date()));
return enums.getPrefix() + currentDate + String.format("%04d", andIncrement);
}
}
方案二:缓存一段时间,需要查询数据库
package com.hmy.generator.service;
import com.hmy.generator.common.constants.Constant;
import com.hmy.generator.common.enums.DocumentTypeEnum;
import com.hmy.generator.common.utils.GeneratorRedisUtil;
import com.hmy.generator.dal.mapper.GeneratorMapper;
import com.hmy.generator.dal.po.GeneratorPo;
import com.mysql.cj.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@Service
public class BusinessNoGenerator {
private static final Logger log = LoggerFactory.getLogger(BusinessNoGenerator.class);
@Resource
private GeneratorMapper generatorMapper;
@Resource
private GeneratorRedisUtil redisUtil;
public String generatorNo(DocumentTypeEnum enums) {
String businessNo = null;
// 获取锁
if (redisUtil.acquireLock(Constant.BUSINESS_NO_LOCK_PREFIX + enums.getTableName(), enums.getFieldName(),
Constant.LOCK_TIME_LENGTH)) {
businessNo = redisUtil.get(Constant.BUSINESS_NO_KEY_PREFIX + enums.getTableName());
// 缓存为空
if (StringUtils.isNullOrEmpty(businessNo)) {
businessNo = generatorMapper.queryBusinessNo(buildGeneratorPo(enums));
// 数据库为空
if (StringUtils.isNullOrEmpty(businessNo)) {
businessNo = enums.getPrefix() + getLocalDate(enums.getRule()) + Constant.DEFAULT_SUFFIX;
} else {
businessNo = getNewBusinessNo(businessNo, enums);
}
} else {
businessNo = getNewBusinessNo(businessNo, enums);
}
redisUtil.set(Constant.BUSINESS_NO_KEY_PREFIX + enums.getTableName(), businessNo,
Constant.CACHE_TIME_LENGTH);
redisUtil.del(Constant.BUSINESS_NO_LOCK_PREFIX + enums.getTableName());
} else {
// 休眠之后,再次尝试获取锁
try {
Thread.sleep(Constant.SLEEP_TIME);
} catch (InterruptedException e) {
log.error("generatorNo exception", e);
throw new RuntimeException(e);
}
for (int i = 1; i < Constant.RETRY_TIME; i++) {
businessNo = generatorNo(enums);
if (!StringUtils.isNullOrEmpty(businessNo)) {
break;
}
}
if (StringUtils.isNullOrEmpty(businessNo)) {
throw new RuntimeException("获取锁失败,稍后再重试");
}
}
return businessNo;
}
/**
* 在原有的业务编码基础上生成新的
*
* @param businessNo 业务编码
* @param enums 业务枚举
*/
private String getNewBusinessNo(String businessNo, DocumentTypeEnum enums) {
String newBusinessNo;
String dateStr = businessNo.substring(enums.getPrefix().length(),
enums.getRule().length() + enums.getPrefix().length());
// 判断是不是当天
if (isCurrentDay(dateStr, enums.getRule())) {
businessNo = businessNo.substring(enums.getPrefix().length() + enums.getRule().length());
int no = Integer.parseInt(businessNo) + 1;
// 补全前面缺失的0
String num = String.format("%04d", no);
newBusinessNo = enums.getPrefix() + getLocalDate(enums.getRule()) + num;
redisUtil.set(Constant.BUSINESS_NO_KEY_PREFIX + enums.getTableName(), newBusinessNo,
Constant.CACHE_TIME_LENGTH);
return newBusinessNo;
} else {
newBusinessNo = enums.getPrefix() + getLocalDate(enums.getRule()) + Constant.DEFAULT_SUFFIX;
}
return newBusinessNo;
}
/**
* 构造GeneratorPo对象
*
* @param enums 枚举
* @return generatorPo
*/
private GeneratorPo buildGeneratorPo(DocumentTypeEnum enums) {
GeneratorPo generatorPo = new GeneratorPo();
generatorPo.setTableName(enums.getTableName());
generatorPo.setFieldName(enums.getFieldName());
generatorPo.setRule(enums.getRule());
generatorPo.setPrefix(enums.getPrefix());
return generatorPo;
}
/**
* @param pattern 日期格式
* @return 日期为字符串
*/
private String getLocalDate(String pattern) {
LocalDate currentDate = LocalDate.now();
// 设置日期格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
// 格式化日期为字符串
return currentDate.format(formatter);
}
/**
* 判断字符串是否是当天
*
* @param date 字符串日期
* @param pattern 日期格式
* @return true/false
*/
private boolean isCurrentDay(String date, String pattern) {
LocalDate currentDate = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
currentDate.format(formatter);
return currentDate.format(formatter).equals(date);
}
}
显然方案一比方案二简单很多
那么设计一个id生成器或者单号生成器我们要考虑哪些东西呢?我罗列了以下点
1、全局唯一
2、三高:高性能、高可靠、高并发这都依赖于redis实现安全性能和可靠问题
3、可读性要好
4、基因编辑,可以考虑在单号里加入业务信息
5、长度不要太长、方便记忆
6、灵活、方便扩展