一次批量操作的优化

  最近在做账务系统的批量核销接口的优化,该接口的功能抽象来说就是暴露给另一个系统,每次调用会进行计算并更新库里的值(感觉好普通的样子)。

  假设我这边的系统是R(received),对方系统是S(send),那么现在的工作流程是这样的:

  在线上数据量比较大的情况下(单日平时交易量10W量级,还款日接近百万),这种单笔请求单笔处理的方式虽然有一定好处,却会导致性能问题。在现有业务流程已经确定不会有太大更改的情况下,对这一情况进行了分析和优化:

  1.请求分充值请求和消费请求。充值请求是商户维度的,而消费是订单维度的。而商户则与订单是一对多的对应关系,产品与商户的关系也是一对多。除开特定的某一产品,每一商户下均有几千到几十万不等的订单数目。

  2.在日记账(可看做交易信息表)中存储了发生交易的订单号以及商户号。核销也是通过这个表的数据进行的。

  3.商户之上还有产品的维度,产品与商户的对应关系基本是一对一,有一个特例存在一个产品对应几百的商户的情况。

  4.优化方向可以从商户和产品的角度入手,考虑到底是从商户的维度进行核销请求还是产品的维度。考虑到每日交易的结算单是从商户维度产生的,因此确定以商户的维度进行核销请求,一方面保证了对外接口的维度统一,一方面由于只有一个产品是特殊情况,因此不打算因为它而影响整个设计。

  综合以上考虑,优化之后的流程图如下图:

  这样可以省去大批量核销过程中产生的网络通讯耗时,但是实际测试过程中发现性能表现并不理想,后来用 yourKit 检测了下运行时耗时,发现是因为异步处理新启动了线程,导致事务失效,而产生了三次数据库操作导致的,于是想到能否将整个商户的核销作为一个事务提交,这样相对于单笔核销而言省去了若干次数据库操作,应该性能上犹有过之。具体spring手动控制事务可以参见这里:。

  然而这样做完之后发现性能获得了大幅度提升,之前单笔操作的耗时大概是150~500ms 之间,手动控制事务后单笔耗时只有30~50ms。然而在并发操作中发现了另外一个问题:如果在核销商户 A 的过程中(假设处理线程的名称为 Thread-A),商户 B 同时进行核销操作(假设处理的线程名称为 Thread-B),如果 A 商户核销数目较大,达到几万甚至几十万的级别,那么 Thread-B 会因为长时间拿不到数据库中表的权限而抛出 deadlock 异常,这明显与我们想要的结果不符,因此只能继续想对策。

  之后想到每指定笔数(比如每100笔)进行一次事务提交,这样在兼顾性能的同时还可以保证并发操作不会出现异常。这样导致的另外一个问题是,如果商户 A 中有1000条数据,在核销第500~600笔中的某一笔失败了的话,这个区间的操作会回滚,但是之前的数据并不会,这样一笔请求回调失败了很难发现错误点,也不好进行重推操作。为了解决这个问题,想了两个方案:

  1.在日志中每次开启一次事务就在日志中打印该批操作的商户号和起止编号,同时在提交的时候和出现异常的情况下也打印出来并报警。这样如果发现某一批次失败了的话,可以在日志中查询并重新推送这笔请求,然后在代码中加以控制,只处理指定区间的核销。

  2.在日记账表中添加标志位,表示该笔是否成功核销,处理的时候将每笔处理过的标识为成功,并且只处理标志位不是成功的。这样在出现失败时重推请求,就不用考虑其他事情。

  在仔细分析对比之后把第一种方案筛掉了,原因是依赖日志不如依赖数据库稳定,二来第二种方案逻辑上更容易理解。

  以上。

原文地址:https://www.cnblogs.com/kangyun/p/6574455.html