ERP系统按配置规则生成单据号

本文讲述了在ERP系统中如何设计灵活的编号规则,以及使用乐观锁结合自旋的方式实现顺序号生成,同时讨论了在并发场景下遇到的问题和解决方案,包括事务隔离级别对乐观锁的影响以及数据库引擎的选择.
摘要由CSDN通过智能技术生成

ERP系统按配置规则生成单据号

  • 背景

在ERP系统中,不同业务单号的生成规则可能是不一样的,如果在代码中把每个业务的单号规则写死,缺少灵活性和可维护性,所以需要有一个编号规则的管理

  • 编号规则设计

MagicErp系统中,编号规则如下图

  1. 业务类型:入库单、出库单、销售订单等具体的业务
  2. 生成规则方式:不重置、每年重置编号、每月重置编号、每天重置编号。例如每天重置顺序号,第二天就又从1开始生成顺序号
  3. 开始顺序号:顺序号从哪个数字开始生成
  4. 规则类型:常量、当前年、当前年月、当前年月日日、顺序号、分部编码。可以添加任意多个规则进行组合。
  5. 按上图规则生成的第一个编号为:RK+20240320/00120--{部门id}
  • 顺序号实现方案

因为顺序号需要保证连续性和不重复,这里着重说一下顺序号的生成:

  1. 使用redis的increase方法自增

这种方式来实现简单,大部分场景不会有问题,但如果想要保证顺序号生成的稳定性,就需要解决以下问题:redis缓存丢失问题、顺序号需要按规则进行重置、编号不重复问题,如果想要把功能实现完善保证100%没问题逻辑还是很复杂的,而且需要依赖数据库和分布式锁

  1. 使用乐观锁+自旋的方式

实现原理是,给编号规则表加version字段,先查询该规则最新的编号,将其+1得到生成的编号,更新时对比version字段,如果更新成功则发号成功,并发情况下更新失败则进行自旋重试

  • 乐观锁+自旋具体实现

接下来,以MagicErp为例,具体说明乐观锁+自旋的顺序号的实现方式

1.编号规则类的lockVersion字段上增加mybatisplus乐观锁注解@Version

public class NoGenerateRuleDO extends BaseDO {

    private static final long serialVersionUID = 1L;

    @TableId(type = IdType.ASSIGN_ID)
    @ApiModelProperty(hidden = true)
    private Long id;

    @ApiModelProperty(name = "type", value = "业务类型")
    private NoBusinessTypeEnum type;

    @ApiModelProperty(name = "name", value = "规则名称")
    private String name;

    @ApiModelProperty(name = "seq_generate_type", value = "顺序号的重置规则")
    private NoSeqResetTypeEnum seqGenerateType;

    @ApiModelProperty(name = "seq_begin_number", value = "顺序号的起始值")
    private Long seqBeginNumber;

    @ApiModelProperty(name = "open_flag", value = "是否启用该规则")
    private Boolean openFlag;

    @ApiModelProperty(name = "item_list", value = "规则项列表")
    @TableField(typeHandler = JacksonTypeHandler.class)
    private RuleItemList itemList;

    @ApiModelProperty(name = "curr_seq_number", value = "当前顺序号")
    private Long currSeqNumber;

    @ApiModelProperty(name = "seq_last_generate_time", value = "最后一次生成顺序号的时间")
    private Date seqLastGenerateTime;

    @Version
    @ApiModelProperty(value = "乐观锁版本号", hidden = true)
    private Long lockVersion;

    public static class RuleItemList extends ArrayList<NoGenerateRuleItem>{

    }

}

2.生成顺序号的代码

/**
 * 数据库乐观锁 + 失败重试 生成顺序号
 *
 * @param id 编号规则id
 * @return 顺序号
 */
private Long createSeq(Long id) {
        NoGenerateRuleDO ruleDO = noGenerateRuleManager.getById(id);
        // 如果需要重置,则使用起始值,否则使用递增的顺序号
        Long currSeqNumber = needReset(ruleDO) ? ruleDO.getSeqBeginNumber() : ruleDO.getCurrSeqNumber();
        // 顺序号+1
        ruleDO.setCurrSeqNumber(currSeqNumber + 1);
        // 最后一次生成顺序号的时间
        ruleDO.setSeqLastGenerateTime(new Date());
        boolean updateResult = noGenerateRuleManager.updateById(ruleDO);
        // 乐观锁更新成功,则直接返回结果
        if (updateResult) {
            return currSeqNumber;
        } else {
            // 乐观锁更新失败进行重试
            // 数据库引擎必须使用MyISAM,让这张表的事务失效,否则事务隔离级别为可重复读,读取的数据永远是旧数据
            log.debug("编号规则:{}乐观锁更新失败,进行重试", id);
            ThreadUtil.sleep(200);
            return createSeq(id);
        }

    }
    
   /**
     * 判断这一时刻是否需要重置顺序号
     *
     * @param ruleDO 编号规则
     * @return true:需要重置顺序号
     */
    private boolean needReset(NoGenerateRuleDO ruleDO) {
        // 重置类型
        NoSeqResetTypeEnum seqResetType = ruleDO.getSeqGenerateType();

        // 不重置
        if (seqResetType == NoSeqResetTypeEnum.Normal) {
            return false;
        }

        String formatDate;
        if (seqResetType == NoSeqResetTypeEnum.Year) {
            formatDate = "yyyy";
        } else if (seqResetType == NoSeqResetTypeEnum.Month) {
            formatDate = "yyyyMM";
        } else {
            formatDate = "yyyyMMdd";
        }

        // 当前时间的日期
        String currDateInfo = DateUtil.format(new Date(), formatDate);
        // 最后一次生成顺序号的日期
        String lastDateInfo = DateUtil.format(ruleDO.getSeqLastGenerateTime(), formatDate);

        // 如果不相等,则代表是今天(今月/今年)第一次生成,需要重置顺序号
        return !currDateInfo.equals(lastDateInfo);
    }

  1. 遇到的问题

上述代码需要被其他业务调用,其他业务如果加了事务,会导致自旋重试时出现死循环,因为mysql的默认隔离级别为可重复读,在同一个事务中第一次查询会生成读快照,就会导致每次重试时查询到的都是旧的数据从而导致死循环。

解决方案:

因为本系统用到的时注解式事务,只控制本段代码不走事务不太可行(Spring的事务隔离级别全部尝试过无效),最终解决方案为将该表的引擎由Innodb改为MyISAM,这张表的事务就不会生效了,也就解决了事务的读快照导致的死循环问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值