背景
项目中需要生成一些特殊的单据编号,例如报销申请单、样品取货单等,这类单据编号通常具有固定的格式:前缀+当前时间(年月日)+5位序号,且这种单据编号具有如下特性:
全局唯一
生成单据编号不允许重复。
递增
单据编号需要成递增的趋势。
实现方案
生成分布式单据编号的实现方案比较多,通常可以采用雪花算法来实现,但是针对这种特殊的单据编号生成,雪花算法并不一定合适,所以需要针对业务实现一个特殊的单据编号生成服务,我们可以采用如下的解决方案。
- 1.数据库方案
- 2.Redis方案
数据库方案
该方案通过数据库的来保证单据唯一。
实现流程
流程说明:
-
新增序号,数据库根据前缀+日期设置唯一索引,由于并发问题会产生相同的数据,插入异常,执行更新操作。
-
修改序号,数据库采用的乐观锁,由于并发问题会导致更新失败。
表结构设计
优缺点
优点
- 基于数据库实现简单
缺点
- 基于乐观锁实现,针对高并发场景出错的机率比较高。
方案优化
针对此方案的缺点,在更新序号出错的时,提供重试机制,当达到错误重试的最大次数时,才更新失败。
Redis方案
Redis方案是基于incr和increby是自增的原子命令实现,因为Redis执行命令是单线程,所以能保证生成的单据编号唯一有序。
核心实现
@Component
public class NumberUtil
{
private static final Logger logger = LoggerFactory.getLogger(RedisUtil.class);
public static final String DATE_PATTERN = "yyyyMMdd";
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public Long getIncrNumber(String key,long alive)
{
RedisAtomicLong entityIdCounter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
Long incrNum = entityIdCounter.getAndIncrement();
if (null == incrNum || incrNum.longValue()==0)
{
entityIdCounter.expire(alive,TimeUnit.MILLISECONDS);
incrNum = entityIdCounter.getAndIncrement();
}
return incrNum;
}
/**
* 生成订单号
* @param prefix
* @return
*/
public String generateOrderNo(String prefix,String key,long alive,int length)
{
logger.info("generateOrderNo prefix:{},key:{},alive:{},length:[}",prefix,key,alive,length);
//当前时间
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_PATTERN);
String currentDate=formatter.format(LocalDateTime.now());
long indexValue=getIncrNumber(key,alive);
return String.format("%s%s%0"+length+"d",prefix,currentDate,indexValue);
}
}
测试示例
@RequestMapping("/generateOrderNo")
public void generateOrderNo()
{
long alive = 1000;
int length=5;
String key ="order:orderNo";
for(int i=0;i<10;i++)
{
String orderNo=numberUtil.generateOrderNo("TS", key, alive, length);
System.out.println("orderNo:"+orderNo);
}
}
优缺点
优点
- 实现简单,高性能、高并发。
- 扩展性强,由于单机Redis性能存在瓶颈,可以根据业务需求通过Redis集群扩展实现。
缺点
- 需要引进Redis组件,增加了系统的复杂性。
- Redis出现异常,导致生成的单据编号异常。
方案优化
Redis生成的序号成功后,将序号保存到数据库。Reids出现了异常,可以从数据库中获取序号。
总结
本文讲解了生成特殊单据编号的几种方案,需要更加业务的并发量和场景选择合适的方案,如有问题请随时反馈。