Spring 事务回滚 及 事务注解@Transactional

1. 引入依赖(数据库相关)

<!--依赖管理 -->
    <dependencies>
        <dependency> <!--添加Web依赖 -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency> <!--添加Mybatis依赖 -->
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>
        <dependency><!--添加MySQL驱动依赖 -->
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency><!--添加Test依赖 -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

why:

事务的配置,就是在mybatis的基础上加上两个注解。 
1、需要的注解为@EnableTransactionManagement(加在启动类上的,springboot默认支持,可以不写) 和@Transactional 两个,它们来自于下边这个包:

spring-tx.jar
该包其实在前边配置mybatis引入依赖时,已自动引入,就是下边这个:
<artifactId>mybatis-spring-boot-starter</artifactId>

事务常见的问题:

  • 更新丢失:当多个事务选择同一行操作,并且都是基于最初的选定的值,由于每个事务都不知道其他事务的存在,就会发生更新覆盖的问题。
  • 脏读:事务A读取了事务B已经修改但为提交的数据。若事务B回滚数据,事务A的数据存在不一致的问题。
  • 不可重复读:书屋A第一次读取最初数据,第二次读取事务B已经提交的修改或删除的数据。导致两次数据读取不一致。不符合事务的隔离性。
  • 幻读:事务A根据相同条件第二次查询到的事务B提交的新增数据,两次数据结果不一致,不符合事务的隔离性。

3.Spring对事务的控制

Spring框架针对事务提供了很多事务管理解决方案。我们这里只说常用的:声明式事务。声明式事务通过传播行为、隔离级别、只读提示、事务超时及回滚规则来进行定义。我们这里讲用Spring提供的注解式事务方法:@Transaction

使用注解式事务的优点:开发团队达到一致的约定,明确标注事务方法的编程风格。

使用事务控制需要注意:

  1. 保证事务方法的执行时间尽可能短,不要穿插其他的网络操作PRC/HTTP请求(可以将这些请求剥离出来)。
  2. 不是所有的放阿飞都需要事务控制,如只有一条修改操作、只读操作等是不需要事务控制的。

注意

Spring默认只对运行期异常(RuntimeException)进行事务回滚操作,对于编译异常Spring是不进行回滚的,所以对于需要进行事务控制的方法尽量将可能抛出的异常都转换成运行期异常,将编译时异常转为运行期异常。

这也是我们我什么要在Service接口中手动封装一些RuntimeException信息的一个重要原因。

/**
 * 转换为运行时异常父类
 *
 */
public class MyException extends RuntimeException {

    public SeckillException(String message) {
        super(message);
    }

    public SeckillException(String message, Throwable cause) {
        super(message, cause);
    }
}

public class RepeatAddException extends MyException{
       public  RepeatAddException(String message) {
        super(message);        
    } 
       public  RepeatAddException(String message,Throwable cause) {
        super(message,cause);        
    } 
    
}


2. 添加配置

主要是配置数据源还有驼峰映射

Spring:
  datasource:
    url: jdbc:mysql://localhost:3306/gamemanage?useUnicode=true&characterEncoding=UTF-8&useSSL=true
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
mybatis:
  configuration:
    map-underscore-to-camel-case: true

 3. 添加数据库记录

DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
  `account_id` varchar(30) ,
  `account_name` varchar(30),
  `balance` decimal(20,2),
  PRIMARY KEY (`account_id`)
);

insert into account values ('1','admin','1000.25');

4. 编写代码

以操作账户金额为例,模拟正常操作金额提交事务,以及发生异常回滚事务。
其中控制层代码如下:
@RestController
public class AccountController {

    @SuppressWarnings("all")
    @Autowired
    AccountService accountService;

    @GetMapping("/")
    public Account getAccount() {
        //查询账户
        return accountService.getAccount();
    }

    @GetMapping("/add")
    public Object addMoney() {
        try {
            accountService.addMoney();
        } catch (Exception e) {
            return "发生异常了:" + accountService.getAccount();
        }
        return getAccount();
    }
}

在业务层在方法前使用 @Transactional 开启事务,执行数据库操作后抛出异常。增、删、改等方法才会需要事务。 

@Service
public class AccountService {

    @SuppressWarnings("all")
    @Autowired
    AccountMapper accountMapper;

    public Account getAccount() {
        return accountMapper.getAccount();
    }

    @Transactional
    public void addMoney() throws Exception {
        //先增加余额
        accountMapper.addMoney();
        //然后遇到故障
        throw new RuntimeException("发生异常了..");
    }
}

数据库层就很简单了,注解方式


@Mapper
public interface AccountMapper {

    @Select("select * from account where account_id=1")
    Account getAccount();

    @Update("update account set balance = balance+100 where account_id=1")
    void addMoney();
}

 Account 实体对象

public class Account {

    private String accountId;
    private String accountName;
    private BigDecimal balance;

    // Override toString Method ..
    // Getter & Setters  ..
}

5. 测试事务

可以看到账户余额并没有增加,如下: 也就是说事务开启成功,数据得到回滚。

6. 常见坑点

坑点1:遇到非检测异常时,事务不开启,也无法回滚。

例如下面这段代码,账户余额依旧增加成功,并没有因为后面遇到检测异常而回滚!!

//改成SQLException,不会回滚,金额照常增加
@Transactional
    public void addMoney() throws Exception {
        //先增加余额
        accountMapper.addMoney();
        //然后遇到故障
        throw new SQLException("发生异常了..");
    }

原因分析:因为Spring的默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。如果想针对非检测异常进行事务回滚,可以在@Transactional 注解里使用
rollbackFor 属性明确指定异常。例如下面这样,就可以正常回滚:

@Transactional(rollbackFor = SQLException.class)//写Exception.class也可以
    public void addMoney() throws Exception {
        //先增加余额
        accountMapper.addMoney();
        //然后遇到故障
        throw new SQLException("发生异常了..");
    }

常见坑点2: 在业务层捕捉异常后,发现事务不生效。

这是许多新手都会犯的一个错误,在业务层手工捕捉并处理了异常,你都把异常“吃”掉了,Spring自然不知道这里有错,更不会主动去回滚数据。例如:下面这段代码直接导致增加余额的事务回滚没有生效。

@Transactional
    public void addMoney() throws Exception {
        //先增加余额
        accountMapper.addMoney();
        //谨慎:尽量不要在业务层捕捉异常并处理
        try {
            throw new SQLException("发生异常了..");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

实际上,使用IDEA编辑器可以避免这些错误,会提醒

推荐做法:在业务层统一抛出异常,然后在控制层统一处理

@Transactional
    public void addMoney() throws Exception {
        //先增加余额
        accountMapper.addMoney();
        //推荐:在业务层将异常抛出
        throw new RuntimeException("发生异常了..");
    }

用法 public 方法

@Transactional 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。

虽然 @Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外, @Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常,方法上注解属性会覆盖类注解上的相同属性

默认情况下,只有来自外部的方法调用才会被AOP代理捕获,也就是,类内部方法调用本类内部的其他方法并不会引起事务行为,即使被调用方法使用@Transactional注解进行修饰。

@Transactional更多属性:

  •  方案1.例如service层处理事务,那么service中的方法中不做异常捕获,或者在catch语句中最后增加throw new RuntimeException()语句,以便让aop捕获异常再去回滚,并且在service上层(webservice客户端,view层action)要继续捕获这个异常并处理
  •   方案2.方法内部try catch ,catch中进行手动回滚,TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();语句,手动回滚,这样上层就无需去处理异常(现在项目的做法),使用TransactionAspectSupport类获取当前事务,

    @Transactional(rollbackFor = Exception.class)
    public boolean insert(String arr1, String arr2) {
        try {
            String id = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 11);
            Ques ques = JSONObject.parseObject(arr2, Ques.class);
            ques.setId(id);
            if (iQuesDao.insert(ques) > 0) {
                List<QuesOption> quesOptionList = JSONObject.parseArray(arr1, QuesOption.class);
                for (QuesOption quesOption : quesOptionList) {
                    quesOption.setQuescontentid(id);
                }
                if (iQuesOptionDao.insertList(quesOptionList) > 0) {
                    return true;
                }
            }
        } catch (Exception e) {
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        return false;
    }

参考:http://www.cnblogs.com/linkstar/p/7372329.html

https://blog.csdn.net/wkl305268748/article/details/77619367

猜你喜欢

转载自blog.csdn.net/qq_38930240/article/details/85249049