SpringBoot 系列教程之事务不生效的几种 case
前面几篇博文介绍了声明式事务@Transactional
的使用姿势,只知道正确的使用姿势可能还不够,还得知道什么场景下不生效,避免采坑。本文将主要介绍让事务不生效的几种 case
I. 配置
本文的 case,将使用声明式事务,首先我们创建一个 SpringBoot 项目,版本为2.2.1.RELEASE
,使用 mysql 作为目标数据库,存储引擎选择Innodb
,事务隔离级别为 RR
1. 项目配置
在项目pom.xml
文件中,加上spring-boot-starter-jdbc
,会注入一个DataSourceTransactionManager
的 bean,提供了事务支持
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
2. 数据库配置
进入 spring 配置文件application.properties
,设置一下 db 相关的信息
## DataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=
3. 数据库
新建一个简单的表结构,用于测试
CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;
II. 不生效 case
在声明式事务的使用教程200119-SpringBoot 系列教程之声明式事务 Transactional 中,也提到了一些事务不生效的方式,比如声明式事务注解@Transactional
主要是结合代理实现,结合 AOP 的知识点,至少可以得出放在私有方法上,类内部调用都不会生效,下面进入详细说明
1. 数据库
事务生效的前提是你的数据源得支持事务,比如 mysql 的 MyISAM 引擎就不支持事务,而 Innodb 支持事务
下面的 case 都是基于 mysql + Innodb 引擎
为后续的演示 case,我们准备一些数据如下
@Service
public class NotEffectDemo {
@Autowired
private JdbcTemplate jdbcTemplate;
@PostConstruct
public void init() {
String sql = "replace into money (id, name, money) values" + " (520, '初始化', 200)," + "(530, '初始化', 200)," +
"(540, '初始化', 200)," + "(550, '初始化', 200)";
jdbcTemplate.execute(sql);
}
}
2. 类内部访问
简单来讲就是指非直接访问带注解标记的方法 B,而是通过类普通方法 A,然后由 A 访问 B
下面是一个简单的 case
/**
* 非直接调用,不生效
*
* @param id
* @return
* @throws Exception
*/
@Transactional(rollbackFor = Exception.class)
public boolean testCompileException2(int id) throws Exception {
if (this.updateName(id)) {
this.query("after update name", id);
if (this.update(id)) {
return true;
}
}
throw new Exception("参数异常");
}
public boolean testCall(int id) throws Exception {
return testCompileException2(id);
}
上面两个方法,直接调用testCompleException
方法,事务正常操作;通过调用testCall
间接访问,在不生效
测试 case 如下:
@Component
public class NotEffectSample {
@Autowired
private NotEffectDemo notEffectDemo;
public void testNotEffect() {
testCall(530, (id) -> notEffectDemo.testCall(530));
}
private void testCall(int id, CallFunc<Integer, Boolean> func) {
System.out.println("============ 事务不生效case start ========== ");
notEffectDemo.query("transaction before", id);
try {
// 事务可以正常工作
func.apply(id);
} catch (Exception e) {
}
notEffectDemo.query("transaction end", id);
System.out.println("============ 事务不生效case end ========== \n");
}
@FunctionalInterface
public interface CallFunc<T, R> {
R apply(T t) throws Exception;
}
}
输出结果如下:
============ 事务不生效case start ==========
transaction before >>>> {id=530, name=初始化, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
after update name >>>> {id=530, name=更新, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
transaction end >>>> {id=530, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
============ 事务不生效case end ==========
从上面的输出可以看到,事务并没有回滚,主要是因为类内部调用,不会通过代理方式访问
3. 私有方法
在私有方法上,添加@Transactional
注解也不会生效,私有方法外部不能访问,所以只能内部访问,上面的 case 不生效,这个当然也不生效了
/**
* 私有方法上的注解,不生效
*
* @param id
* @return
* @throws Exception
*/
@Transactional
private boolean testSpecialException(int id) throws Exception {
if (this.updateName(id)) {
this.query("after update name", id);
if (this.update(id)) {
return true;
}
}
throw new Exception("参数异常");
}
直接使用时,下面这种场景不太容易出现,因为 IDEA 会有提醒,文案为: Methods annotated with '@Transactional' must be overridable
4. 异常不匹配
@Transactional
注解默认处理运行时异常,即只有抛出运行时异常时,才会触发事务回滚,否则并不会如
/**
* 非运行异常,且没有通过 rollbackFor 指定抛出的异常,不生效
*
* @param id
* @return
* @throws Exception
*/
@Transactional
public boolean testCompleException(int id) throws Exception {
if (this.updateName(id)) {
this.query("after update name", id);
if (this.update(id)) {
return true;
}
}
throw new Exception("参数异常");
}
测试 case 如下
public void testNotEffect() {
testCall(520, (id) -> notEffectDemo.testCompleException(520));
}
输出结果如下,事务并未回滚(如果需要解决这个问题,通过设置@Transactional
的 rollbackFor 属性即可)
============ 事务不生效case start ==========
transaction before >>>> {id=520, name=初始化, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
after update name >>>> {id=520, name=更新, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
transaction end >>>> {id=520, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
============ 事务不生效case end ==========
5. 多线程
这个场景可能并不多见,在标记事务的方法内部,另起子线程执行 db 操作,此时事务同样不会生效
下面给出两个不同的姿势,一个是子线程抛异常,主线程 ok;一个是子线程 ok,主线程抛异常
a. case1
/**
* 子线程抛异常,主线程无法捕获,导致事务不生效
*
* @param id
* @return
*/
@Transactional(rollbackFor = Exception.class)
public boolean testMultThread(int id) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
updateName(id);
query("after update name", id);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
boolean ans = update(id);
query("after update id", id);
if (!ans) {
throw new RuntimeException("failed to update ans");
}
}
}).start();
Thread.sleep(1000);
System.out.println("------- 子线程 --------");
return true;
}
上面这种场景不生效很好理解,子线程的异常不会被外部的线程捕获,testMultThread
这个方法的调用不抛异常,因此不会触发事务回滚
public void testNotEffect() {
testCall(540, (id) -> notEffectDemo.testMultThread(540));
}
输出结果如下
============ 事务不生效case start ==========
transaction before >>>> {id=540, name=初始化, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
after update name >>>> {id=540, name=更新, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
Exception in thread "Thread-3" java.lang.RuntimeException: failed to update ans
at com.git.hui.boot.jdbc.demo.NotEffectDemo$2.run(NotEffectDemo.java:112)
at java.lang.Thread.run(Thread.java:748)
after update id >>>> {id=540, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
------- 子线程 --------
transaction end >>>> {id=540, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
============ 事务不生效case end ==========
b. case2
/**
* 子线程抛异常,主线程无法捕获,导致事务不生效
*
* @param id
* @return
*/
@Transactional(rollbackFor = Exception.class)
public boolean testMultThread2(int id) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
updateName(id);
query("after update name", id);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
boolean ans = update(id);
query("after update id", id);
}
}).start();
Thread.sleep(1000);
System.out.println("------- 子线程 --------");
update(id);
query("after outer update id", id);
throw new RuntimeException("failed to update ans");
}
上面这个看着好像没有毛病,抛出线程,事务回滚,可惜两个子线程的修改并不会被回滚
测试代码
public void testNotEffect() {
testCall(550, (id) -> notEffectDemo.testMultThread2(550));
}
从下面的输出也可以知道,子线程的修改并不在同一个事务内,不会被回滚
============ 事务不生效case start ==========
transaction before >>>> {id=550, name=初始化, money=200, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:38.0}
after update name >>>> {id=550, name=更新, money=200, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:40.0}
after update id >>>> {id=550, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:40.0}
------- 子线程 --------
after outer update id >>>> {id=550, name=更新, money=220, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:41.0}
transaction end >>>> {id=550, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:40.0}
============ 事务不生效case end ==========
6. 传播属性
上一篇关于传播属性的博文中,介绍了其中有几种是不走事务执行的,所以也需要额外注意下,详情可以参考博文 200202-SpringBoot 系列教程之事务传递属性
7. 小结
下面小结几种@Transactional
注解事务不生效的 case
- 数据库不支持事务
- 注解放在了私有方法上
- 类内部调用
- 未捕获异常
- 多线程场景
- 传播属性设置问题
III. 其他
0. 系列博文&源码
系列博文
- 180926-SpringBoot 高级篇 DB 之基本使用
- 190407-SpringBoot 高级篇 JdbcTemplate 之数据插入使用姿势详解
- 190412-SpringBoot 高级篇 JdbcTemplate 之数据查询上篇
- 190417-SpringBoot 高级篇 JdbcTemplate 之数据查询下篇
- 190418-SpringBoot 高级篇 JdbcTemplate 之数据更新与删除
- 200119-SpringBoot 系列教程之声明式事务 Transactional
- 200120-SpringBoot 系列教程之事务隔离级别知识点小结
- 200202-SpringBoot 系列教程之事务传递属性
源码
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 实例源码: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/101-jdbctemplate-transaction
1. 一灰灰 Blog
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激
下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
- 一灰灰 Blog 个人博客 https://blog.hhui.top
- 一灰灰 Blog-Spring 专题博客 http://spring.hhui.top
SpringBoot 系列教程之事务不生效的几种 case的更多相关文章
- SpringBoot 系列教程之事务隔离级别知识点小结
SpringBoot 系列教程之事务隔离级别知识点小结 上一篇博文介绍了声明式事务@Transactional的简单使用姿势,最文章的最后给出了这个注解的多个属性,本文将着重放在事务隔离级别的知识点上 ...
- SpringBoot 系列教程自动配置选择生效
191214-SpringBoot 系列教程自动配置选择生效 写了这么久的 Spring 系列博文,发现了一个问题,之前所有的文章都是围绕的让一个东西生效:那么有没有反其道而行之的呢? 我们知道可以通 ...
- SpringBoot系列教程之事务传递属性
200202-SpringBoot系列教程之事务传递属性 对于mysql而言,关于事务的主要知识点可能几种在隔离级别上:在Spring体系中,使用事务的时候,还有一个知识点事务的传递属性同样重要,本文 ...
- SpringBoot系列教程web篇Servlet 注册的四种姿势
原文: 191122-SpringBoot系列教程web篇Servlet 注册的四种姿势 前面介绍了 java web 三要素中 filter 的使用指南与常见的易错事项,接下来我们来看一下 Serv ...
- SpringBoot 系列教程之编程式事务使用姿势介绍篇
SpringBoot 系列教程之编程式事务使用姿势介绍篇 前面介绍的几篇事务的博文,主要是利用@Transactional注解的声明式使用姿势,其好处在于使用简单,侵入性低,可辨识性高(一看就知道使用 ...
- Java工程师之SpringBoot系列教程前言&目录
前言 与时俱进是每一个程序员都应该有的意识,当一个Java程序员在当代步遍布的时候,你就行该想到我能多学点什么.可观的是后端的框架是稳定的,它们能够维持更久的时间在应用中,而不用担心技术的更新换代.但 ...
- SpringBoot系列教程web篇Listener四种注册姿势
java web三要素Filter, Servlet前面分别进行了介绍,接下来我们看一下Listener的相关知识点,本篇博文主要内容为SpringBoot环境下,如何自定义Listener并注册到s ...
- SpringBoot系列教程web篇之过滤器Filter使用指南
web三大组件之一Filter,可以说是很多小伙伴学习java web时最早接触的知识点了,然而学得早不代表就用得多.基本上,如果不是让你从0到1写一个web应用(或者说即便从0到1写一个web应用) ...
- SpringBoot系列教程web篇之自定义异常处理HandlerExceptionResolver
关于Web应用的全局异常处理,上一篇介绍了ControllerAdvice结合@ExceptionHandler的方式来实现web应用的全局异常管理: 本篇博文则带来另外一种并不常见的使用方式,通过实 ...
随机推荐
- SpringBoot开发环境要求
JDK 截止到目前Spring Boot 的最新版本:2.1.8.RELEASE 要求 JDK 版本在 1.8 以上,所以确保你的电脑已经正确下载安装配置了 JDK(推荐 JDK 1.8 版本). 构 ...
- @vue-cli的安装及vue项目创建
1.安装 Node.js & Vue CLI @vue/cli3,是vue-进行搭建的脚手架项目,它本质上是一个全局安装的 npm 包,通过安装它,可以为终端提供 vue 命令,进行vue项目 ...
- Redis散列表类型
散列类型(hash)的键值也是一种字典结构,其存储了字段(field)和字段值的映射,但字段值只能是字符串,不支持其他的数据类型. 一个散列类型键可以包含至多2^32 -1个字段. 命令 赋值 HSE ...
- 使用zabbix server监控tomcat实战案例
使用zabbix server监控tomcat实战案例 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 大家都知道,zabbix server效率高是使用C语言编写的,有很多应用程序 ...
- 201706 gem 'rails-erd'生成Model关系图
[工具]一张图理清各个model之间关系 安装 Graphviz 2.22+: 终端机中执行 brew install graphviz Gemfile中添加 gem 'rails-erd' 终端机中 ...
- Tomcat删除时问题——eclipse部署tomcat时弹出Resource'/Servers' does not exist
如果你删除一个项目的Servers文件,或者相应文件损坏等,会出现错误, Resource '/Servers' does not exist 那么就需要把它在控制台出的Servers下所部署的Tom ...
- 手机连接jmeter录制脚本测试
1.准备条件 电脑安装好jmeter,准备好一个手机 注意: 电脑和手机连接的网络要一致 手机设置代理协议前要先进入想要抓取的网站: http://39.107.96.138:3000/ 2.jmet ...
- Linux 只复制目录,不复制目录下数据文件
[root@yoon u02]# mkdir yoon [root@yoon u02]# mkdir hank [root@yoon yoon]# mkdir -p 1/data [root@yoon ...
- java#lambda相关之方法引用
lambda在java中通常是()->{}这样的方式,来书写的.通常的lambda是四大函数型接口的一个“实现”. 如果我们要写的lambda已经有现成的实现了,那么就可以把现成的实现拿过来使用 ...
- 需要多个参数输入时-----------------考虑使用变种的Builder模式
业务需求: 创建一个不可变的Person对象,这个Person可以拥有以下几个属性:名字.性别.年龄.职业.车.鞋子.衣服.钱.房子. 要求: 其中名字和性别是必填项,而其他选填项可以根据情况自由输入 ...