文档章节

SpringBoot 系列教程之事务不生效的几种 case

小灰灰Blog
 小灰灰Blog
发布于 02/05 08:32
字数 2421
阅读 2.8K
收藏 5

3 月,跳不动了?>>>

SpringBoot 系列教程之事务不生效的几种 case

前面几篇博文介绍了声明式事务@Transactional的使用姿势,只知道正确的使用姿势可能还不够,还得知道什么场景下不生效,避免采坑。本文将主要介绍让事务不生效的几种 case

<!-- more -->

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. 系列博文&源码

系列博文

源码

1. 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog

© 著作权归作者所有

小灰灰Blog

小灰灰Blog

粉丝 230
博文 277
码字总数 497875
作品 0
武汉
程序员
私信 提问
加载中

评论(2)

ssss111231
ssss111231
加油
小灰灰Blog
小灰灰Blog 博主
tks 👉
SpringBoot 系列教程之编程式事务使用姿势介绍篇

SpringBoot 系列教程之编程式事务使用姿势介绍篇 前面介绍的几篇事务的博文,主要是利用注解的声明式使用姿势,其好处在于使用简单,侵入性低,可辨识性高(一看就知道使用了事务);然而缺点...

小灰灰Blog
02/05
54
0
SpringBoot 系列教程之声明式事务 Transactional

200119-SpringBoot 系列教程之声明式事务 Transactional 当我们希望一组操作,要么都成功,要么都失败时,往往会考虑利用事务来实现这一点;之前介绍的 db 操作,主要在于单表的 CURD,本文将...

小灰灰Blog
01/20
1.3K
0
SpringBoot系列教程web篇之重定向

原文地址: SpringBoot系列教程web篇之重定向 前面介绍了spring web篇数据返回的几种常用姿势,当我们在相应一个http请求时,除了直接返回数据之外,还有另一种常见的case -> 重定向; 比如我...

小灰灰Blog
2019/10/10
99
0
SpringBoot 系列教程 web 篇之自定义请求匹配条件 RequestCondition

191222-SpringBoot 系列教程 web 篇之自定义请求匹配条件 RequestCondition 在 spring mvc 中,我们知道用户发起的请求可以通过 url 匹配到我们通过定义的服务端点上;不知道有几个问题大家是...

小灰灰Blog
2019/12/23
77
0
【SpringBoot2.0系列08】SpringBoot之redis数据缓存管理

【SpringBoot2.0系列01】初识SpringBoot 【SpringBoot2.0系列02】SpringBoot之使用Thymeleaf视图模板 【SpringBoot2.0系列03】SpringBoot之使用freemark视图模板 【SpringBoot2.0系列04】Spr...

余空啊
2018/08/20
0
0

没有更多内容

加载失败,请刷新页面

加载更多

专业视频下载工具:Allavsoft for Mac

作为MacOS系统上的一款专业视频下载工具,Allavsoft mac破解版可以帮助用户快速进行视频在线下载和转换功能,Allavsoft中文版支持从雅虎、YouTube、Facebook等多达100家的视频分享网站的影片...

MacW软件分享
18分钟前
17
0
【德邦快递】从发货、分拨、分拣到配送,敏态数字化团队支撑高效的供应链!

“2019双11物流订单量创下新纪录,11月11号天猫全天物流订单突破10.42亿,截止至当天18:42,德邦快递宣布大件快递单产品实现收入破亿!” 如今物流行业强劲的运输效能,撑起了我们习以为常的...

嘉为科技
26分钟前
15
0
答应我,不会这些概念,简历不要写 “熟悉” zookeeper

唠唠叨叨 本文主要分享一下zookeeper的一些基本概念,在正式进入正题前,和大家聊一聊刚入行时我的面试经验,可以说是耿直的有些可爱。 面试官:用过zookeeper 吗? 我:用过啊,给dubbo提供...

程序员内点事
27分钟前
11
0
GitLab备份与恢复

因公司更换服务器服务商,服务器迁移到另外一家服务商,今天就模拟线上迁移gitlab的操作 git-A IP:192.168.82.184 git-B IP:192.168.82.184 在做备份迁移前提是版本号一样,否则会失败. Step ...

Linux_Anna
29分钟前
12
0
Snippetty for Mac(现场代码演示工具) v1.7.1

Snippetty是一个从markdown文件加载代码片段的应用程序。Snippetty Mac破解版适合每个人,Snippetty不会假设您的演示文稿的全部内容,只需要在正确的时间将正确的代码段放入计算机的剪贴板中...

麦克W
39分钟前
19
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部