在业务系统逻辑实现中,经常涉及异步执行、异步更新场景的开发和使用。但在性能测试中,经常会出现因为异步逻辑设计不合理引发的不可预知问题,比如在开发验证时一切正常,测试人员在性能测试时偶发报错。
本文从Spring事务、业务逻辑顺序、数据库死锁等方面介绍在项目研发中遇到的几种异常场景供读者学习。
一、事务延迟提交引发的异步执行偶发问题分析和设计思考。
1、场景说明:当前有两个线程A、B,A是生产者,B是消费者,A、B两个线程异步执行,A每次从队列获取n条待处理任务,然后依次处理每条任务,包括业务逻辑处理、远程过程调用、更新数据库、将任务放入消费者队列,B从消费队列获取任务进行处理。
2、问题分析:消费者B从队列获取到待处理任务列表并逐条处理任务时,会从数据库查询任务的信息,当生产者未完成n条数据的全部处理过程从而造成Spring事务未整体提交时,消费者查询到的数据为生产者未提交的数据,类似与数据库的read uncommited。
3、原理分析。@Transactional是spring中声明式事务管理的注解配置方式,通过@Transactional注解可以帮助我们把事务开启、提交或者回滚的操作通过AOP的方式进行管理,免去了重复的事务管理逻辑,减少对业务代码的侵入,使我们开发人员能够专注于业务层面开发。
@Transactional注解的切面逻辑类似于@Around,在业务逻辑执行之前开始事务,然后执行具体的业务逻辑,业务逻辑执行完成后,根据是否异常提交或者回滚事务。
在该场景中,由于@Transactional注解配置在获取n条数据然后对每条数据处理的最外层,只有对n条数据处理完成后才能提交事务,但是在处理完成每条任务后已经将任务放入消费者队列,导致消费者处理任务时无法从数据库获取到最新的信息,导致任务处理失败。
4、设计规范。数据库事务中如果进行诸如调用第三方系统、上传下载文件、复杂计算等耗时操作,应在数据库事务外进行耗时操作,避免操作阻塞或者缓慢引起数据库连接耗尽。
5、优化方案。缩小事务生效范围,将@Transactional注解从最外层移动到处理每条数据更新DB的逻辑上,如下图所示:将逻辑处理、远程调用、放入队列等逻辑从事务生效范围内移除。修改后再次测试验证,可以发现不会再出现消费者获取到旧数据的问题。
6、测试设计。对于涉及事务逻辑的功能,需要设计场景验证是否存在事务延迟提交造成状态查询不一致的问题。
二、业务执行时序错乱造成的状态不一致问题分析与设计思考。
1、场景说明:在多个线程并行执行,并可能对同一张表的同一条记录更新时,因执行时序不一致造成状态异常,最终造成结果错误。
如下图所示:
步骤1:线程A调起任务插件执行;
步骤2:任务插件调用第三方服务执行具体业务;
步骤3:线程A每隔5S轮询任务执行状态并在状态变化时更新数据库;
步骤4:第三方服务执行完成后,更新数据库状态;
步骤5:插件查询任务执行状态并作出响应。
2、问题分析。步骤3、4都会进行数据库状态更新操作,这两个步骤执行顺序的异常,步骤5插件查询到的状态就会出现不一致,从而导致业务执行的一致性和幂等性无法保证。
3、设计思考。对于多个逻辑可能更新同一条数据的场景,需要考虑逻辑执行顺序和可能出现的风险场景,避免出现最终结果的不一致性。
1)从业务上保证步骤之间的顺序,即确保步骤3一定在步骤4之前执行完成。
2)设计每个步骤更新数据库后唯一状态码,通过不同状态码的流转保证整个业务流程一致性。
4、测试设计。性能测试场景设计时,对正常的业务执行顺序和异常业务执行顺序充分考虑,设计不同的业务场景。
三、异步更新数据库出现数据库死锁问题分析和设计思考。
1、场景介绍。在系统运维过程中,发现大量的数据库操作响应时间比较长,并出现了数据库死锁问题。
2、问题分析。多个线程更新同一张表的多条记录时,由于数据更新时序和事务原因,出现数据库死锁,进而造成数据更新异常。
如下图所示,有两个数据库连接Connection1、Connection2,在时间t1 ,Connection1、Connection2分别锁定key(1),key(2),在时间t2 Connection1、Connection2分别尝试获取锁key(2)、key(1),即获取对方已经占用的锁,就会出现死锁。
3、原理分析。多个数据库更新和插入操作有可能产生死锁,产生条件:
1)多个并发事务(2个或者以上);
2)每个事务都持有锁(或者是已经在等待锁);
3)每个事务都需要再继续持有锁(为了完成事务逻辑,还必须更新更多的行);
4)事务之间产生加锁的循环等待,形成死锁。
4、业务设计。
1)保证消除长事务,避免在事务中进行多条数据的操作。
2)尽量基于primary或unique key更新数据。
3)修改多个表或者多个行的时候,将修改的顺序保持一致。
4)创建索引,可以使产生的锁更少。
5、测试设计。对应多个线程可能异步并发更新相关联的数据时,设计并发压力测试场景,验证是否存在数据库死锁问题发生。
本文介绍了Spring事务配置范围过大、业务逻辑执行顺序控制缺失、事务控制不合理造成数据库死锁等3个场景,深入分析了异常发生原因、逻辑设计注意事项及测试中需要额外关注的场景,希望能够抛砖引玉,给软件开发和测试人员提供有益经验。
作者:郭朝兴、达日汗等