全局单据编号生成解决方案

874 阅读2分钟

背景

项目中需要生成一些特殊的单据编号,例如报销申请单、样品取货单等,这类单据编号通常具有固定的格式:前缀+当前时间(年月日)+5位序号,且这种单据编号具有如下特性:

全局唯一

生成单据编号不允许重复。

递增

单据编号需要成递增的趋势。

实现方案

生成分布式单据编号的实现方案比较多,通常可以采用雪花算法来实现,但是针对这种特殊的单据编号生成,雪花算法并不一定合适,所以需要针对业务实现一个特殊的单据编号生成服务,我们可以采用如下的解决方案。

  • 1.数据库方案
  • 2.Redis方案

数据库方案

该方案通过数据库的来保证单据唯一。

实现流程

生成订单编号.png

流程说明:

  • 新增序号,数据库根据前缀+日期设置唯一索引,由于并发问题会产生相同的数据,插入异常,执行更新操作。

  • 修改序号,数据库采用的乐观锁,由于并发问题会导致更新失败。

表结构设计

图片.png

优缺点

优点

  • 基于数据库实现简单

缺点

  • 基于乐观锁实现,针对高并发场景出错的机率比较高。

方案优化

针对此方案的缺点,在更新序号出错的时,提供重试机制,当达到错误重试的最大次数时,才更新失败。

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);
        }
    }

图片.png

优缺点

优点

  • 实现简单,高性能、高并发。
  • 扩展性强,由于单机Redis性能存在瓶颈,可以根据业务需求通过Redis集群扩展实现。

缺点

  • 需要引进Redis组件,增加了系统的复杂性。
  • Redis出现异常,导致生成的单据编号异常。

方案优化

Redis生成的序号成功后,将序号保存到数据库。Reids出现了异常,可以从数据库中获取序号。

总结

本文讲解了生成特殊单据编号的几种方案,需要更加业务的并发量和场景选择合适的方案,如有问题请随时反馈。