解决AOP拦截Mapper方法不知道事务是否回滚的尴尬问题
1.0.9x-RELEASE版本更新内容如下:
1、自动配置注解事务支持;2、添加事务方法出口监听器;
需求背景
最近做的一些需求遇到一个棘手的问题,例如埋点事件、AOP切面更新缓存等,可以感知事务的存在,却无法得知事务最终是回滚了还是提交了。
埋点事件:
通过Mybatis插件监听insert、update、delete三种写操作的sql语句,解析sql、分析sql操作的表、字段是否有观察者在观察(使用了观察者模式),如果有则异步回调观察者,由观察者去完成埋点事件需求。但有个弊端,事务回滚无法得知,埋点事件还是执行了。
最理想的方案应该是这样的。如果当前线程的调用链路上存在事务,那么应该在事务完成时再回调观察者,包括事务提交完成和事务回滚完成;如果当前线程的调用链路上不存在事务,则可以立即回调观察者。
AOP切面更新缓存:
在业务代码非常复杂的情况下,如果只要为了使用缓存优化查询接口的性能,去修改一大堆的接口,修改完又要重新测试,这是非常痛苦的事情,这个时候AOP就能派上用场了,无业务代码侵入当然是最好的方法。当然,也可以使用上面提到的sql监听方案。
通过在Mapper方法上添加自定义注解拦截业务相关Mapper方法的执行,方法执行完成立即更新缓存。但这样无法感知事务回滚,无法保证缓存和数据库数据的一致性。
理想的方案是这样的。如果当前调用链路上存在事务,例如,在Service层调用Mapper方法是在事务中调用的,那么应该要等到事务提交或者回滚时再确定要不要更新缓存;如果当前调用链路上不存在事务,则可以立即更新缓存。
问题:
如何将埋点事件、AOP拦截Mapper方法更新缓存的实现逻辑放在事务提交或者回滚之后再执行呢?
落地实现
easymulti-datasource-spring-boot-starter是笔者开源的一个快速整合mybatis-plus + 动态多数据源支持的starter包,不局限于主从多数据源、非主从多数据源,也支持单个数据源使用。笔者在最新的1.0.9x-RELEASE版本解决了上面提出的问题。
前提条件是项目中使用了easymulti-datasource-spring-boot-starter,由easymulti-datasource自动配置注解事务的支持。然后只需要在application配置文件中打开追踪事务方法调用链路的开关,配置如下。
## 监控事务方法调用链路
easymuti:
transaction:
open-chain: true
定义切面,拦截Mapper方法,在环绕方法中实现更新缓存的逻辑,代码如下。
- TransactionInvokeContext.currentExistTransaction:判断当前调用链路上是否存在事务;
- TransactionInvokeContext.addCurrentTransactionMethodPopListener:给当前事务绑定一个监听器(PopTransactionListener),当事务提交或者回滚时监听器被调用;
如上代码所示,首先是判断当前调用链路上是否存在事务,如果存在,则给当前事务注入一个监听器,由监听器完成缓存更新逻辑,如果不存在事务,在目标方法执行完成后且无异常抛出时执行更新缓存逻辑。
PopTransactionListener接口代码如下:
@FunctionalInterface
public interface PopTransactionListener {
/**
* 事务退出时回调用
*
* @param methodInfo 事务方法元数据信息
*/
void onTransactionPop(TxMethodMetadata methodInfo);
}
- methodInfo:包括反射获取的事务方法的Method、Method所属的类、事务是否抛出异常、事务注解。
拿到事务注解就可以拿到事务的隔离级别以及rollbackFor,根据方法抛出的异常就能判断当前事务是提交了还是回滚了。简单的事务回滚判断如下。
// 事务回滚简单判断
if (methodInfo.getThrowable() != null
&& methodInfo.getTransactional() != null) {
}
存在的不足
理解是美好的,现实是骨感的。
解决事务问题也会出现新的问题,比如,这个Mapper方法虽然是在事务方法中被调用的,但由于业务上的原因,并不需要其实现回滚,结果使用try-catch包装了Mapper方法,这种情况就凉凉了。但如果换个角度想,支持使用try-catch包装调用的Mapper显然也是对一致性要求不高的。
虽然无法实现百分百的可靠,但也并非一无是处。下个版本考虑是否需要支持通过TransactionInvokeContext获取调用链路上的所有事务方法,目前是使用双向链表存储的,想要获取并不难实现。
- Angrok 一个内网穿透服务
- Druid数据库连接池 实现数据库账号密码加密
- 【maven学习】划分模块
- 缓存策略优化
- Quartz框架应用(1)
- 6个编写优质干净代码的技巧
- 【Jfinal源码】第一章 com.jfinal.core.JFinalFilter(1)
- 【ehcache】 timeToLiveSeconds 和 timeToLiveSeconds 的区别
- 升级 CentOS7 、Redis 3.2.x 的问题
- AngularJS 中使用Swiper制作滚动图不能滑动
- JAVA服务端配置允许跨域请求
- CentOS mysql配置主从复制
- Quartz依赖数据库表
- Spring Security Oauth2.0 实现短信验证码登录
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Laravel框架中的路由和控制器操作实例分析
- php利用array_search与array_column实现二维数组查找
- Python实现手绘图效果实例分享
- php如何把表单内容提交到数据库
- Laravel5.5 视图 – 创建视图和数据传递示例
- 浅谈Python爬虫原理与数据抓取
- PHP封装cURL工具类与应用示例
- php中isset与empty函数的困惑与用法分析
- 布隆过滤器(bloom filter)及php和redis实现布隆过滤器的方法
- PHP使用反向Ajax技术实现在线客服系统详解
- PHP设计模式之适配器模式(Adapter)原理与用法详解
- laravel5.5框架的上传图片功能实例分析【仅传到服务器端】
- Laravel5.1框架自带权限控制系统 ACL用法分析
- php使用filter_var函数判断邮箱,url,ip格式示例
- Python json格式化打印实现过程解析