Spring:事务的属性

事务属性:只读

对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。

对增删改操作设置只读会抛出下面异常:

Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
@Transactional(readOnly = true)
public String updateBalance(){
    String sql1 = "update t_user set balance = balance-50 where user_id = ? and username=?";
    int r1 = jdbcTemplate.update(sql1,1,"张三");
    System.out.println("r1="+r1);
    String sql2 = "update t_user set balance = balance+50 where user_id = ? and username=?";
    int r2 = jdbcTemplate.update(sql2,2,"李四");
    System.out.println("r2="+r2);
    //        int i =1/0;
    return "success";
}

@@Transactional(readOnly = true)在测试类中使用会失效,总之不要在测试类用相关事务了

查询还需要事务吗?干脆不设置事务性能不更高吗?

单条SELECT SQL语句可以不开启事务,多条SELECT SQL语句,并且多条SELECTSQL语句的查询结果有关联,比如用作统计、报表等功能,要求开启事务。但是为了提高效率,建议只读事务。

【原因】

如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;

如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。

事务属性:超时

超时回滚,释放资源。

执行过程中抛出异常:

org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Mon Sep 30 23:48:23 CST 2024

使用:

@Transactional(timeout = 2)
public String updateBalanceTimeout(){
    String sql1 = "update t_user set balance = balance-50 where user_id = ? and username=?";
    int r1 = jdbcTemplate.update(sql1,1,"张三");
    System.out.println("r1="+r1);
    try {
        TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    String sql2 = "update t_user set balance = balance+50 where user_id = ? and username=?";
    int r2 = jdbcTemplate.update(sql2,2,"李四");
    System.out.println("r2="+r2);

    return "success";
}

注意:如果超时反正在所以数据库操作之后,是并不影响事务的执行的,也就是事务也是可以成功提交,不会回滚

@Transactional(timeout = 2)
public String updateBalanceTimeout(){
    String sql1 = "update t_user set balance = balance-50 where user_id = ? and username=?";
    int r1 = jdbcTemplate.update(sql1,1,"张三");
    System.out.println("r1="+r1);

    String sql2 = "update t_user set balance = balance+50 where user_id = ? and username=?";
    int r2 = jdbcTemplate.update(sql2,2,"李四");
    System.out.println("r2="+r2);

    try {
        TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "success";
}

事务属性:回滚策略

声明式事务默认只针对运行时异常回滚,编译时异常不回滚。

可以通过@Transactional中相关属性设置回滚策略

  • rollbackFor属性:需要设置一个Class类型的对象(哪些异常回滚)
  • rollbackForClassName属性:需要设置一个字符串类型的全类名(哪些异常回滚)
  • noRollbackFor属性:需要设置一个Class类型的对象(那些异常不回滚)
  • rollbackFoClassNamer属性:需要设置一个字符串类型的全类名(那些异常不回滚)
// 表示所有的异常都回滚,除了ArithmeticException
//  @Transactional(rollbackFor=Exception.class,noRollbackFor = ArithmeticException.class)
//  @Transactional(rollbackForClassName="java.lang.Exception",noRollbackForClassName = "java.lang.ArithmeticException")
@Transactional(rollbackFor = Exception.class,noRollbackForClassName = "java.lang.ArithmeticException")
public void updateBalanceRollBack(){

    String sql1 = "update t_user set balance = balance-50 where user_id = ? and username=?";
    int r1 = jdbcTemplate.update(sql1,1,"张三");
    System.out.println("r1="+r1);
    int i = 1/0;
    String sql2 = "update t_user set balance = balance+50 where user_id = ? and username=?";
    int r2 = jdbcTemplate.update(sql2,2,"李四");
    System.out.println("r2="+r2);

}

事务隔离级别

各个隔离级别解决并发问题的能力见下表:

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE

各种数据库产品对事务隔离级别的支持程度:

隔离级别 Oracle MySQL
READ UNCOMMITTED ×
READ COMMITTED √(默认)
REPEATABLE READ × √(默认)
SERIALIZABLE
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化

事务属性:传播行为

一个视频教会你spring的事务传播行为_哔哩哔哩_bilibili

什么是事务的传播行为

当我们遇到如下的代码,在一个service中调用了另一个service的方法,此时AService的事务被传播到了BService中,这样就产生了事务的传播:

@service
public class Aservice {
    @Autowired
    private Bservice bservice:
    public void order(){
        yyyyyy();
        bservice.xxxx():
        zzzzz();
    }
}

因为两个service都存在事务,那么生成的的sql语句可能如下:

BEGIN;
    update yyyy;
    -- 两个平务的分界线-------------------
        begin;
        update xxxx;
        commit:
    -- --------------------------------
update zzz;
commit:

在这里很明显是存在问题的,当第二个begin执行时,会隐式的将第一个事务直接提交了,从而导致AService的部分事务失效。什么是事务的传播行为?

所以当【B事务】传播到【A事务】中时【B事务】需要做一下微调,微调的结果不外乎以下几种情况:

当AService有事务,AService也有事务

第一种情况:融入A事务(干掉B的事务)形成的sq如下,这种场景是最多的:

BEGIN;
    update yyyy;
    -- 两个平务的分界线-------------------

        update xxxx;

    -- --------------------------------
update zzz;
commit:

第二种情况:挂起A事务,让B事务独立于A事务运行。

当两个事务需要各自独立维护自身事务,单个事务则无法独立完成,B事务启动时可以暂时将A事务挂起,就是阻塞住,不给他继续发送sql,让他无法提交,B开启一个新的事务,B执行完成后A继续。

当AService无事务,而BService有事务

第一种情况:B事务以事务方式运行

select yyyy;
-- 分界线---------------
    begin;
    update xxxx;
    commit;
-- 分界线---------------
select zzzz;

第二种情况:B事务以无事务方式运行

select yyyy;
-- 分界线---------------

    update xxxx;

-- 分界线---------------
select zzzz;

嵌套事务

通过设置保存点,将内部的事务转化为通过保存点和回滚至保存点,实现类似两个事务的操作。

伪代码如下:

begin;
    update mystudent set score =100 where id =1:
    SAVEPOINT a;
        update mystudent set score =100 where id =3:
        update mystudent set score =100 where id =4;
        -- 以上的代码有问题则会回滚至保存点
    ROLLBACK to a:
    -- 后边的事务不会受到影响
    update mystudent set score=100 where id =2;
commit:

嵌套的过程如下:

  • 内部SAVEPOINT a后的代码如果有问题则直接回滚至保存点。
  • 整个事务的提交也不受内部【伪事务】的影响。

Spring定义的七种传播行为

REQUIRED

PROPAGATION.REQUIRED:这是Spring默认的传播行为,表示当前方法(被REQUIRED修饰的方法)必须运行在事务中。如果外层的调用方法有事务,则按照外层的事务运行,如果外层的方法没有事务,那么外层方法还是按照没有事务的方法运行,但是被调用的方法,会创建一个和原来一模一样的事物

外层方法有事务

AService 方法

// AService 方法
@Transactional(timeout = 5)
public void UpdateBalanceAndName(){

    String sql1 = "update t_user set balance = balance-50 where user_id = ? ";
    int r1 = jdbcTemplate.update(sql1,1);
    System.out.println("r1="+r1);
    //---------------分界线-------------------------------

    bService.updateName("王五",1);

    //---------------分界线-------------------------------
    String sql2 = "update t_user set balance = balance+50 where user_id = ? ";
    int r2 = jdbcTemplate.update(sql2,2);
    System.out.println("r2="+r2);

}

BService 方法

@Transactional(propagation = Propagation.REQUIRED,timeout = 2)
public void updateName(String name,Integer userId) {
    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("----------3秒了-----------");
    String sql = "update t_user set username = ? where user_id = ?";
    int r = jdbcTemplate.update(sql,name,userId);
    System.out.println("r="+r);
}

如果我们只运BService,那么无法修改数据库,会报错

@Test
public void test5(){
    bService.updateName("王五",1);
}
//错误日志:TransactionTimedOutException: Transaction timed out: deadline was Tue Oct 01 11:58:30 CST 2024

但是如果我们AService 调用BService,这样就可以成功,因为事务的隔离级别是 REQUIRED,会使用AService的事务,BService事务会失效,变成这样

@Transactional(timeout = 5)
public void UpdateBalanceAndName(){

    String sql1 = "update t_user set balance = balance-50 where user_id = ? ";
    int r1 = jdbcTemplate.update(sql1,1);
    System.out.println("r1="+r1);
    //---------------分界线-------------------------------

     try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("----------3秒了-----------");
    String sql = "update t_user set username = ? where user_id = ?";
    int r = jdbcTemplate.update(sql,"王五",1);
    System.out.println("r="+r);

    //---------------分界线-------------------------------
    String sql2 = "update t_user set balance = balance+50 where user_id = ? ";
    int r2 = jdbcTemplate.update(sql2,2);
    System.out.println("r2="+r2);

}

外层方法没有事务

// 外层的方法:这里是没事务的,所以还是按照没事务的来处理
public void UpdateBalanceAndName(){

        String sql1 = "update t_user set balance = balance-50 where user_id = ? ";
        int r1 = jdbcTemplate.update(sql1,1);
        System.out.println("r1="+r1);
        // int i=1/0; // 如果在这个地方发生异常,是不会回滚的
        //---------------分界线-------------------------------

        bService.updateName("王五",1);
        // int i=1/0; // 如果在这个地方发生异常,是不会回滚的
        //---------------分界线-------------------------------
        String sql2 = "update t_user set balance = balance+50 where user_id = ? ";
        int r2 = jdbcTemplate.update(sql2,2);
        System.out.println("r2="+r2);
}

// 这里是有事务的,Spring会在这里创建一个和我们配置一模一样的事物
@Transactional(propagation = Propagation.REQUIRED,readOnly = true )
    public void updateName(String name,Integer userId) {
        System.out.println("----------3秒了-----------");
        String sql = "update t_user set username = ? where user_id = ?";
        int r = jdbcTemplate.update(sql,name,userId);
        System.out.println("r="+r);
    }
}

其他的测试

// 外层方法:只回滚FileNotFoundException的异常
@Transactional(rollbackFor = FileNotFoundException.class)
public void UpdateBalanceAndName(){
    String sql1 = "update t_user set balance = balance-50 where user_id = ? ";
    int r1 = jdbcTemplate.update(sql1,1);
    System.out.println("r1="+r1);
    //---------------分界线-------------------------------

    bService.updateName("王五",1);

    //---------------分界线-------------------------------
    String sql2 = "update t_user set balance = balance+50 where user_id = ? ";
    int r2 = jdbcTemplate.update(sql2,2);
    System.out.println("r2="+r2);
}

// 内层方法:模拟一个 ArithmeticException
@Transactional(propagation = Propagation.REQUIRED )
public void updateName(String name,Integer userId) {
    System.out.println("----------3秒了-----------");
    String sql = "update t_user set username = ? where user_id = ?";
    int r = jdbcTemplate.update(sql,name,userId);
    int i = 1/0; // 模拟异常
    System.out.println("r="+r);
}

如果按照上诉理论,updateName方法最终会使用UpdateBalanceAndName的事务,也就是

  • update t_user set balance = balance-50 where user_id = ?生效
  • update t_user set username = ? where user_id = ? 生效
  • update t_user set balance = balance+50 where user_id = ? 不生效

相当于没有事务:

String sql1 = "update t_user set balance = balance-50 where user_id = ? "; 
int r1 = jdbcTemplate.update(sql1,1);
String sql = "update t_user set username = ? where user_id = ?";
int r = jdbcTemplate.update(sql,name,userId);
int i = 1/0; // 这里发生异常,后面的代码不运行,但是上面的代码对数据库的操作还是有效
String sql2 = "update t_user set balance = balance+50 where user_id = ? ";
int r2 = jdbcTemplate.update(sql2,2);

实际情况是,整体都回滚

原因:Spring默认会对运行时异常进行回滚。那段异常代码,我们没有try-catch处理,于是内部的方法向上抛异常,被Spring检测到了,就会自动回滚。


如果我们在AService上是这么配置的:不回滚ArithmeticException

@Transactional(noRollbackFor = ArithmeticException.class)
//    @Transactional(timeout = 5)
public void UpdateBalanceAndName(){
    String sql1 = "update t_user set balance = balance-50 where user_id = ? ";
    int r1 = jdbcTemplate.update(sql1,1);
    System.out.println("r1="+r1);
    // int i=1/0; // 如果在这个地方发生异常,是不会回滚的
    //---------------分界线-------------------------------

    bService.updateName("王五33",1);
    // int i=1/0; // 如果在这个地方发生异常,是不会回滚的
    //---------------分界线-------------------------------
    String sql2 = "update t_user set balance = balance+50 where user_id = ? ";
    int r2 = jdbcTemplate.update(sql2,2);
    System.out.println("r2="+r2);
}

会报如下日志:

12:59:07.112 [main] ERROR org.springframework.transaction.interceptor.TransactionInterceptor - Application exception overridden by commit exception
java.lang.ArithmeticException: / by zero
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

REQUIRED_NEW

PROPAGATION.REQUIRED_NEW:(需要个新的) 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果外面没有,就以自己独立的方式运行。

如果AService有没有事务,那么BService里面 的方法就要自己的事务,外层方法照样无事务运行。这个很好理解

下面我检验AService有事务的情况:

// AService 
@Transactional
public void UpdateBalanceAndName(){
    String sql1 = "update t_user set balance = balance-50 where user_id = ? ";
    int r1 = jdbcTemplate.update(sql1,1);
    System.out.println("r1="+r1);
    //---------------分界线-------------------------------

    bService.updateName("王五xx",2);
    //---------------分界线-------------------------------
    String sql2 = "update t_user set balance = balance+50 where user_id = ? ";
    int r2 = jdbcTemplate.update(sql2,2);
    int i = 1/0;
    System.out.println("r2="+r2);
}


// BService
@Transactional(propagation = Propagation.REQUIRES_NEW )
public void updateName(String name,Integer userId) {
    System.out.println("------updateName--------");
    String sql = "update t_user set username = ? where user_id = ?";
    int r = jdbcTemplate.update(sql,name,userId);
    System.out.println("r="+r);
}

在调用BService的updateName方法的是时候,AService的UpdateBalanceAndName的方法的事务会被挂起

BService的方法运行完(修改id=2的用户的名字),因为是两个独立的事物,所以BService的修改会被提交。

而AService的方法,后面遇到异常,AService相关修改就会被回滚

总结:外层方法的异常不会影响内层方法的事务提交,但是内层方法的异常会影响外层方法的提交

以下代码会导致Communications link failure

// AService 
@Transactional
public void UpdateBalanceAndName(){
    String sql1 = "update t_user set balance = balance-50 where user_id = ? ";
    int r1 = jdbcTemplate.update(sql1,1);
    System.out.println("r1="+r1);
    //---------------分界线-------------------------------

    bService.updateName("王五xx",1);// 这里是原因
    //---------------分界线-------------------------------
    String sql2 = "update t_user set balance = balance+50 where user_id = ? ";
    int r2 = jdbcTemplate.update(sql2,2); 
    int i = 1/0;
    System.out.println("r2="+r2);
}


// BService
@Transactional(propagation = Propagation.REQUIRES_NEW )
public void updateName(String name,Integer userId) {
    System.out.println("------updateName--------");
    String sql = "update t_user set username = ? where user_id = ?";
    int r = jdbcTemplate.update(sql,name,userId);
    System.out.println("r="+r);
}

错误日志:

org.springframework.dao.RecoverableDataAccessException: PreparedStatementCallback; SQL [update t_user set username = ? where user_id = ?]; Communications link failure

Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

原因:由于传播级别是REQUIRES_NEW,所以内层方法和外层方法是两个独立的事务,倒要修改id=1的数据,造成死锁

SUPPORTS

PROPAGATION.SUPPORTS:表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行。如果外层没有事务,该方法就以非事务运行。SUPPORTS表示支持,就是B事务会支持A事务。

@Transactional(propagation = Propagation.SUPPORTS )
public void updateName(String name,Integer userId) {
    System.out.println("------updateName--------");
    String sql = "update t_user set username = ? where user_id = ?";
    int r = jdbcTemplate.update(sql,name,userId);
    int i = 1/0;// 模拟异常
    System.out.println("r="+r);
}

另外,如果我们单独使用上面的代码,出现了异常也是不会回滚的。相当于这段代码没有使用

MANDATORY

PROPAGATION.MANDATORY:(强制性的)表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常。

// BService   
@Transactional(propagation = Propagation.MANDATORY )
public void updateName(String name,Integer userId) {
    System.out.println("------updateName--------");
    String sql = "update t_user set username = ? where user_id = ?";
    int r = jdbcTemplate.update(sql,name,userId);

    System.out.println("r="+r);
}

如果我单独使用或者在上层方法无事务都会报如下错误:

org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'

NOT_SUPPORTED

PROPAGATION.NOT_SUPPORTED:(不支持事务)表示该方法不应该运行在事务中。如果存在当前事务在该方法运行期间,当前事务将被挂起

// AService 
@Transactional
public void UpdateBalanceAndName(){
    String sql1 = "update t_user set balance = balance-50 where user_id = ? ";
    int r1 = jdbcTemplate.update(sql1,1);
    System.out.println("r1="+r1);
    //---------------分界线-------------------------------

    bService.updateName("王五xx",2);
    //---------------分界线-------------------------------
    String sql2 = "update t_user set balance = balance+50 where user_id = ? ";
    int r2 = jdbcTemplate.update(sql2,2);

    System.out.println("r2="+r2);
}


// BService
@Transactional(propagation = Propagation.NOT_SUPPORTED )
public void updateName(String name,Integer userId) {
    System.out.println("------updateName--------");
    String sql = "update t_user set username = ? where user_id = ?";
    int r = jdbcTemplate.update(sql,name,userId);
    int i = 1/0; // 模拟异常
    System.out.println("r="+r);
}

BService发生异常是不回滚的,AService接收到BService会回滚

NOT_SUPPORTED 和 REQUIRED_NEW 的异同

同:如果上层方法有事务,上层方法都是被挂起

异:

  • NOT_SUPPORTED 是在无事务下运行,那么相关的方法发生异常不回滚
  • REQUIRED_NEW 是在有事务下运行,那么相关方法发生异常会回滚

NEVER

PROPAGATION.NEVER:(不会运行在有事务的环境)表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常

异常为:

org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'

NEVER 和 MANDATORY 的异同

同:都是会直接抛异常

异:NEVER 要求不能有事务,MANDATORY 要求要有事务

  • 单独使用:NEVER 不抛异常,MANDATORY 抛异常
  • 上层无事务:NEVER 不抛异常,MANDATORY 抛异常
  • 上层有事务:NEVER 抛异常,MANDATORY 不抛异常

NESTED

PROPAGATION.NESTED:(嵌套的)表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务。

@Service
public class BService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(propagation = Propagation.NESTED )
    public void updateName(String name,Integer userId)  {
        try {
            System.out.println("------updateName--------");
            String sql = "update t_user set username = ? where user_id = ?";
            int r = jdbcTemplate.update(sql,name,userId);
            System.out.println("r="+r);
            int i = 1/0;
        } catch (DataAccessException e) {
            e.printStackTrace();
        }
    }
}




@Service
public class AService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private BService bService;

    @Transactional
    public void UpdateBalanceAndName(){
        String sql1 = "update t_user set balance = balance-50 where user_id = ? ";
        int r1 = jdbcTemplate.update(sql1,1);
        System.out.println("r1="+r1);
        //---------------分界线-------------------------------

        try {

            bService.updateName("李四",1);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //---------------分界线-------------------------------
        String sql2 = "update t_user set balance = balance+50 where user_id = ? ";
        int r2 = jdbcTemplate.update(sql2,2);
        System.out.println("r2="+r2);
    }
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1909773034@qq.com

×

喜欢就点赞,疼爱就打赏