在网易支付系统风险治理过程中,有一项重要的风险即是事务内存在外部调用,本文就事务内外部调用带来的危害、如何发现、如何解决问题进行阐述。
数据库事务,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。
事务在我们业务处理过程中频繁的会使用,但由于开发人员对事务的理解,业务调用链较多的情况下,容易出现事务使用的不合理、不准确等情况,比如事务未生效、事务内资源加锁时间过长、事务内存在部分不受事务控制的业务数据变更等等情况的发生,本文主要就事务内存在外部调用带来的问题进行分析、解决。
一、事务内外部调用的危害
1、数据不一致
数据不一致是指在一个事务内对外调用的业务指令完成,在该指令的后置流程,产生了异常导致事务的回滚,而回滚无法完成对外部调用业务数据的回滚引起的数据不一致情况。

在如上示例中,会导致B指令无法回滚,从而造成了本系统与外部系统数据的不一致。
2、稳定性不可控
事务内需要保证数据的一致性,在对部分资源加锁,若锁资源长时间无法释放,会增加数据库的压力,甚至造成数据库连接的崩溃,这要求我们在对资源加锁时,需要严格控制加锁时间,不宜长时间加锁,但在事务内存在外部调用,而外部调用本身存在调用时间不可控,无疑增加锁连接时间的不可控。

二、事务内外部调用检测
当前我们的应用中存在事务内外部调用的方式主要有http、dubbo两种形式;事务检测基于spring的事务管理org.springframework.transaction.support.TransactionSynchronizationManager判断本次外部请求中是否存在激活的事务即可发现所有的事务存在的内外部调用情况。
其核心实现如下
/**
* 检查当地事务是否存在外部调用
*/
public static void checkInvokeInTransaction(String... params)
{
try
{
//判断是否存在事务
boolean hasTransaction = TransactionSynchronizationManager.isActualTransactionActive();
if (hasTransaction)
{
log.warn("【E2】invoke in transaction !params[" + GsonUtil.toJson(params) + "]");
}
}
catch (Exception e)
{
log.error("checkInvokeInTransaction Exception!", e);
}
}
http请求:在http请求模板中增加统一的check流程
dubbo:实现Filter,增加事务检测的filter
三、事务内外部调用处理
1、外部查询调用前置
在治理事务内外部调用最常见的问题是在事务内存在外部的查询调用,此类问题解决也较为简单,通常将查询操作前置在事务操作之前。

2、去除大事务,实现多流程幂等
在一个大事务流程中包含了多个业务按顺序执行的流程,若依赖外部的结果且依赖事务前置部分的数据,则方案1中则无法解决此类问题。当然在实际的业务处理过程中,非必要情况也不建议将大量流程放在一个事务中处理,可以采用按原子业务的形式,形成可编排可幂等的执行链路。

3、减小事务范围,采用编程式事务管理
Spring事务管理主要可以分为两种类型:编程式事务管理和声明式事务管理。这两种类型提供了不同层次上的事务控制。
编程式事务管理允许你在代码中显式地管理事务边界。这种类型的事务管理需要更多的编码工作,但是提供了更精确的控制,允许你在事务管理中进行更多的定制。
声明式事务管理允许你在配置中声明事务边界,而不是在代码中。这种方式减少了样板代码的数量,让业务逻辑更加清晰,并且在大多数情况下,是更推荐使用的事务管理策略。
而开发人员在开发过程中,为了方便简单的通过@Transactional注解来实现,容易不分场景暴力使用声明式事务。

-- End --
点击下方的公众号入口,关注「技术对话」微信公众号,可查看历史文章,投稿请在公众号后台回复:投稿




