Spring事务失效的常见场景

1 事务


        1.1 数据库事务


                作为单个逻辑工作单元执行的一系列操作,要么完全执行,要么完全不执行

        1.2 事务的四大特性(ACID)


                原子性(Atomicity):要么成功,要么失败。一个事务内的所有SQL语句同步执行(依靠undo.log日志保证)
                一致性(Consistency):事务前后总量不变,数据库完整性约束没有被破坏
                隔离性(Isolation):一个事务执行不被其他事务干扰(锁+MVCC)
                持久性(Durability):事务一旦提交,结果就是永久性的(依靠redo.log日志保证)

        1.3 JDBC中事务管理


                关闭自动提交,提交 / 回滚

/**
 * @description: JDBC事务管理
 * @author: panhong
 * @date: 2025/3/19
 */
public class JDBCMain {
    public static void main(String[] args) throws SQLException {
        Statement stmt = null;
        Connection conn = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
            String sql = "insert into user values(null,'zs',25)";
            conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/btest_engine",
                    "root", "root");
            stmt = conn.createStatement();

            // 开启事务
            conn.setAutoCommit(false);

            int count = stmt.executeUpdate(sql);//影响的行数

            // 模拟异常
            int i = 1 / 0;

            // 提交事务
            conn.commit();
            System.out.println(count);
            if(count > 0){
                System.out.println("添加成功!");
            }else{
                System.out.println("添加失败!");
            }
        } catch (Exception e) {
            // 回滚事务
            conn.rollback();
            e.printStackTrace();
        } finally {

            if(stmt != null){
                try {
                    stmt.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }

            if(conn != null){
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2 Spring事务


        2.1 使用方式


                编程式事务

                声明式事务
                        添加@EnableTransactionManagement注解
                        添加@Transactional注解

        2.2 事务的传播行为


                ① PROPAGATION_REQUIRED:如果当前没有事务,就新建一个事务。如果当前存在事务,则加入这个事务。 
                ② PROPAGATION_NESTED:如果当前没有事务,就新建一个事务。如果当前事务存在,则执行一个嵌套事务。
                ③ PROPAGATION_REQUIRES_NEW:如果当前没有事务,就新建一个事务。如果当前存在事务,把当前事务挂起,并且自己创建一个新的事务给自己使用。 
                ④ PROPAGATION_SUPPORTS:如果当前没有事务,就以非事务方式执行。 如果当前有事务,则使用事务。
                ⑤ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
                ⑥ PROPAGATION_MANDATORY:以事务方式执行,如果当前没有事务,就抛出异常。 
                ⑦ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。 


        2.3 事务的隔离级别


                ① DEFAULT:使用数据库的默认隔离级别。
                        MySQL 的 InnoDB 存储引擎默认使用 REPEATABLE_READ,而 Oracle 通常使用 READ_COMMITTED

                ② READ_UNCOMMITTED:读未提交
                        会出现脏读、不可重复读和幻读问题

                ③ READ_COMMITTED:读已提交
                        会出现不可重复读和幻读问题

                ④ REPEATABLE_READ:可重复读
                        会出现幻读问题

                ⑤ SERIALIZABLE:可串行化
                        可以避免所有并发问题,但是效率极低

                不同隔离级别产生的并发问题。

                        ① 脏读:事务能够读取未提交的数据。
                        ② 不可重复读:当一个事务A在执行过程中,数据被另外一个事务B修改,造成本次事务前后读取的信息不一样
                        ③ 幻读:当一个事务A读取某一个范围的数据时,另一个事务B在这个范围插入一行数据,A事务再次读取这个范围的数据时,会产生幻读

3 Spring事务失效常见场景


        3.1 方法内的自调用


                Spring 事务是基于AOP的,只有使用代理对象调用某个方法时,Spring 事务才能生效
                而在一个方法中使用this.xxx()调用方法,this并不是代理对象,而是当前对象,所以会导致事务失效

@Component
public class UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    
    @Transactional
    public void methodA() {
        jdbcTemplate.update("insert into user values(null,'zhangsan',18)");
        int i = 1 / 0;
    }

    public void methodB() {
        this.methodA(); // 方法内的自调用
    }
}

执行后结果:

Spring事务失效的常见场景_第1张图片

        解决:
                ① 自己注入自己

// 自己注入自己
@Autowired
private UserService userService;
    
public void methodB() {
    userService.methodA();
}

                ② 把调用方法拆分到第二个Bean中

                ③ AopContext.currentProxy() + @EnableAspectJAutoProxy(exposeProxy = true)

public void methodB() {
        UserService userService = (UserService)AopContext.currentProxy();
        userService.methodA();
 }
@Configuration
@ComponentScan(basePackages = "com.pandy") 
@EnableTransactionManagement
@EnableAspectJAutoProxy(exposeProxy = true)
public class AppConfig {
    ......
}

3.2 方法是 private 修饰的

        Spring事务会基于CGLIB来进行AOP,而CGLIB会基于父子类来代理,子类是代理类,父类是被代理类。如果父类中的某个方法是private修饰的,那么子类就没有方法重写它,也就没有方法额外增加Spring事务的逻辑。

@Transactional
private void methodA() {
    jdbcTemplate.update("insert into user values(null,'zhangsan',18)");
    int i = 1 / 0;
}


3.3 方法是final修饰的

        原因同上


3.4 单独的线程调用方法

        MyBatis或jdbcTemplate执行SQL时,会从ThreadLocal中去获取数据库连接对象。如果开启事务的线程和执行SQL的线程是用一个,那么就能拿到数据库连接对象,如果不是同一个线程,那就拿不到数据库连接对象。这样MyBatis或jdbcTemplate就会自己去新建一个数据库连接来执行SQL,此数据库连接的autoCommit是true,那么执行SQL就会提交。后续再抛异常也就不能再回滚之前已经提交的SQL了。

@Transactional
public void methodA() {
	new Thread(() -> {
		jdbcTemplate.update("insert into user values(null,'zhangsan',18)");
	}).start();

	int i = 1 / 0;
}


3.5 没添加@Configuration注解

        如果用SpringBoot基本没有这个问题。
        但是如果用Spring,那么可能会有问题。MyBatis或jdbcTemplate会从ThreadLocal中去获取数据库连接对象,ThreadLocal中存储的是一个Map,Map的key为DataSource对象,value为Connection对象。如果我们没有添加@Configuration注解的话,会导致Map中存储的DataSource对象和MyBatis或jdbcTemplate中存储的DataSource对象不相等,从而拿不到数据库连接信息,导致自己去创建数据库连接对象。

//@Configuration
@ComponentScan(basePackages = "com.pandy") // 扫描指定包下的组件
@EnableTransactionManagement
public class AppConfig {
	......
}


3.6 Spring事务没有捕获到异常

        默认情况下,Spring会捕获RuntimeException和Error。

@Transactional
public void methodA() {
	jdbcTemplate.update("insert into user values(null,'zhangsan',18)");

	try {
		throw new IOException();
	} catch (IOException e) {
		e.printStackTrace();
	}
}


3.7 类没有被Spring管理


3.8 数据库不支持事务

后面两种方式Spring事务失效显而易见,就不详述了。

你可能感兴趣的:(spring,java,数据库)