场景:我们在开发企业应用时,由于数据操作在顺序执行的过程中,线上可能有各种无法预知的问题,
任何一步操作都有可能发生异常,异常则会导致后续的操作无法完成。此时由于业务逻辑并未正确的完
成,所以在之前操作过数据库的动作并不可靠,需要在这种情况下进行数据的回滚。
springboot的事务管理需要导入spring-boot-starter-jdbc;而我们导入的mybatis-spring-boot-starter包含了它,所以无需重复导入;
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.1version>
dependency>
1)mapper接口
@Insert("insert into tb_filminfo (typeid,filmname,ticketprice) values(#{typeid},#{filmname},#{ticketprice})")
int insert(FilminfoPO po);
2)service接口
public interface IFilmInfoService {
//插入一条记录
public int insert(FilminfoPO po);
3)service实现类
@Service
public class FilmInfoServiceImpl implements IFilmInfoService {
@Resource
private FilmInfoMapper mapper;
@Transactional
@Override
public int insert(FilminfoPO po) {
return mapper.insert(po);
}
4)controller类
@Controller
@RequestMapping("/film")
public class FilmInfoController {
@Resource
IFilmInfoService service;
@RequestMapping("/insert")
public String insert(FilminfoPO po) {
if (po!=null) {
int i = service.insert(po);
return "success";
} else {
return "false";
}
}
}
当没有异常抛出时添加成功,有异常出现添加失败;
1)异常并没有被捕获到
异常并没有被 ”捕获“ 到,导致事务并没有回滚。
Spring Boot 默认的事务规则是遇到运行异常(RuntimeException)和程序 错误(Error)才会回滚。但是抛出 SQLException 就无法回滚了。
@Transactional
@Override
public void insert(FilminfoPO po) throws SQLException {
// 手动抛出异常
mapper.insert(po);
throw new SQLException("数据库异常");
虽然抛出异常但是数据插入成功;
解决方案:针对非运行时异常,如果要进行事务回滚的话,可以在 @Transactional 注解中使用 rollbackFor 属性来指定异常,比如 @Transactional(rollbackFor = Exception.class) ,这样就没有问题了,所以在实际项目中,一定要指定异常。
@Transactional(rollbackFor = Exception.class)
@Override
public void insert(FilminfoPO po) throws SQLException {
// 手动抛出异常
mapper.insert(po);
throw new SQLException("数据库异常");
}
这样就插入失败了;
2)异常在方法中被捕获导致事务回滚失败
我们在处理异常时,有两种方式, 要么抛出去,让上一层来捕获处理;要么把异常 try catch 掉,在异常出现的地方给处理掉。就因为有 这中 try…catch,所以导致异常被 ”吃“ 掉,事务无法回滚。
@Transactional
@Override
public int insert(FilminfoPO po) {
try {
int i = 1 / 0;
} catch (Exception e) {
e.getMessage();
}
return mapper.insert(po);
}
记录成功被插入;
解决方法:直接往上抛,给上一层来处理即可
3)事务的范围 冲突导致回滚失败
许多业务需要在高并发的情况下保证数据唯一性所比要加synchronized关键字如一个数据库中,针对某个用户,只有一条记录,下一个插入动作过来,会先判断该数据库 中有没有相同的用户,如果有就不插入,就更新,没有才插入,所以理论上,数据库中永远就一条同一 用户信息,不会出现同一数据库中插入了两条相同用户的信息。
@Transactional(rollbackFor = Exception.class)
@Override
public synchronized void insert(FilminfoPO po) throws SQLException {
// 手动抛出异常
mapper.insert(po);
}
但是在压测时,数据库中确实可能有两条同一用户的信息,分析其原因,在于事务的
范围和锁的范围问题。
在执行该方法开始时,事务启动,执行 完了后,事务关闭。但是 synchronized 没有起作用,其实根本原因是因为事务的范围比锁的范围大。 也就是说,在加锁的那部分代码执行完之后,锁释放掉了,但是事务还没结束,此时另一个线程进来 了,事务没结束的话,第二个线程进来时,数据库的状态和第一个线程刚进来是一样的。即由于mysql Innodb引擎的默认隔离级别是可重复读(在同一个事务里,SELECT的结果是事务开始时时间点的状 态),线程二事务开始的时候,线程一还没提交完成,导致读取的数据还没更新。第二个线程也做了插 入动作,导致了脏数据。
解决方案:
1.把事务去掉即可(不推荐);
2. 在调用该 service 的地方加锁,保证锁 的范围比事务的范围大即可。