【Java 开发常见的坑】——@Transactional 事务失效

Spring中通过@Transactional注解动态代理对目标方法的增强,可以很方便的回滚事务。但是,如果不熟悉使用@Transactional注解的话,却会有很多隐藏的坑不容易被发现,往往是在线上环境才出现问题,通过一番排查才找到问题所在,以下是本人实际工作中或是浏览其他相关博客模拟实现的场景,以此加深记忆和记录。

1.@Transactional注解标记的方法是private

2.@Transactional注解标记的方法不是Spring注入的bean调用

3.@Transactional注解没有显示声明rollbackFor属性

4.@Transactional注解标记的方法内,使用try...catch捕获异常

5.@Transactional注解使用默认的传播机制

打开@Transactional注解的内容

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}
@Transactional属性详解

废话不多说,直接上案例!

以下的案例都是模拟新增用户的流程,为了简便,使用Spring Data JPA操作数据库。

User实体类

 @Entity
 public class User {
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Integer id;
     private String name;
    
    // 省略getter、setter
 }

UserDao类

@Repository
public interface UserDao extends JpaRepository<User,Integer> {
}

Controller类

@RestController
public class TransactionController {
    @Autowired
    private UserService userService;

    @GetMapping("test")
    public void test(){
        userService.createUser();
    }
}

1.@Transactional注解标记的方法是private

接下来看下Service实现类

@Service
public class UserService{

    @Autowired
    private UserDao userDao;

    /**
     * 创建用户
     */
    public void createUser() {
        insertUser();
    }

    @Transactional
    private void insertUser(){
        User user = new User();
        user.setName("MuggleLee");
        userDao.save(user);
        throw new RuntimeException("错误");
    }
}

访问:http://localhost:8080/test后,可以发现控制台报错,证明有抛出异常,那么事务是否有回滚呢?查看一下数据库的User表,却发现有新增用户信息,就证明事务并没有回滚,事务回滚失效了!

这是为什么呢?这就需要知道@Transactional的原理,实际上就是Spring中的AOP,使用@Transactional注解,Spring就会通过动态代理的方式增强目标方法。所以private的方法是无法被代理,所以动态代理失效,无法回滚事务!

既然知道原因,那是不是将private方法改为public就行啦?

@Service
public class UserService{

    @Autowired
    private UserDao userDao;

    /**
     * 创建用户
     */
    public void createUser() {
        insertUser();
    }

    @Transactional
    public void insertUser(){
        User user = new User();
        user.setName("MuggleLee");
        userDao.save(user);
        throw new RuntimeException("错误");
    }
}

再次访问http://localhost:8080/test,虽然控制台有输出报错信息,但还是没有回滚数据库的操作,这就纳闷了,不是使用@Transactional注解就可以了吗?

这就引申到下一个"坑"了

2.@Transactional注解标记的方法不是Spring注入的bean调用

有点拗口,其实简单理解为@Transactional注解标记的方法应该是Bean的调用,而不是方法内调用。例子中@Transactional注解标记的方法是由Bean内部方法的调用,所以将@Transactional注解放到例子中的createUser方法就可以了。

@Service
public class UserService{

    @Autowired
    private UserDao userDao;

    /**
     * 创建用户
     */
    @Transactional
    public void createUser() {
        insertUser();
    }

    public void insertUser(){
        User user = new User();
        user.setName("MuggleLee");
        userDao.save(user);
        throw new RuntimeException("错误");
    }
}

访问http://localhost:8080/test,这次数据表就没有新增用户信息了,就证明事务回滚。

小结:使用@Transactional注解的方法,访问级别应该是public,而且应该是被Bean调用的方法

3.@Transactional注解没有显示声明rollbackFor属性

那我再对Service改一下,抛出的异常由原来的RuntimeException改为Exception

@Service
public class UserService{

    @Autowired
    private UserDao userDao;

    /**
     * 创建用户
     */
    @Transactional
    public void createUser() throws Exception {
        insertUser();
    }

    public void insertUser() throws Exception {
        User user = new User();
        user.setName("MuggleLee");
        userDao.save(user);
        throw new Exception("错误");
    }
}

访问http://localhost:8080/test,再次发现由新增用户信息。My God!这又是什么坑呀?

其实,这是由于不熟悉@Transactional注解的原因。

这是因为Spring框架的事务管理默认地只在发生不受控异常(RuntimeException和Error)时才进行事务回滚。也就是说,当事务方法抛出受控异常(Exception中除了RuntimeException及其子类以外的)时不会进行事务回滚。

rollbackFor属性的默认值是 RuntimeException ,但是如果抛出的异常是 Exception 类型,@Transactional注解无法捕获异常,所以也就无法回滚事务。阿里巴巴规范建议使用@Transactional注解的时候显式地声明rollbackFor属性的值

// @Transactional注解 rollbackFor 属性默认值
@Transactional(rollbackFor = RuntimeException.class)

错误使用:
@Transactional
public void test(){}

正确使用:
@Transactional(rollbackFor = Exception.class)
public void test(){}

ps.强烈建议大家在Idea上安装阿里巴巴规范插件,插件扫描代码,发现有不规范的地方就回有提示,使咱们的代码更加规范、更加优雅!

blog-插件提示.jpg

将原本使用 @Transactional 改为 @Transactional(rollbackFor = Exception.class)后,重新启动访问http://localhost:8080/test后可以发现,用户信息没有新增,就证明事务回滚了!

小结:使用 @Transactional 注解的时候,为了避免隐藏的bug,一定要显式声明rollbackFor属性的值!

4.@Transactional注解标记的方法内,使用try...catch捕获异常

接下来,模拟另外一个坑,这也是一个十分常见的事务失效问题

改动使用 @Transactional 注解的方法,将原本throw异常改为try...catch捕获异常

    /**
     * 创建用户
     */
    @Transactional(rollbackFor = Exception.class)
    public void createUser(){
        try {
            insertUser();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

访问http://localhost:8080/test后可以发现,用户信息新增了,就证明事务并没有回滚!

这是因为异常信息在被@Transactional捕获之前被try...catch...捕获了,相对于try...catch..."吃"掉了异常,@Transactional就无法捕获异常,所以就无法回滚事务!

那我想通过使用try...catch...捕获异常并做出一些补偿机制,怎么办?其实也是可以的,加上一行:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

    /**
     * 创建用户
     */
    @Transactional(rollbackFor = Exception.class)
    public void createUser(){
        try {
            insertUser();
        } catch (Exception e) {
            e.printStackTrace();
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            // 可以自定义出异常后的操作
        }
    }

小结:使用@Transactional注解的时候,要注意异常信息会不会被try...catch...捕获。

5.@Transactional注解使用默认的传播机制

@Transactional注解中,有个属性propagation,默认的传播级别为Propagation.REQUIRED

propagation属性的值有以下几种选择

  • Propagation.REQUIRED(默认):如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务
  • Propagation.SUPPORTS:如果当前存在事务,则加入事务,没有则以非事务方式运行
  • Propagation.MANDATORY:当前存在事务,则加入事务,不存在事务则抛出异常
  • Propagation.REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起
  • Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起
  • Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常
  • Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED

但要根据实际的业务场景选择事务传播级别,不一定默认的传播级别适用!

假设现在的业务场景是,先创建用户信息,然后根据用户信息创建学生信息(Student表),但如果由于某些原因,创建学生信息失败,但不能影响用户信息的创建。所以创建用户信息和学生信息应该在不同的事务内,这样才不会相互影响,这样的话,使用@Transactional默认的传播级别就实现不了,但我们可以改变propagation属性值,改为Propagation.REQUIRES_NEW

Student实体类

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    private String name;
    private String classroom;
    
    // 省略getter、setter
}

StudentDao类

@Repository
public interface StudentDao extends JpaRepository<Student,Integer> {
}

StudentService实现类

@Service
public class StudentService {
    @Autowired
    private StudentDao studentDao;

    /**
     * 创建学生基本信息
     */
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
    public void createStudentInfo() throws Exception {
        Student student = new Student();
        student.setName("MuggleLee");
        student.setClassroom("高一一班");
        studentDao.save(student);
        throw new Exception("错误");
    }
}

UserService实现类

@Service
public class UserService {

    @Autowired
    private UserDao userDao;

    @Autowired
    private StudentService studentService;

    /**
     * 创建用户
     */
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public void createUser() {
        insertUser();
        try {
            studentService.createStudentInfo();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void insertUser(){
        User user = new User();
        user.setName("MuggleLee");
        userDao.save(user);
    }
}

重启后访问http://localhost:8080/test,可以发现用户信息可以正常新增,但学生信息却没有新增,就证明学生新增信息被事务回滚,但不影响用户信息新增。

以上都是常见的事务失效的场景,希望能够诸位在开发的时候,多加注意!

如果觉得文章不错的话,麻烦点个赞哈,你的鼓励就是我的动力!对于文章有哪里不清楚或者有误的地方,欢迎在评论区留言~

参考资料:
极客时间——专栏:Java业务开发常见错误100例

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353