事务

逻辑上是一组操作,要么执行要么不执行

ACID 的 4 个重要特性:

原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。
事务隔离(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

2. Spring的事务支持:

2.1 编程式事务:

将事务管理代码嵌入到业务代码里,来控制事务提交和回滚

 

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

@Service
@Slf4j
public class SpringTransaction {
    @Autowired
    private MongoTemplate mongoTemplate;
    @Autowired
    private TransactionTemplate transactionTemplate;

    public void transactionTest(){
        User user=new User();
        user.setAge(22);
        user.setId(1);
        user.setName("Chen");
        user.setSex("male");
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                try {
                    mongoTemplate.save(user, "testone");
                    //int i=1/0; //条件1
                } catch (Exception e){
                    status.setRollbackOnly();
                }
            }
        });
    }
}

条件1注释掉情况下,数据可以写入到db中,但如果条件1不注释,会抛出exception,然后执行rollback操作,数据不会写入到Mongo

注意在没有使用编程式事务时,如果在mongo save完之后出现某种异常数据是会存到mongo的,不会发生rollback

 

 

2.2 声明式事务:@EnableTransactionManagement  @Transactional

将事务管理从业务代码里抽离,以声明的方式来实现事务管理。声明式事务原理是AOP代理,即通过自动创建bean的proxy实现

2.2.1 mongoTemplate 声明式事务

 mongoDB在4.0及以上版本才支持在4.x之后版本开始支持复本集的事务和分片模式的事务 ,所以springboot版本需要2.6.3及以上 + @EnableTransactionManagement + MongoTransactionManager  bean

 

build.gradle

implementation group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version: springBootVersion
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-mongodb', version: springBootVersion
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: springBootVersion

 

springboot启动类

启动类加上**@EnableTransactionManagement** 注解, 并注入 MongoTransactionManager 事务管理器

@Bean
public MongoTransactionManager transactionManager(MongoDatabaseFactory mongoDatabaseFactory){
return new MongoTransactionManager(mongoDatabaseFactory);
}
@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class})
@EnableTransactionManagement //非必须,因为springboot在TransactionAutoConfiguration类里为我们自动配置了启用了这个注解
public class Application { //NOSONAR

    public static void main(String[] args) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
        SpringApplication.run(Application.class, args);  //NOSONAR
    }

    @Bean
    public MongoTransactionManager transactionManager(MongoDatabaseFactory mongoDatabaseFactory){
        return new MongoTransactionManager(mongoDatabaseFactory);
    }
}

 

测试类

@Compoent
public class MongoDbTransactionTest {
  @Autowired
  private MongoTemplate mongoTemplate;
  @Autowired
  private TransactionTemplate transactionTemplate;
    //声明式事务
    @Transactional(rollbackFor = {Exception.class})
    public void plantFormTransaction(){
        Integer reduceMoney=80;
        User user=new User();
        user.setAge(22);
        user.setId(3);
        user.setName("ZhaoSan");
        user.setSex("male");
        user.setMoney(reduceMoney);
        this.mongoTemplate.insert(user, "testone");
        int i=1/0;  //条件1
        transfer(20);
    }

    private void transfer(Integer money){
        System.out.println("transfer "+ money+"$ from bank");
    }
}

 

当条件1注释后,数据成功写入mongo

当条件1放开后,数据没有写入mongo。通过如下log可以看出,首先是准备写入,但后边由于1/0程序异常,紧接着log显示insert失败原来的inert回滚

2022-12-10 09:28:13.108 o.s.d.m.c.MongoTemplate http-nio-8080-exec-1 [DEBUG] Inserting Document containing fields: [_id, name, age, sex, money, _class] in collection: testone

 

2022-12-10 09:28:30.938 o.m.d.p.command http-nio-8080-exec-1 [DEBUG] Sending command '{"insert": "testone", "ordered": true, "$db": "test", "$clusterTime": {"clusterTime": {"$timestamp": {"t": 1670635698, "i": 2}}, "signature": {"hash": {"$binary": {"base64": "55L+xrLa2E1NrHvxzvUco5unsmw=", "subType": "00"}}, "keyId": 7129165744469704705}}, "lsid": {"id": {"$binary": {"base64": "GmnPFHv4RNi9iWvQs1MgxA==", "subType": "04"}}}, "txnNumber": 1, "startTransaction": true, "autocommit": false, "documents": [{"_id": 3, "name": "ZhaoSan", "age": 22, "sex": "male", "money": 80, "_class": "com.service.qftest.User"}]}' with request id 21 to database test on connection [connectionId{localValue:7, serverValue:99383}] to server

2.2.2 回滚事务

默认情况下,如果发生了RuntimeException,Spring的声明式事务将自动回滚。在一个事务方法中,如果程序判断需要回滚事务,只需抛出RuntimeException,例如:

@Transactional
public buyProducts(long productId, int num) {
    ...
    if (store < num) {
        // 库存不够,购买失败:
        throw new IllegalArgumentException("No enough products");
    }
    ...
}

如果要针对Checked Exception回滚事务,需要在@Transactional注解中写出来:

@Transactional(rollbackFor = {RuntimeException.class, IOException.class})
public buyProducts(long productId, int num) throws IOException {
    ...
}

上述代码表示在抛出RuntimeExceptionIOException时,事务将回滚。

 

2.2.3 Spring事务失效的场景

2.2.3.1 数据库引擎不支持事务

MySql中MyISAM引擎不支持事务操作,只有InnoDB才支持事务

2.2.3.2 Service没有被Spring管理

没有添加 @Component @Service注解,这个类不会被Spring管理,就不会被加载成一个bean

2.2.3.3 方法不是public

@Transactional只能用在public方法上

2.2.3.4 自身调用问题 

@Service
public class OrderServiceImpl implements OrderService { 
    public void update(Order order) {
        this.updateOrder(order);    
    }    
    @Transactional    
    public void updateOrder(Order order) {
        // update order;    
    }
}

View Code

update方法上面没有加 @Transactional 注解,调用有 @Transactional 注解的 updateOrder 方法,updateOrder 方法上的事务不管用

2.2.3.5 数据源没有配置事务管理器

implementation group: "org.springframework.boot", name:"spring-boot-starter-data-jdbc", version: springBootVersion

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
View Code

需要配置如上事务管理器 

2.2.3.6 异常被catch捕获,没有throw出来

    @Service
    public class OrderServiceImpl implements OrderService {
        @Transactional
        public void updateOrder(Order order) {
            try {            
                 update order;         
            }catch (Exception e){
                //do something;        
                 }    
        }
    }
View Code

如上,异常被catch。

2.2.3.7异常错误类型或者格式配置错误

 默认回滚的是RuntimeException,如果想触发其他异常的回滚,需要在注解上配置一下,如

@Transactional(rollback=Exception.class)

 

 2.2.4 手动回滚事务

 事务场景中,抛出异常被catch后,如果需要回滚一定要手动回滚事务,不要用自动回滚,错误使用方式

@Service
    public class OrderServiceImpl implements OrderService {
        @Transactional(rollbackFor={Exception.class})
        public void updateOrder(Order order) {
            try {
                 update order;
            }catch (Exception e){
                throw e;
                 }
        }
    }
View Code

 

 正确方式

   @Autowired
    private DataSourceTransactionManager transactionManager;

    @Service
    public class OrderServiceImpl implements OrderService {
        @Transactional(rollbackFor = {Exception.class})
        public void updateOrder(Order order) {
            DefaultTransactionDefinition def = new DefaultTransactionDefinition();
            def.setName("SomeTxName");
            def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
            TransactionStatus status = transactionManager.getTransaction(def);
            try {
                //do operation
            } catch (Exception e) {
                transactionManager.rollback(status);
                throw e;
            }
        }
    }
View Code

 

阿里推荐的方式属于自动提交/手动回滚,如果我们想手动提交、手动回滚,参考如下:
    @Service
    public class OrderServiceImpl implements OrderService {
        @Transactional(rollbackFor = {Exception.class})
        public void updateOrder(Order order) {
            DefaultTransactionDefinition def = new DefaultTransactionDefinition();
            def.setName("SomeTxName");
            def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
            TransactionStatus status = transactionManager.getTransaction(def);
            try {
                //do operation
                transactionManager.commit(status);
            } catch (Exception e) {
                transactionManager.rollback(status);
                throw e;
            }
        }
    }
View Code

  

2.3 编程式事务vs声明式事务

编程式事务可以到代码层,声明式事务是方法层(作用在class上,类中所有public方法都具有事务支持)。但声明式事务更易用

 

2.4 事务边界与传播

2.4.1事务边界 

 @Component

public class BonusService {
    @Transactional
    public void addBonus(long userId, int bonus) { // 事务开始
       ...
    } // 事务结束
}
事务的边界就是addBonus()方法的开始和结束

2.4.2 事务传播 

事务传播行为

  • REQUIRED (默认) 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
  • SUPPORTS 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行
  • MANDATORY 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
  • REQUIRES_NEW 创建一个新的事务,如果当前存在事务,则把当前事务挂起
  • NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起
  • NEVER 以非事务方式运行,如果当前存在事务,则抛出异常
  • NESTED 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没


@Service
@Slf4j
public class TransactionBoardService {
    @Autowired
    private MongoTemplate mongoTemplate;
    @Autowired
    private TransactionTemplate transactionTemplate;
    @Transactional
    public void step1(){
        User user=new User();
        user.setAge(22);
        user.setId(1);
        user.setName("Zhang");
        user.setSex("fmale");
        user.setMoney(100);
        mongoTemplate.insert(user,"testone");
        this.step2();
    }

    @Transactional
    public void step2(){
        User user=new User();
        user.setAge(26);
        user.setId(2);
        user.setName("Li");
        user.setSex("male");
        user.setMoney(80);
        mongoTemplate.insert(user,"testone");
        int i=1/0; //条件1
    }
}

 

条件1注释条件下,step1创建一个新事务,调用step2时判断已经存在事务,所以不在新创建事务而是加入到方法1创建的事务里

条件1 不注释情况下,step2中出现异常行为,因为step1 step2 在同一个事务里,所以step1 step2中insert操作都回滚不会将数据写入到mongo中。相关log如下

2022-12-10 10:35:18.646 o.s.d.m.MongoTransactionManager http-nio-8080-exec-1 [DEBUG] Creating new transaction with name [com.citi.ark.mls.service.qftest.transactionboard.TransactionBoardService.step1]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT

java.lang.ArithmeticException: / by zero] with root cause
java.lang.ArithmeticException: / by zero
at com.citi.ark.mls.service.qftest.transactionboard.TransactionBoardService.step2(TransactionBoardService.java:38)
at com.citi.ark.mls.service.qftest.transactionboard.TransactionBoardService.step1(TransactionBoardService.java:26)

 

2022-12-10 10:35:51.798 o.s.d.m.MongoTransactionManager http-nio-8080-exec-1 [DEBUG] Initiating transaction rollback

 

try...catch捕捉异常会导致事务不回滚

  /**
     * 知识点:声明式事务是通过切面来完成的,线程调用目标对象的方法时会被代理对象拦截并且在目标方法前后添加
     * 事务管理的代码,代理对象确认这次事务是否回滚的指标就是目标对象调用时有无抛出异常,如果目标对象抛出异常
     * 到代理对象,代理对象就要调用事务回滚的方法。
     *  this.transactionPropagationService.handleREQUIRED6() 传播行为REQUIRED (方法1)
     *  this.transactionPropagationService2.handleREQUIRED6() 传播行为REQUIRED (方法2)
     *  1.方法1创建事务
     *  2.方法2判断调用他的方法1是有事务的然后方法2会加入方法1创建的事务
     *  3.方法1执行到int i = 1 / 0;这一步会报错,由于方法1加了try...catch 内部消化了异常所以导致事务无法回滚。
     *  4.如果把//throw new RuntimeException("这一步加上,就该回滚了");注释放开事务会回滚,因为重新抛出了
     *  异常,代理对象就可以捕捉到异常然后就可以执行回滚了。
     *  5.如果在方法2中执行int i = 1 / 0;方法1必须要把异常抛给代理对象不然会报错。
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public void handleREQUIRED6() {
        try {
            jdbcTemplate.update("update account set balance=balance+100 where name = 'lilei'");
            this.transactionPropagationService2.handleREQUIRED6();
            int i = 1 / 0;
        } catch (Exception e) {
            System.out.println("我虽然报错了,但是我就不回滚,就是玩。");
            //如果方法2抛出异常了则必须要把下面这行代码的注释去除,否则会报错导致整个事务无法回滚
            //throw new RuntimeException("这一步加上,就该回滚了");
        }
    }

 

 2.4.3 事务隔离级别

Spirng默认事务隔离级别

以连接数据库的事务隔离界别为准

读未提交

事务A在执行过程中还没有提交,但事务B就可以看到。此时会引起脏读

读已提交

只有事务A提交后,事务B才能看到事务A提交结果。这种隔离级别可以避免脏读,但是由于事务B在执行过程中可以读取到事务A提交的结果,比如事务B在事务A提交前读取一次,在事务A提交后再读取一次,这样事务B在同一事务过程中读取到结果不一样,造成不可重复读

可重复读

事务A中代码在同一事务过程中,多次查询结果一致(比如如上情况,事务A执行过程中,有事务B提交,但事务A多次查询结果是一样的,即事务B提交结果在事务A中和B提交前结果一样)。这种情况下,即使事务B提交了某条数据,但是事务A读取不到,但事务A有插不进这条数据,造成幻读

串行化

强制事务排序,使之不会发生冲突,从而解决脏读、不可重复读、幻读问题。但效率很低 

 

脏读:一个事务读取到了另一个事务修改的数据之后,后一个事务又进行了回滚操作,从而导致第一个事务读取的数据是错误的。
不可重复读:一个事务两次查询得到的结果不同,因为在两次查询中间,有另一个事务把数据修改了。
幻读:一个事务两次查询中得到的结果集不同,因为在两次查询中另一个事务有新增了一部分数据。

 

参考: https://blog.csdn.net/h295928126/article/details/125530184 

https://www.liaoxuefeng.com/wiki/1252599548343744/1282383642886177

https://blog.csdn.net/u013887008/article/details/122970797

https://www.jianshu.com/p/2c70169b52c3


posted on 2022-12-09 17:33  colorfulworld  阅读(51)  评论(0编辑  收藏  举报