避免踩坑,SpringBoot中的事务配置管理与异常处理

SpringBoot中的事务配置管理
事务的概念
  • 使用场景:我们在开发的时候,由于数据操作在顺序进行中可能出现无法预知的问题,任何一步都有可能发生异常。此时逻辑并未正确的完成,所以在之前操作过数据库的动作并不可靠,需要在这种情况下进⾏数
    据的回滚。
  • 作用:事务的作⽤就是为了保证⽤户的每⼀个操作都是可靠的,事务中的每⼀步操作都必须成功执⾏,只要有发⽣异常就回退到事务开始未进⾏操
    作的状态。这很好理解,转账、购票等等,必须整个事件流程全部执⾏完才能⼈为该事件执⾏成功,不能转钱转到⼀半,系统死了,转账⼈
    钱没了,收款⼈钱还没到。
  • 事务管理是 Spring Boot 框架中最为常⽤的功能之⼀,我们在实际应⽤开发时,基本上在 service 层处理业务逻辑的时候都要加上事务,
    当然了,有时候可能由于场景需要,也不⽤加事务(⽐如我们就要往⼀个表⾥插数据,相互没有影响,插多少是多少,不能因为某个数据挂
    了,把之前插的全部回滚)。
SpringBoot中的事务配置
  • 依赖导入
<dependency>
 <groupId>org.mybatis.spring.boot</groupId>
 <artifactId>mybatis-spring-boot-starter</artifactId>
 <version>1.3.2</version>
</dependency>
  • 导⼊了 mysql 依赖后,Spring Boot 会⾃动注⼊ DataSourceTransactionManager,我们不需要任何其他的配置就可以
    ⽤ @Transactional 注解进⾏事务的使⽤。
事务的测试
  • 存在一张user表
create table user
(
    id         bigint auto_increment
        primary key,
    password   varchar(20)    not null,
    username   varchar(20)    not null,
    have_money decimal(10, 4) null,
    constraint UK_sb8bbouer5wak8vyiiy4pf2bx
        unique (username)
);
  • 创建对应的实体类、Mapper类、service类、与测试的controller类
@Setter
@Getter
@ToString
public class User {
  private Long id;
  private String username;
  private String password;
  @JsonSerialize(using = BigDecimalSerializer.class)
  private BigDecimal haveMoney;
  public User() {}
  public User(Long id, String username, String password, BigDecimal haveMoney) {
    this.id = id;
    this.username = username;
    this.password = password;
    this.haveMoney = haveMoney;
  }
}
@Mapper
public interface UserMapper {
    @Insert("insert into user values(#{id},#{username}, #{password}, #{haveMoney})")
    Integer insertUser(User user);
}
public interface UserService extends IService<Book> {
  @Transactional
  void insertUser(User user);
}
@RequestMapping("/testTransactional")
@RestController
public class testTransactional {
  @Resource
  UserService userService;
  @RequestMapping("/test1")
  public JsonResult getInfo(){
    User user = new User(4L,"帅小伙","10086",new BigDecimal(19.8888));
    userService.insertUser(user);
    return new JsonResult();
  }
}
  • 在 service 层,我们⼿动抛出个异常来模拟实际中出现的异常,然后观察⼀下事
    务有没有回滚,如果数据库中没有新的记录,则说明事务回滚成功。
  • 调⽤⼀下该接⼝,因为在程序中抛出了个异常,会造成事务回滚,我们刷新⼀下数据库,并没有增加⼀条记录,说明事
    务⽣效了。
事务中异常没有捕获到
  • 程序中经常会碰到异常没有捕获到,而导致事务并没有回滚。我们在业务层中也许已经考虑到异常可能会发生,但是需要注意,并不是说我们把异常跑出来了,有异常了事务就会回滚。
@Service
public class UserServiceImpl implements UserService {
 @Resource
 private UserMapper userMapper;
 
 @Override
 @Transactional
 public void isertUser2(User user) throws Exception {
 // 插⼊⽤户信息
 userMapper.insertUser(user);
 // ⼿动抛出异常
 throw new SQLException("数据库异常");
 }
}
  • 我们看上⾯这个代码,其实并没有什么问题,⼿动抛出⼀个 SQLException 来模拟实际中操作数据库发⽣的异常,在这个⽅法中,既然抛出
    了异常,那么事务应该回滚,实际却不如此,读者可以使⽤我源码中 controller 的接⼝,通过 postman 测试⼀下,就会发现,仍然是可
    以插⼊⼀条⽤户数据的。
  • 因为 Spring Boot 默认的事务规则是遇到运⾏异常(RuntimeException)和程序错误(Error)才会回滚。⽐如上⾯
    我们的例⼦中抛出的 RuntimeException 就没有问题,但是抛出 SQLException 就⽆法回滚了。针对⾮运⾏时异常,如果要进⾏事务回滚
    的话,可以在 @Transactional 注解中使⽤ rollbackFor 属性来指定异常,⽐如 @Transactional(rollbackFor = Exception.class),这样就没
    有问题了,所以在实际项⽬中,⼀定要指定异常。
异常被吞掉没有抛出
  • 我们在处理异常时,有两种⽅式,要么抛出去,让上⼀层来捕获处理;
    要么把异常 try catch 掉,在异常出现的地⽅给处理掉。就因为有这中 try…catch,所以导致异常被 ”吃“ 掉,事务⽆法回滚。
@Service
public class UserServiceImpl implements UserService {
 @Resource
 private UserMapper userMapper;
 @Override
 @Transactional(rollbackFor = Exception.class)
 public void isertUser3(User user) {
 try {
 // 插⼊⽤户信息
 userMapper.insertUser(user);
 // ⼿动抛出异常
 throw new SQLException("数据库异常");
 } catch (Exception e) {
 // 异常处理逻辑
 }
 }
}

  • 测试⼀下,就会发现,仍然是可以插⼊⼀条⽤户数据,说明事务并没有因为抛
    出异常⽽回滚。这个细节往往⽐上⾯那个坑更难以发现,因为我们的思维很容易导致 try…catch 代码的产⽣,⼀旦出现这种问题,往往排
    查起来⽐较费劲,所以我们平时在写代码时,⼀定要多思考,多注意这种细节,尽量避免给⾃⼰埋坑。
事务的范围导致出现的问题
@Service
public class UserServiceImpl implements UserService {
 @Resource
 private UserMapper userMapper;
 @Override
 @Transactional(rollbackFor = Exception.class)
 public synchronized void isertUser4(User user) {
 // 实际中的具体业务……
 userMapper.insertUser(user);
 }
}
  • 因为要考虑并发问题,我在业务层代码的⽅法上加了个 synchronized 关键字。我举个实际的场景,⽐如⼀个数据库中,针对某
    个⽤户,只有⼀条记录,下⼀个插⼊动作过来,会先判断该数据库中有没有相同的⽤户,如果有就不插⼊,就更新,没有才插⼊,所以理论
    上,数据库中永远就⼀条同⼀⽤户信息,不会出现同⼀数据库中插⼊了两条相同⽤户的信息。
  • 但是在压测时,就会出现上⾯的问题,数据库中确实有两条同⼀⽤户的信息,分析其原因,在于事务的范围和锁的范围问题。
  • 从上⾯⽅法中可以看到,⽅法上是加了事务的,那么也就是说,在执⾏该⽅法开始时,事务启动,执⾏完了后,事务关闭。但是
    synchronized 没有起作⽤,其实根本原因是因为事务的范围⽐锁的范围⼤。也就是说,在加锁的那部分代码执⾏完之后,锁释放掉了,但
    是事务还没结束,此时另⼀个线程进来了,事务没结束的话,第⼆个线程进来时,数据库的状态和第⼀个线程刚进来是⼀样的。即由于
    mysql Innodb引擎的默认隔离级别是可重复读(在同⼀个事务⾥,SELECT的结果是事务开始时时间点的状态),线程⼆事务开始的时
    候,线程⼀还没提交完成,导致读取的数据还没更新。第⼆个线程也做了插⼊动作,导致了脏数据。
  • 这个问题可以避免,第⼀,把事务去掉即可(不推荐);第⼆,在调⽤该 service 的地⽅加锁,保证锁的范围⽐事务的范围⼤即可。
  • 16
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值