tulip notes
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Star-Lord

希望一天成为大师的学徒
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 探索SpringBoot

    • 初步认识SpringBoot
    • 自定义starter
    • 一些常用注解
    • 自动装配
    • spring中开启事务
      • 什么是事务
        • 事务的特性
      • Spring中的事务
        • 编程式事务
        • 声明式事务
        • @Transactional实现原理
        • 注意事项
        • 代理不生效
        • 框架或底层不支持
        • 使用不当
        • 关于不建议使用@Transactional
        • 受检异常例子
        • 远程调用例子
    • Spring中事务的属性
    • SpringBoot启动流程
    • Spring中Bean的创建跟循环依赖
    • IOC的实现机制是什么
  • 常用功能实现

  • Security认证授权

  • 扩展

  • 实战与注意事项

  • 其它

  • 《SpringBoot》笔记
  • 探索SpringBoot
EffectTang
2023-10-01
目录

spring中开启事务

# Spring中开启事务

# 什么是事务

在编程中,事务(Transaction)是指一个包含了一组操作或步骤的程序单元,这些操作或步骤被封装在一起作为一个单独的执行单元来执行。更简单的说就是,一个有若干操作的集合。执行后,它们要么一起成功,要么一起失败。

# 事务的特性

事务有四大特性(ACID),原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)四个属性,这些属性被称为ACID属性。

  • 原子性:事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。
  • 隔离性:事务的执行不会影响其他事务的执行。
  • 持久性:一旦事务提交,其修改的效果就是永久性的。
  • 一致性:事务的执行不能破坏数据库数据的完整性和一致性(必须使数据库从一个一致性状态变换到另一个一致性状态)。

# Spring中的事务

在简单的了解事务后,接下来看看在Spring中如何实现事务。

Spring 支持两种类型的事务:声明式事务和编程式事务。

# 编程式事务

它一种通过编程方式控制事务边界的方法,它提供了更加灵活的事务控制方式,允许开发人员根据业务需求自定义事务的边界——由开发人员手动控制事务的开启、提交和回滚,以实现对事务的精细控制。

Spring提供了PlatformTransactionManager接口和TransactionTemplate类来实现编程式事务管理。

开发人员可以通过注入TransactionTemplate实例,并使用该实例的execute方法来执行包含事务操作的业务逻辑。

  • 以下是通过TransactionTemplate类来实现事务管理的方法
@Autowired
private TransactionTemplate transactionTemplate;

public void updateUser(User user) {
    try {
        transactionTemplate.execute(status -> {
            // 在事务中执行业务逻辑
            // ...
            return null;
        });
    } catch (Exception e) {
        // 发生异常时回滚事务
        transactionTemplate.rollback(status -> {
            // 回滚事务
            return null;
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

而PlatformTransactionManager接口定义了3个基本的事务操作方法,具体的实现都是由不同的子类来实现的。它有众多实现,但是我们并不需要掌握这些具体实现类的用法,我们只需要掌握好 PlatformTransactionManager 的用法即可。例如:如果你使用的是 JDBC 那么可以将 DataSourceTransactionManager 作为事务管理器。

public interface PlatformTransactionManager extends TransactionManager {
  TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
  void commit(TransactionStatus status) throws TransactionException;
  void rollback(TransactionStatus status) throws TransactionException;
}
// 源码
1
2
3
4
5
6
  • 以下是使用PlatformTransactionManager的实现类DataSourceTransactionManager(springboot内置的)来实现编程式事务
@Autowired
    private DataSourceTransactionManager transactionManager;

    public void updateName(){
        // 定义事务属性
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        // 获取事务状态
        TransactionStatus status = transactionManager.getTransaction(def);
        try {
            // 在此处执行数据库操作或其他业务逻辑
            String username = "ready to one+1";
            Integer num = 1;
            workersMapper.updateUserName(username, num);
            int res = 1/0;
            // 提交事务
            transactionManager.commit(status);

        } catch (Exception e) {
            // 发生异常时回滚事务
            transactionManager.rollback(status);
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

上述例子中,会发生异常ArithmeticException,从而导致事务回滚,不会对数据库进行更新。

以上就是编程式事务,可以发现——它将事务的控制跟业务代码混合在了一起,这一定程度上增加了代码的复杂性。且在每个操作前后都需要手动调用事务管理器的方法,这可能会增加出错的可能性。如果在一些业务复杂的情况下,该情况尤为明显。

因此,对于复杂的业务逻辑,通常建议使用声明式事务管理,因为它更简洁、更易于管理。

# 声明式事务

使用声明式事务——首先,需要在Spring的配置文件中配置一个PlatformTransactionManager的实现,例如DataSourceTransactionManager。然后,可以在需要使用事务的方法或类上添加注解,例如@Transactional注解,来开启事务。

  • 配置事务管理器
@Configuration
public class AppConfig {
    
    @Autowired
    private DataSource dataSource;

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource);
    }
}
1
2
3
4
5
6
7
8
9
10
11
  • 通过注解来开启事务
		@Transactional
    public void test(){
        String username = "part+1";
        Integer num = 1;
        workersMapper.updateUserName(username, num);
        int res = 1/0;
    }
1
2
3
4
5
6
7

@Transactional注解开启了事务,test方法在事务的保护下执行,如果在执行过程中出现了异常,事务会被自动回滚。

以上就是spring中使用事务的两种方式

# @Transactional实现原理

  • 它是声明式事务,它是不侵入代码的,之所以有这个特性,是跟它实现的原理有关

@Transactional注解的逻辑是通过动态代理来实现的,而生成这个动态代理类分成了两步: 1、向spring容器注册事务相关的切面逻辑 2、根据切面逻辑生成动态代理

当一个方法被标记为 @Transactional 时,Spring 会创建一个代理对象(JDK 动态代理或 CGLIB 代理),这个代理对象会在方法调用前开启事务,在方法调用后提交或回滚事务。

  • 以下为参考讲解链接
  • 掘金文章讲解 (opens new window)

动态代理为标注了@Transactional注解的方法增加切面逻辑,而事务的上下文包括数据库链接都是通过ThreadLocal来传递。

具体的执行则是通过反射实现。

Java 反射(Reflection)是一种强大的机制,允许程序在运行时检查和操作类、字段、方法和构造函数等类的内部结构。通过反射,你可以在运行时获取类的信息,并且可以动态地调用方法、访问字段、创建对象等。这种能力使得 Java 反射在很多场景下非常有用,比如框架开发、动态代理、插件系统等。

# 注意事项

使用注解@Transactional会有失效的情况,主要可以分为以下三大类:

# 代理不生效

  1. 在同一类中调用该类的另一方法,另一方法的事务不会生效,因为同一类中方法相互调用不会被方法拦截器拦截到,因此没有经过Spring的代理类。默认只有在外部调用事务才会生效。即相同类里边,A 方法没有事务,B 方法有事务,A 方法调用 B 方法,则 B 方法的事务会失效。
  2. 被final或static修饰的方法,也无法生效
  3. 使用的方法为非public方法
  4. 当前类没被spring管理,换句话说就是,没有在类上标注@Service、@Component等注解,这个类就不会被加载成一个Bean,这个类就不会被Spring管理了,事务就失效了。

关于final的原因

  • 不可重写:final 方法不能被子类重写。这意味着即使通过 JDK 动态代理或 CGLIB 代理生成了代理类,代理类也无法覆盖 final 方法。

关于static的原因:

  • 不属于对象:static 方法属于类而不是对象。代理对象是针对对象的,因此无法拦截 static 方法的调用。

Spring 使用代理模式来拦截带有 @Transactional 注解的方法调用,并在方法调用前后插入事务管理的逻辑。因此,只有被 Spring 管理的类(即由 Spring 容器创建和管理的 Bean)才能正确地应用 @Transactional 注解。如果一个类没有被 Spring 管理,@Transactional 注解就会失效。

# 框架或底层不支持

  1. spring的事务注解@Transactional只能应用到 public 方法上才会有效。其他类型(如:private)的方法虽然不报错,但不会生效。
  2. 多线程调用
  3. 数据库本身不支持事务

@Transactional 注解只能应用到 public 方法上才会有效,这是由 Spring AOP 的实现机制决定的。

下面是简要解释:

Spring AOP 的设计决策之一是只支持 public 方法。主要原因如下:

  • 代理对象的可见性:代理对象需要能够访问目标方法,public 方法是最容易被访问的。
  • 一致性:确保所有被代理的方法都具有相同的可见性,避免混淆和潜在的问题。
  • 性能:public 方法的代理实现更为简单和高效。

# 使用不当

  1. 异常被方法内部try catch捕获,未抛出
  2. rollbackFor属性设置错误
  3. 设置不支持事务

建议事务注解 @Transactional 一般添加在实现类上,而不要定义在接口上,如果加在接口类或接口方法上时,只有配置基于接口的代理这个注解才会生效。

# 关于不建议使用@Transactional

在阿里的开发手册中可以看到,它是不推荐使用@transactional注解的。因为它会有很多注意事项,一不注意就会导致事务失效,且还容易导致接口效率变慢————比如:方法中涉及到远程方法调用,或者方法中涉及到多个读操作。甚至出错,不容易排查,比如:方法中不小心捕获了异常,没有抛出,或者是抛出了异常是非受检异常,导致事务无法回滚。尤其是在复杂系统中。

个人认为其根本原因在于@transactional锁的粒度太大导致的。以下是2个例子:

# 受检异常例子

下面是一个伪代码

@Transactional
public 某个结算任务(支付参数) throws ParseException{
    // 前置检查
  	xxxx
     for(){
       .....
     }
  ....
}
1
2
3
4
5
6
7
8
9

如果参数中,带有日期字段,如果在这执行途中抛出了解析异常,该方法是不会回滚的。如果它的偏差只有几分钱,你可能不会注意到,因为该方法不会报错。日期月累下来,突然某一天要结算时,你会发现差额会异常的大。

因为解析异常是受检异常,即使抛出也不会回滚。

当然,它的解决方法也有,比如,加上rollbackFor属性

@Transactional(rollbackFor=Exception.class)
public 某个结算任务(支付参数) throws ParseException{
    // 前置检查
  	xxxx
     for(){
       .....
     }
  ....
}
1
2
3
4
5
6
7
8
9

这样一来就变成了任何异常都会回滚,当然你也可以指定更细粒度的异常。

# 远程调用例子

以下的事务可以正常实现,但遇到流量高峰期,会导致系统异常缓慢:

@Transactional(rollbackFor=Biz.class)
public Boolean service(){
  	queryTable1();
  	
  // 下列三个是 外部服务调用
  	outerServiceA();
  	outerServiceB();
  	outerServiceC();
  	
  	updateTable1();
  	updateTable2();
  
  	return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

如果外部服务异常,返回就会很慢,但因为之前就已经开启了事务,所以queryTable1()方法,会造成大量事务,大量的锁堆积在数据库,从而影响其他服务对数据库的访问,导致整个系统响应很低,甚至于说系统垮掉。

当然解决方案也是有的,比如使用编程式事务:

@Resource
private TransactionTemplate transactionTemplate;

@Transactional(rollbackFor=Biz.class)
public Boolean service(){
  	queryTable1();
  	
  // 下列三个是 外部服务调用
  	outerServiceA();
  	outerServiceB();
  	outerServiceC();
  	
  	//5.事务统一处理
    transactionTemplate.execute((transactionstatus)->-
      try{
          //事务尽可能尽可能的小
          updateTable1();
          updateTable2();
      }catch (Exception e){
          transactionStatus.setRollbackOnly();
          log.error("[服务] 更新错误!",e);

      }
       return transactionStatus;                     
    });
  	return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

以上的目的是为了提高事务的精确度,这也是不提倡用注解事务的原因,它的粒度太大了。

有人可能会说,把更新的方法抽离出来,再使用@transactional可以吗?当然可以的,但前提是你别在本类中调用,否则它无法生效。

当然,如果你可以保证事务的粒度,使用注解@transactional也没事,毕竟官网是推荐使用它的,因为它的的确确很方便。

上次更新: 2025/04/23, 16:23:16
自动装配
Spring中事务的属性

← 自动装配 Spring中事务的属性→

最近更新
01
面向切面跟自定义注解的结合
05-22
02
时间跟其他数据的序列化
05-19
03
数据加密与安全
05-17
更多文章>
Theme by Vdoing | Copyright © 2023-2025 EffectTang
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式