首页 > 其他分享 >Spring声明式事务不生效?

Spring声明式事务不生效?

时间:2024-09-12 17:24:35浏览次数:8  
标签:事务 name Spring 回滚 生效 import 方法 public

背景

本篇博文将会讲一讲Spring中使用@Transactional注解会出现的不生效问题。事务的生效与否,一般不是我们冒烟自测的范围,测试也不会去测,但是一旦上线后,事务出现不生效的情况,就可能引发较大的问题,甚至会带来损失。所以,使用好事务注解是非常重要的,尤其是注意哪些场景下会出现事务失效。

事务失效效常见情况分析

  • 1、Transactional注解必须修饰在public方法上面,如果不是public方法,则事务不会生效。
    原理说明
    因为声明式事务是通过SpringAOP代理来实现的,当在类中声明一个事务方法时,Spring会创建一个代理类,以便在调用该类的方法时应用事务管理。
    SpringAOP的实现有JDK动态代理和CGLIB代理,对于实现了接口的方法使用JDK动态代理,那些没有实现接口的方法,则使用CGLIB代理。但是不管哪种代理,都必须保证目标方法是public方法;
    因为Spring AOP在运行时会创建代理对象来拦截目标方法的调用,并在代理对象中添加事务管理的逻辑;如果不是public,那么代理对象就无法直接调用目标方法。
    举例说明
package com.example.demo3.commonpitfalls;
import com.example.demo3.commonpitfalls.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("transactional")
public class TransactionalPit {

    @Autowired
    private UserService userService;

    @GetMapping("wrong")
    public int wrong1(@RequestParam("name") String name) {
        return userService.createUserWrong1(name);
    }
}

package com.example.demo3.commonpitfalls.service;

import com.example.demo3.commonpitfalls.dto.UserEntity;
import com.example.demo3.commonpitfalls.dto.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;

@Service
@Slf4j
public class UserService {

    @Autowired
    private UserRepository userRepository;

    // 一个公共方法供Controller调用,内部调用事务性的私有方法
    public int createUserWrong1(String name) {
        try {
            this.createUserPrivate(new UserEntity(name));
        } catch (Exception ex) {
            log.error("create user failed because {}", ex.getMessage());
        }
        return userRepository.findByName(name).size();
    }

    //标记了@Transactional的private方法
    @Transactional
    private void createUserPrivate(UserEntity entity) {
        userRepository.save(entity);
        if (entity.getName().contains("test"))
            throw new RuntimeException("invalid username!");
    }
}
package com.example.demo3.commonpitfalls.dto;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

import static javax.persistence.GenerationType.AUTO;
import static javax.persistence.GenerationType.IDENTITY;

@Entity
@Data
@Table(name = "user_entity")
public class UserEntity {
    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;
    private String name;
    public UserEntity() {}
    public UserEntity(String name) {
        this.name = name;
    }
}
package com.example.demo3.commonpitfalls.dto;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
    List<UserEntity> findByName(String name);
}



当我们传入一个name ,按照代码中的逻辑,会抛出一个异常,然后会期望触发事务的回滚,但我们的执行结果却是数据库成功保存了这个name。那就说明事务并未生效。
仔细看我们的事务注解,即没有指定回滚异常,而且还用在了private方法上面。Spring AOP代理对象无法访问private的方法,从而导致事务没有生效。
修改为方法修饰符为public

controller增加一个方法
    @GetMapping("wrong2")
    public int wrong2(@RequestParam("name") String name) {
        return userService.createUserWrong2(name);
    }
service中增加两个方法
     // 一个公共方法供Controller调用,内部调用事务性的公有方法
    public int createUserWrong2(String name) {
        try {
            this.createUserPublic(new UserEntity(name));
        } catch (Exception ex) {
            log.error("create user failed because {}", ex.getMessage());
        }
        return userRepository.findByName(name).size();
    }
    //标记了@Transactional的public方法
    @Transactional
    public void createUserPublic(UserEntity entity) {
        userRepository.save(entity);
        if (entity.getName().contains("test"))
            throw new RuntimeException("invalid username!");
    }


从结果发现,这次非法name又保存成功了,说明事务依然没有生效。仔细观察我们的调用方式,使用了this, 在一个方法的内部自调用了另一个带有事务注解的方法。那么,这里给出事务的第二个失效情况。
2、Transactional注解必须通过代理过的类从外部调用目标方法才能生效, 否则会事务失效。
这是因为 Spring 使用代理来实现事务,自调用会绕过代理,导致事务不生效!
修改代码

service
首先注入本类的service
 @Autowired
 UserService userService;
然后,使用userService去调用。
// 一个公共方法供Controller调用,外部调用事务性的公有方法
    public int createUserWrong2(String name) {
        try {
           userService.createUserPublic(new UserEntity(name));
        } catch (Exception ex) {
            log.error("create user failed because {}", ex.getMessage());
        }
        return userRepository.findByName(name).size();
    }

经过调试发现,userService是由SpringCglib增强过的类,故访问的方法也是代理后的方法,具有事务的特性。

再次测试,控制台抛出了异常,数据库已经无法保存这个非法的name了。

说明事务生效了,之前执行过的save, 也在后面有执行了回滚。
这边说的外部调用,也可以是直接从另外一个类调用本来UserService类的方法!
3、事务生效后,如何捕获异常,并保证一定回滚。
有些错误的理解,认为只要有异常,事务一定会回滚,实则不然。
一般,我们写代码的时候,都这样定义事务,即@Transactional(rollbackFor = Exception.class),这个含义指的是只要方法中遇到异常,那么就执行回滚。
因为默认情况下,出现RuntimeException(非受检异常)或Error的时候,Spring才会回滚事务。如果是受检异常,Spring认为受检异常一般是业务异常,或者说
是类似另一种方法的返回值,出现这样的异常可能业务还能完成,所以不会主动回滚;而 Error或RuntimeException 代表了非预期的结果,应该回滚。

public class UserService {
        @Autowired
        private UserRepository userRepository;
        @Autowired
        UserService userService;
        // 异常无法传播出方法,导致事务无法回滚
        @Transactional
        public void createUserWrong1(String name) {
            try {
                userRepository.save(new UserEntity(name));
                throw new RuntimeException("error");
            } catch (Exception ex) {
                log.error("create user failed", ex);
            }
        }
        @Transactional
        public void createUserWrong2(String name) throws IOException {
            userRepository.save(new UserEntity(name));
            otherTask();
        }
        //因为文件不存在,一定会抛出一个 IOException
        private void otherTask() throws IOException {
            Files.readAllLines(Paths.get("file-that-not-exist"));
        }

以上代码中的两种情况, 事务都不会回滚。第一种情况是异常没有传播出方法,是由于方法内catch了所有异常,所以异常RuntimeException无法从方法传播出去,事务自然无法回滚;第二种情况是属于受检异常,createUserWrong2能将这个受检异常传播出去,但事务看到了这种异常,默认不会进行回滚。
针对第一种情况的解决方式可以是,我们手动在catch中让事务执行回滚。运行后,debug日志中,我们会看到Transactional code has requested rollback, 这个就表示是手动回滚。
针对第二种情况,既然不能回滚受检异常,那么我们就改变这个模式,让事务遇到不再区分受检还是不受检,只要是异常,那么就进行回滚。

  @Transactional(rollbackOn = Exception.class)
        public void createUserWrong2(String name) throws IOException {
            userRepository.save(new UserEntity(name));
            otherTask();
        }

改完以后,会发现可以正常回滚。一般,建议用这个方式。@Transactional(rollbackOn = Exception.class)
总结
这个例子中,我们展现的是一个复杂的业务逻辑,其中有数据库操作、IO 操作,在 IO 操作出现问题时,希望让数据库事务也回滚,以确保逻辑的一致性。在有些业务逻辑中,可能
会包含多次数据库操作,我们不一定希望将两次操作作为一个事务来处理,这时候就需要仔细考虑事务传播的配置了,否则也可能踩坑。
3、事务传播配置是否符合自己的业务逻辑
有时,事务传播级别没有设置正确,业务逻辑和我们预期的不一致,会在排查问题的时候,一下子不容易看出来是什么地方出现的错误。
那我先来普及一下声明式事务有哪些传播级别吧。

标签:事务,name,Spring,回滚,生效,import,方法,public
From: https://www.cnblogs.com/xyuanzi/p/18407909

相关文章

  • SpringCloud入门(一)SpringCloud简介
    一、为什么要学SpringCloud?企业开发都使用微服务,面试必问微服务要学会微服务开发的问题和解决方案。应对用户的增长和并发需求。要应对用户需求变化;单点式架构无法应对高并发;服务之间的错综复杂调用;二、认识微服务单体架构,订单模块,用户功能,商......
  • 基于Node.js+vue基于springboot的影视资讯管理系统(开题+程序+论文) 计算机毕业设计
    本系统(程序+源码+数据库+调试部署+开发环境)带文档lw万字以上,文末可获取源码系统程序文件列表开题报告内容研究背景随着互联网的飞速发展,影视行业迎来了前所未有的繁荣期。海量影视资源的涌现,使得用户对于高效、便捷地获取影视资讯的需求日益增长。传统的影视资讯管理方式......
  • idea+docker远程一键部署springboot
     一、配置本机和服务器通过ssh连接 ssh-keygen-trsa会在 ~/.ssh/id_rsa.pub文件中生成公钥,把改文件的内容复制到服务器的~/.ssh/authorized_keys文件中,然后在本机使用 sshroot@<服务器IP>进行连接 二、配置idea通过ssh远程连接服务器 通过Keypair进行连接......
  • 在线教育|基于springboot+vue的在线教育系统(源码+数据库+文档)
    在线教育|在线教育系统目录基于springboot+vue的在线教育系统一、前言二、系统设计三、系统功能设计 四、数据库设计 五、核心代码 六、论文参考七、最新计算机毕设选题推荐八、源码获取:博主介绍:✌️大厂码农|毕设布道师,阿里云开发社区乘风者计划专家博主,CSDN平......
  • 卫生健康系统|基于springboot+vue的智能推荐的卫生健康系统(源码+数据库+文档)
    卫生健康系统目录基于springboot+vue的智能推荐的卫生健康系统一、前言二、系统设计三、系统功能设计 四、数据库设计 五、核心代码 六、论文参考七、最新计算机毕设选题推荐八、源码获取:博主介绍:✌️大厂码农|毕设布道师,阿里云开发社区乘风者计划专家博主,CSDN平......
  • 基于springboot+vue的师生健康信息管理系统(源码+数据库+文档)
    师生健康信息|师生健康信息管理系统目录基于springboot+vue的师生健康信息管理系统一、前言二、系统设计三、系统功能设计 四、数据库设计 五、核心代码 六、论文参考七、最新计算机毕设选题推荐八、源码获取:博主介绍:✌️大厂码农|毕设布道师,阿里云开发社区乘风......
  • 基于springboot+vue的学生宿舍管理系统
    摘要随着科学技术的飞速发展,社会的方方面面、各行各业都在努力与现代的先进技术接轨,通过科技手段来提高自身的优势,新生宿舍管理系统当然也不能排除在外。新生宿舍管理系统是以实际运用为开发背景,运用软件工程原理和开发方法,采用springboot框架构建的一个管理系统。整个开......
  • 基于springboot小区物业管理系统
    513Springboot的小区物业管理系统小区物业管理系统摘要随着科学技术的飞速发展,各行各业都在努力与现代先进技术接轨,通过科技手段提高自身的优势;对于小区物业管理系统当然也不能排除在外,随着网络技术的不断成熟,带动了小区物业管理系统,它彻底改变了过去传统的管理方式,不仅......
  • 基于java+SpringBoot+Vue的小徐影城管理系统设计与实现
    开发语言:Java数据库:MySQL技术:SpringBoot+MyBatis工具:IDEA/Ecilpse、Navicat、Maven系统简介小徐影城管理系统是一款基于Java、SpringBoot和Vue.js技术开发的影院管理系统,旨在为用户提供一个便捷、高效的在线购票和影院管理平台。系统通过B/S架构,实现了管理员和用......
  • 为大学新生打造智能审核系统:Spring Boot应用
    2相关技术2.1MYSQL数据库MySQL是一个真正的多用户、多线程SQL数据库服务器。是基于SQL的客户/服务器模式的关系数据库管理系统,它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等,非常适用于Web站点或者其他......