• 欢迎访问搞代码网站,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站!
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏搞代码吧

关于java:Spring声明式事务使用的那些坑-最全的Transactional注解的避坑指南

java 搞代码 4年前 (2022-01-27) 89次浏览 已收录 0个评论
文章目录[隐藏]

一、序言

哈喽小伙伴们,俺是啤酒熊,明天想来和大家聊聊Spring中波及数据库事务的一些坑。

Spring为开发人员提供了申明式事务的应用形式,即在办法上标记@Transactional注解来开启事务。

咱们都分明,在业务代码对数据进行操作的时候肯定是心愿有事务管制的。

比方在写商家卖东西的业务代码时,代码的逻辑是商家学生成一个订单(订单信息插入到数据库中),再将钱支出到本人的账户中(数据库中的money减少)。如果前面这个操作失败了,那么前者也肯定不能插入胜利,这时候就会用到事务的回滚。

尽管大部分做后端开发同学们都有这方面的概念,然而在应用@Transactional注解时依旧会呈现一些谬误。

前几天在对公司新来的同学们写的代码做code review的时候,看到了他们在Spring的我的项目中,对于@Transactional注解的一些谬误应用。在给他们纠正错误的同时也不禁想到本人也曾掉进过这些坑之中 ヽ(ー_ー)ノ

于是便想对该注解的应用做个避坑指南~,分享到社区中。

本文将介绍平时的业务开发中对于@Transactional的常见的几种谬误应用,并给出相应的错误代码示例。针对每种谬误类型,解释其起因,并给出应用@Transactional注解的正确应用姿态。接下来就让咱们一起来看一下吧!

二、试验筹备

2.1 数据库

咱们在数据库中定义一个goods_stock货物库存表,并赋予一些初始数据:

表明目前商品id为good_0001的商品库存为10件。

2.2 Spring Boot+Mybatis

咱们在Java的办法中利用Mybatis进行减库存的操作,并在办法上标注@Transactional的注解,看该注解是否能使事务生效,遇到谬误就回滚。

我的项目构造如下:

咱们将利用Swagger调用Controller层中的接口,在接口中调用ServiceGoodsStockServiceImp中的具体业务代码,即减库存的操作。

具体的sql语句,执行库存减10操作,即若业务代码执行胜利,该商品库存变为0:

<code class="XML"><?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.beerbear.springboottransaction.dao.GoodsStockMapper">
    <update id="updateStock">
        update goods_stock set stock = stock - 10
    </update>
</mapper>

三、抛出异样

3.1 异样流传未出@Transactional标记的办法

很多时候,在实在的业务开发中,总心愿接口能返回一个固定的类实例——这叫做对立返回后果。在本文中,以Result类作为对立返回后果,具体可看本文附带代码。

于是就有可能为了不便就间接在Service的办法中return一个Result类对象,为了防止受异样的影响而无奈返回该后果集,就会应用try-catch语句,当业务代码呈现谬误而抛出异样时会捕捉此异样,将异样信息写入Result的相干字段中,返回给调用者。上面给出该类型的实例:

Controller层:

<code class="java">@Controller
@RestController
@Api( tags = "测试事务是否失效")
@RequestMapping("/test/transactionalTest")
@Slf4j
public class GoodsStockController {

    @Autowired
    private GoodsStockService goodsStockService;
    /**
     * create by: Beer Bear
     * description: 第一个办法。
     * create time: 2021/7/25 21:38
     */
    @GetMapping("/exception/first")
    @ApiOperation(value = "对于异样的第一个办法,不可能回滚", notes = "因为异样未能被事务发现,所以没有回滚")
    @ResponseBody
    public Result firstFunctionAboutException(){
        try{
            return goodsStockService.firstFunctionAboutException();
        }catch (Exception e){
            return Result.server_error().Message("操作失败:"+e.getMessage());
        }
    }
}

Service中的办法:

<code class="java">@Autowired
    private GoodsStockMapper goodsStockMapper;

    @Override
    @Transactional
    public Result firstFunctionAboutException() {
        try{
            log.info("减库存开始");
            goodsStockMapper.updateStock();
            if(1 == 1) throw new RuntimeException();
            return Result.ok();
        }catch (Exception e){
            log.info("减库存失败!" + e.getMessage());
            return Result.server_error().Message("减库存失败!" + e.getMessage());
        }
    }

firstFunctionAboutException办法的try代码块中,肯定会抛出一个RuntimeException的异样,但这样是否能回滚呢?咱们无妨通过试验来看一下:

利用Swagger调用接口:

调用接口后,按理说事务应该回滚,库存数量不会变为0,但后果却是:

为了节缩篇幅,下文中不再将呈现这些截图,而是以文字代替

显然事务没有回滚。咱们都晓得当程序执行时呈现谬误而抛出异样时,事务才会回滚,这里尽管呈现了异样但却被办法自身消化了(catch掉了),异样没有被事务所发现,所以这样子是不会呈现回滚的。

上面咱们咱们给出相干正确的解决办法——将service中的try-catch语句去掉:

<code class="java">@Override
@Transactional
public void secondFunctionAboutException() {
    log.info("减库存开始");
    goodsStockMapper.updateStock();
    if(1 == 1) throw new RuntimeException();
}

这样子就能实现事务回滚了。(不过这样的话,异样怎么办呢,总不能间接报个异样吧,很简略,将异样放在Controller层去解决就行了。)

在此总结避坑指南的第一坑

当标记了@Transactional注解的办法中出现异常时,如果该异样未流传到该办法外,则事务不会回滚;反之,只有异样流传到该办法之外,事务才会回滚。

3.2 明明抛出了异样却不回滚

当初咱们都晓得了当程序执行时呈现谬误而抛出异样时,只有别去解决该异样,让异样冲破@Transactional所标注的办法,就能实现冀望的回滚。

然而事实真的如此么?上面咱们再来看个案例:

<code class="java">@Override
@Transactional
public void thirdFunctionAboutException() throws Exception {
    log.info("减库存开始");
    goodsStockMapper.updateStock();
    if(1 == 1) throw new Exception();
}

实际上,这个办法中的事务并不会回滚。

这也是咱们在理论开发时最常常犯的谬误,感觉只有抛出异样了就肯定会回滚,后果被事实啪啪啪的打脸。

但我并不感觉这是件丢人的事件,因为咱们去用一个工具的时候,或者一开始的确没精力和能力去学习它的一些原理,从而掉入了一些咱们不易发现的坑。只有前面保持学习,就肯定会缓缓把这些坑填满,本人也就越来越强了。

好,言归正传,为啥在此事务不会回滚呢。咱们将该办法与下面的secondFunctionAboutException一比照,发现只不过是RuntimeExceptionException的区别。的确是这样,就是因为Spring@Transactional注解就是默认只有当抛出RuntimeException运行时异样时,才会回滚。

Spring通常采纳RuntimeException示意不可复原的谬误条件。也就是说对于其余异样,Spring感觉无所谓所以就不回滚。

上面我给出两种解决方案:

<code class="java">@Override
@Transactional
public void thirdFunctionAboutException1(){
    try{
        log.info("减库存开始");
        goodsStockMapper.updateStock();
        if(1 == 1) throw new Exception();
    }catch (Exception e){
        log.info("出现异常"+e.getMessage());
        throw new RuntimeException("手动抛出RuntimeException");
    }
}

@Override
@Transactional(rollbackFor = Exception.class)
public void thirdFunctionAboutException2() throws Exception {
    log.info("减库存开始");
    goodsStockMapper.updateStock();
    if(1 == 1) throw new Exception();
}

第一种咱们手动来抛出RuntimeException异样,第二种是扭转默认的@Transactional回滚的异样设置(RuntimeException是继承了Exception异样的)。

@Transactional(rollbackFor = Exception.class)

在此总结避坑指南的第二坑

默认状况下,如果咱们抛出的异样不是RuntimeException时,事务仍旧不会回滚;须要手动抛出RuntimeException异样或者更改Spring中@Transactional默认配置。

四、事务还是不失效

就算咱们留神到了异样与@Transactional的关系,并正确地避开了这些坑,然而咱们还是会掉入一些更不容易发现和了解的坑中。在这一大节中咱们将持续举出反例,并阐明这些例子中事务未失效的起因以及给出解决办法。在这一大节中你还将学到@Transactional事务与Spring AOP的分割。

4.1 示例一

service中增加这样两个办法:

<code class="java">@Override
public void privateFunctionCaller (){
    privateCallee();
}

@Transactional
private void privateCallee(){
    goodsStockMapper.updateStock();
    throw new RuntimeException();
}

Controller中调用serviceprivateFunctionCaller办法从而间接调用标注了@Transactional注解的办法privateCallee

执行代码后,发现事务并没有回滚。这又是为什么呢?

咱们在Service的类上标注了@Service注解示意该类作为Bean注入AOP容器,而Spring是通过动静代理来实现AOP的。也就说AOP容器中的Bean实际上都是代理对象。

Spring也正是通过该形式对@Transactional进行反对的,Spring会对原对象中的办法进行封装(即查看到标有该注解的办法时,就会为它加上事务).

这个行为就叫做为指标办法进行加强。尽管Spring实现动静代理的形式是CGLIB,但在此我想以JDK动静代理的实现形式来解释,因为这更便于了解。

service.function()能够看出,要是走代理的加强的形式,那么必然function不能是private的。所以private的办法上的事务并不能失效,天然就不能回滚了。

实际上当你写出这种上述的代码时,如果你应用的编译器是IDEA,编译器就会提醒报错,当然只是报红,而不会影响编译和执行。

Java 中实现动静代理的形式中就有JDK的实现形式和CGLIB。不了解动静代理的同学能够去学习一下代理模式,以及MaBtais在Spring中的实现。

4.2 示例二

那咱们是不是只有把private换成public就能够了呢?上面的代码是很多同学刚应用@Transactional时常常掉入一个坑。

<code class="java">@Override
public void publicFunctionCaller (){
    publicCallee();
}

@Override
@Transactional
public void publicCallee(){
    goodsStockMapper.updateStock();
    throw new RuntimeException();
}

咱们在Controller中调用Service中的publicFunctionCaller时,发现事务还是不能回滚,这又是为什么呢?

上文咱们提到,在Controller中,被注入的Service对象其实是他的代理对象,当调用publicCallee办法时,下面是没有@Transactional注解的。

故只是简略的执行service.function()而已,即在代理对象的办法publicFunctionCaller中,先由Service的原对象来调用本人的publicFunctionCaller办法,再由其调用本人的publicCallee办法。压根就不会走代理对象加强过(带有事务)的publicCallee办法。天然事务也就不会回滚。

解决办法,我想大家就能本人找到了,那就是在Controller中由注入servicebean间接调用标注了@Transactional的办法,例如前文中的secondFunctionAboutException的被调用。

当然,咱们还能够曲线救国,在service中注入本人,这样就能实现代理对象来调用加强的办法:

<code class="java">@Override
@Transactional
public void publicCallee(){
    goodsStockMapper.updateStock();
    throw new RuntimeException();
}

@Autowired
private GoodsStockService self;

@Override
public void aopSelfCaller (){
    self.publicCallee();
}

不过显然这不合乎分层构造,也不优雅。

在此总结避坑指南的第三坑

标记了@Transactioal注解的办法必须是public的且必须由注入bean来间接调用能力事务回滚。

到此为止,@Transactional的避坑指南就算是完结了,大家有啥疑难,请评论留言,咱们互相交换。

也心愿大家多多点赞,当前还会持续输入更多优质文章的!

本文所有代码,放在Gitee上,须要的小伙伴自取。


搞代码网(gaodaima.com)提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发送到邮箱[email protected],我们会在看到邮件的第一时间内为您处理,或直接联系QQ:872152909。本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:关于java:Spring声明式事务使用的那些坑-最全的Transactional注解的避坑指南

喜欢 (0)
[搞代码]
分享 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址