事务传播特性和隔离级别

事务常用属性

事务有以下几个常用属性:

a.read-only:设置该事务中是否允许修改数据。(对于只执行查询功能的事务,设置为TRUE可以提高事务的执行速度)

b.propagation:事务的传播机制。一般设置为required。可以保证在事务中的代码只在当前事务中运行,防止创建多个事务。

c.isolation:事务隔离级别。不是必须的。默认值是default。

d.timeout:允许事务运行的最长时间,以秒为单位。

e.rollback-for:触发回滚的异常。

f.no-rollback-for:不会触发回滚的异常。

  • 实际开发中,对于只执行查询功能的事务,要设置read-only为TRUE,其他属性一般使用默认值即可。

注意事项

注意同一个的方法里面嵌套事务,会让嵌套的事务失效。

  1. 接口中A、B两个方法,A无@Transactional标签,B有,上层通过A间接调用B,此时事务不生效。

  2. 接口中异常(运行时异常)被捕获而没有被抛出。
    默认配置下,spring 只有在抛出的异常为运行时 unchecked 异常时才回滚该事务,
    也就是抛出的异常为RuntimeException 的子类(Errors也会导致事务回滚),
    而抛出 checked 异常则不会导致事务回滚 。可通过 @Transactional rollbackFor进行配置。

  3. 多线程下事务管理因为线程不属于 spring 托管,故线程不能够默认使用 spring 的事务,
    也不能获取spring 注入的 bean 。
    在被 spring 声明式事务管理的方法内开启多线程,多线程内的方法不被事务控制。
    一个使用了@Transactional 的方法,如果方法内包含多线程的使用,方法内部出现异常,
    不会回滚线程中调用方法的事务。

rollbackFor属性

rollbackFor=Exception.class 和rollbackFor=RuntimeException.class

Exception.class在遇到运行时异常和非运行时异常的时候都会回滚。(没有捕获异常的情况下)

RuntimeException.class。只对运行时异常回滚。 (如果不加rollbackFor属性,那么默认就是运行时异常回滚数据)

readOnly属性

因为只读的接口就不需要事务管理,由于配置了@Transactional就需要AOP拦截及事务的处理,可能影响系统性能。如果想放在实现类上,可以加一个readOnly=true,忽略那些不需要事务的方法,比如读取数据。

经常见到的3种事务不回滚的产生原因:

(1)声明式事务配置切入点表达式写错了,没切中Service中的方法
(2)Service方法中,把异常给try catch了,但catch里面只是打印了异常信息,没有手动抛出RuntimeException异常
(3)Service方法中,抛出的异常不属于运行时异常(如IO异常),因为Spring默认情况下是捕获到运行时异常就回滚

七个传播特性

PROPAGATION_REQUIRED

PROPAGATION_REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中

主事务里面执行,一个子事务。

// 第一个类TransactionService
@Transactional(rollbackFor=Exception.class)
    public void pay(ScanPayTransDO scanPayTransDO)  {

        int index = scanPayManager.save(scanPayTransDO);

        log.info("index=============={}",index);
        try {
            insertService.insert(scanPayTransDO);
        } catch (Exception e){
            log.info("操作异常,",e);
        }

    }
// 第二个类 InsertService
@Transactional(rollbackFor=Exception.class,propagation = Propagation.REQUIRED)
    public void  insert(ScanPayTransDO scanPayTransDO){
        String ss = TaskDateUtils.getCurrentDateTimeStr("HHmmss");
        scanPayTransDO.setBusinessNo("txtest000000000001"+ "_"+ss);
        int index =scanPayManager.save(scanPayTransDO);
        log.info("子事务插入:{}",index);
        int i = 1/0;
    }

这种情况下,两条sql都不会插入数据,即使在TransactionService捕获了异常也不会插入数据,因为这里的传播特性设置为REQUIRED。spring事务传播机制默认是REQUIRED,也就是说支持当前事务,如果当前没有事务,则新建事务,如果当前存在事务,则加入当前事务,合并成一个事务。这里面的两个事务合并成了一个事务

REQUIRES_NEW

REQUIRES_NEW: 新建事务,如果当前存在事务,则把当前事务挂起,这个方法会独立提交事务,不受调用者的事务影响,父级异常,它也是正常提交,但如果是此方法发生异常未被捕获处理,且异常满足父级事务方法回滚规则,则父级方法事务会被回滚

情景一:把上面案例修改下,将第二个事务特性更改为REQUIRES_NEW。第一个类不变。子方法抛出异常,主方法捕获异常,
此时可以发现,主方法里面可以正常插入数据,子方法里面插入失败了。注意,如果这里在主方法里面没有捕获子方法跑出来的异常,主方法同样不能正常插入数据。

// 第一个类TransactionService
@Transactional(rollbackFor=Exception.class)
    public void pay(ScanPayTransDO scanPayTransDO)  {

        int index = scanPayManager.save(scanPayTransDO);

        log.info("index=============={}",index);
        try {
            insertService.insert(scanPayTransDO);
        } catch (Exception e){
            log.info("操作异常,",e);
        }

    }
// 第二个类 InsertService
@Transactional(rollbackFor=Exception.class,propagation = Propagation.REQUIRES_NEW)
    public void  insert(ScanPayTransDO scanPayTransDO){
        String ss = TaskDateUtils.getCurrentDateTimeStr("HHmmss");
        scanPayTransDO.setBusinessNo("txtest000000000001"+ "_"+ss);
        int index =scanPayManager.save(scanPayTransDO);
        log.info("子事务插入:{}",index);
        int i = 1/0;
    }

情景二:比如下面案例,去掉主方法的异常捕获,主方法同样不能插入数据

// 第一个类TransactionService
@Transactional(rollbackFor=Exception.class)
    public void pay(ScanPayTransDO scanPayTransDO)  {

        int index = scanPayManager.save(scanPayTransDO);

        log.info("index=============={}",index);
        // try {
            insertService.insert(scanPayTransDO);
        //} catch (Exception e){
        //    log.info("操作异常,",e);
        //}

    }
// 第二个类 InsertService
@Transactional(rollbackFor=Exception.class,propagation = Propagation.REQUIRES_NEW)
    public void  insert(ScanPayTransDO scanPayTransDO){
        String ss = TaskDateUtils.getCurrentDateTimeStr("HHmmss");
        scanPayTransDO.setBusinessNo("txtest000000000001"+ "_"+ss);
        int index =scanPayManager.save(scanPayTransDO);
        log.info("子事务插入:{}",index);
        int i = 1/0;
    }

情景三:子方法正常,主方法抛出异常。那么子方法独立提交事务,不受调用者的事务影响,父级异常,子方法它也是正常提交,

    @Transactional(rollbackFor=Exception.class)
    public void pay(ScanPayTransDO scanPayTransDO)  {

        int index = scanPayManager.save(scanPayTransDO);

        log.info("index=============={}",index);

        insertService.insert(scanPayTransDO); // 子方法正常执行,没有异常

        int i =  1/0; // 主方法抛出异常

    }

    @Transactional(rollbackFor=Exception.class,propagation = Propagation.REQUIRES_NEW)
    public void  insert(ScanPayTransDO scanPayTransDO){
        String ss = TaskDateUtils.getCurrentDateTimeStr("HHmmss");
        scanPayTransDO.setBusinessNo("txtest000000000001"+ "_"+ss);
        int index =scanPayManager.save(scanPayTransDO);
        log.info("子事务插入:{}",index);
    }

NESTED

NESTED: 如果当前存在事务,它将会成为父级事务的一个子事务,方法结束后并没有提交,只有等父事务结束才提交,如果当前没有事务,则新建事务(此时,类似于REQUIRED ),如果子方法异常,它本身进行事务回滚,父级可以捕获它的异常而不进行回滚,正常提交(父级不捕获也就也回滚),但如果父级异常,子方法必然回滚。

情景一:子方法设置NESTED,子方法抛出异常,主方法没有捕获异常,主方法和子方法都会回滚,不能插入数据。
情景二:子方法设置NESTED,子方法抛出异常,主方法捕获了异常,子方法回滚不能插入数据。主方法正常插入数据 (这就是跟REQUIRED 的区别,REQUIRED 里即使主方法捕获了异常,主方法也不能插入数据,同样回滚)

// 第一个类TransactionService
@Transactional(rollbackFor=Exception.class)
    public void pay(ScanPayTransDO scanPayTransDO)  {

        int index = scanPayManager.save(scanPayTransDO);

        log.info("index=============={}",index);
        // try { // 如果子方法出现了异常,主方法没有捕获异常,父子一起回滚。主方法捕获了异常,子方法回滚
            insertService.insert(scanPayTransDO);
        //} catch (Exception e){
        //    log.info("操作异常,",e); 
        //}

    }
// 第二个类 InsertService
@Transactional(rollbackFor=Exception.class,propagation = Propagation.NESTED)
    public void  insert(ScanPayTransDO scanPayTransDO){
        String ss = TaskDateUtils.getCurrentDateTimeStr("HHmmss");
        scanPayTransDO.setBusinessNo("txtest000000000001"+ "_"+ss);
        int index =scanPayManager.save(scanPayTransDO);
        log.info("子事务插入:{}",index);
        int i = 1/0;
    }

情景三:子方法设置NESTED,子方法没有异常,主方法最后抛出异常
此时会发现,主方法和子方法都没有插入数据,主方法异常时候,子方法必然一起回滚(这里就是跟REQUIRES_NEW的最大区别,会受到主方法的影响)

// 第一个类TransactionService
@Transactional(rollbackFor=Exception.class)
    public void pay(ScanPayTransDO scanPayTransDO)  {

        int index = scanPayManager.save(scanPayTransDO);

        log.info("index=============={}",index);
         try { // 如果子方法没有异常,主方法有异常
            insertService.insert(scanPayTransDO);
        } catch (Exception e){
            log.info("操作异常,",e); 
        }
		int  i = 1/0; // 主方法这里设置异常,那么

    }
// 第二个类 InsertService
@Transactional(rollbackFor=Exception.class,propagation = Propagation.NESTED)
    public void  insert(ScanPayTransDO scanPayTransDO){
        String ss = TaskDateUtils.getCurrentDateTimeStr("HHmmss");
        scanPayTransDO.setBusinessNo("txtest000000000001"+ "_"+ss);
        int index =scanPayManager.save(scanPayTransDO);
        log.info("子事务插入:{}",index);
        // int i = 1/0;  子方法异常去掉,正常执行
    }

SUPPORTS

SUPPORTS:如果当前存在事务,则加入事务,如果当前不存在事务,则以非事务方式运行。

如果当前存在事务,则加入事务,此时类似于REQUIRED,只要子方法有异常,主方法必然一起回滚,不管主方法有没有捕获异常。
如果当前不存在事务,则以非事务方式运行。(此时就是正常的非事务执行)

NOT_SUPPORTED

NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。 不管当前有没有存在事务,都将当前事务挂起。(不管主方法或者调用者里面有没有事务,子方法都忽略当前事务,以非事务方式正常执行),如果子方法里面抛出了异常,主方法回滚,子方法不受影响

NEVER

NEVER 以非事务方式运行,如果当前存在事务,则抛出异常,即父级方法必须无事务。

MANDATORY

MANDATORY:如果当前存在事务,则运行在当前事务中,如果当前无事务,则抛出异常,即父级方法(调用此方法的方法)必须有事务。
如果当前存在事务,则运行在当前事务中,此时类似REQUIRED,子方法有异常,主方法必然一起回滚,不管主方法有没有捕获异常。
如果当前无事务,则抛出异常,即父级方法(调用此方法的方法)必须有事务

@EnableTransactionManagement // 启注解事务管理,等同于xml配置方式的 <tx:annotation-driven />
@SpringBootApplication
public class ProfiledemoApplication {
 
    @Bean
    public Object testBean(PlatformTransactionManager platformTransactionManager){
        System.out.println(">>>>>>>>>>" + platformTransactionManager.getClass().getName());
        return new Object();
    }
 
    public static void main(String[] args) {
        SpringApplication.run(ProfiledemoApplication.class, args);
    }
}

如果项目做的比较大,添加的持久化依赖比较多,我们还是会选择人为的指定使用哪个事务管理器。

对于同一个工程中存在多个事务管理器要怎么处理,请看下面的实例,具体说明请看代码中的注释。实现TransactionManagementConfigurer 接口,定义多个PlatformTransactionManager ,指定一个默认的即可

@EnableTransactionManagement // 开启注解事务管理,等同于xml配置文件中的 <tx:annotation-driven />
@SpringBootApplication
public class ProfiledemoApplication implements TransactionManagementConfigurer {
 
    @Resource(name="txManager2")
    private PlatformTransactionManager txManager2;
 
    // 创建事务管理器1
    @Bean(name = "txManager1")
    public PlatformTransactionManager txManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
 
    // 创建事务管理器2
    @Bean(name = "txManager2")
    public PlatformTransactionManager txManager2(EntityManagerFactory factory) {
        return new JpaTransactionManager(factory);
    }
 
    // 实现接口 TransactionManagementConfigurer 方法,其返回值代表在拥有多个事务管理器的情况下默认使用的事务管理器
    @Override
    public PlatformTransactionManager annotationDrivenTransactionManager() {
        return txManager2;
    }
 
    public static void main(String[] args) {
        SpringApplication.run(ProfiledemoApplication.class, args);
    }
 
}
 
@Component
public class DevSendMessage implements SendMessage {
 
    // 使用value具体指定使用哪个事务管理器
    @Transactional(value="txManager1")
    @Override
    public void send() {
        System.out.println(">>>>>>>>Dev Send()<<<<<<<<");
        send2();
    }
 
    // 在存在多个事务管理器的情况下,如果使用value具体指定
    // 则默认使用方法 annotationDrivenTransactionManager() 返回的事务管理器
    @Transactional
    public void send2() {
        System.out.println(">>>>>>>>Dev Send2()<<<<<<<<");
    }
 
}

隔离级别

  • 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据

  • 提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)

  • 可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读

  • 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

以下转:https://www.cnblogs.com/shoshana-kong/p/10516404.html

1.数据库默认隔离级别: mysql —repeatable,oracle,sql server —read commited

2.mysql binlog的格式三种:statement,row,mixed

3.为什么mysql用的是repeatable而不是read committed:在 5.0之前只有statement一种格式,而主从复制存在了大量的不一致,故选用repeatable

4.为什么默认的隔离级别都会选用read commited 原因有二:repeatable存在间隙锁会使死锁的概率增大,在RR隔离级别下,条件列未命中索引会锁表!而在RC隔离级别下,只锁行。

2.在RC级用别下,主从复制用什么binlog格式:row格式,是基于行的复制!

开始我们的内容,相信大家一定遇到过下面这些问题

面试官:“mysql有几个事务隔离级别?”
你:“读未提交,读已提交,可重复读,串行化四个!默认是可重复读”
面试官:“为什么mysql选可重复读作为默认的隔离级别?”
(你面露苦色,不知如何回答!)
面试官:“你们项目中选了哪个隔离级别?为什么?”
你:“当然是默认的可重复读,至于原因。。呃。。。”
(然后你就可以回去等通知了!)

为了避免上述尴尬的场景,请继续往下阅读!
Mysql默认的事务隔离级别是可重复读(Repeatable Read),那互联网项目中Mysql也是用默认隔离级别,不做修改么?
OK,不是的,我们在项目中一般用读已提交(Read Commited)这个隔离级别!
what!居然是读已提交,网上不是说这个隔离级别存在不可重复读和幻读问题么?不用管么?好,带着我们的疑问开始本文!

即:默认的是可重复读,但是我们做了修改成了读已提交
为什么mysql设置为可重复读,历史原因!
mysql5.1之前,binlog为STATEMENT格式。在master上执行的顺序为先删后插!而此时binlog为STATEMENT格式,它记录的顺序为先插后删!从(slave)同步的是binglog,因此从机执行的顺序和主机不一致!就会出现主从不一致!
将binglog的格式修改为row格式,此时是基于行的复制,自然就不会出现sql执行顺序不一样的问题!奈何这个格式在mysql5.1版本开始才引入。因此由于历史原因,mysql将默认的隔离级别设为可重复读(Repeatable Read),保证主从复制不出问题! 我们代码中可以手动设置隔离级别。

在RC级别下,主从复制用什么binlog格式?
OK,在该隔离级别下,用的binlog为row格式,是基于行的复制!Innodb的创始人也是建议binlog使用该格式!

互联网项目请用:读已提交(Read Commited)这个隔离级别!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EmineWang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值