1.事务
使用功能场景: 由于数据操作在顺序执行的过程中,线上可能有各种无法预知的问题,任何一步操作都有可能发生异常,异常则会导致后续的操作无法完成,此时由于业务逻辑并未正确的完成,所以在之前操作数据库的动作并不可靠,需要在这种情况下进行数据的回滚。 事务的作用就是为了保证用户的每一个操作都是可靠的,事务中的每一步操作都必须成功执行,只要有发生异常就回退到事务未进行操作的状态。 事务管理是SpringBoot框架中最为常用的功能之一,我们在实际应用开发时,基本上在service层处理业务逻辑的时候都要加上事务,当然了,有时候可能由于场景需要,也不用加事务(比如我们往一个表里插数据,相互没有影响,插多少是多少,不能因为某个数据挂了,把之前插的全部回滚)
2.SpringBoot事务配置
2.1 依赖导入
在SpringBoot中使用事务,需要导入mybatis依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
导入了mybatis依赖后,SpringBoot会自动注入DataSourceTransactionManager,我们不需要任何其他的配置就可以用@Transactional注解进行事务的使用,关于MyBatis的配置,上文已经说明,这里和上文Mybatis配置一致即可
2.2 事务的测试
我们首先在数据库表中插入一条数据 然后我们写一个插入的mapper:
package com.example.springdemo1.dao;
import com.example.springdemo1.pojo.User;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface UserMapper {
@Insert("insert into user(id,name,pwd) values (#{id},#{name},#{pwd}")
Integer insertUser(User user);
}
OK,接下来我们来测试一下SpringBoot中的事务处理,在service层,我们手动抛出个异常来模拟实际中出现的异常,然后观察一下事务有没有回滚,如果数据库中没有新的记录,则说明事务回滚成功
package com.example.springdemo1.service;
import com.example.springdemo1.pojo.User;
public interface UserService{
void insertUser(User user);
}
package com.example.springdemo1.service.impl;
import com.example.springdemo1.dao.UserMapper;
import com.example.springdemo1.pojo.User;
import com.example.springdemo1.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class UserServiceImpl implements UserService{
@Resource
private UserMapper userMapper;
@Override
@Transactional
public void insertUser(User user) {
userMapper.insertUser(user);
throw new RuntimeException();
}
}
我们来测试一下:
package com.example.springdemo1.controller;
import com.example.springdemo1.pojo.User;
import com.example.springdemo1.service.UserService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/test")
public class testController11 {
@Resource
private UserService userService;
@PostMapping("/addUser")
public String addUser(@RequestBody User user){
if(user != null){
userService.insertUser(user);
return "success";
}else{
return "failure";
}
}
}
我们使用postman调用一下该接口,因为在程序中抛出了个异常,会造成事务回滚,我们刷新一下数据库,并没有增加一条记录,说明事务生效了。
3.常见问题总结
3.1 异常并没有被捕获到
首先要说的,就是异常并没有被捕获到,导致事务并没有回滚,我们在业务层代码中,也许已经考虑到了异常的存在,或者编辑器已经提示我们需要抛出异常,但是这里面有个需要注意的地方:并不是说我们把异常抛出来了,有异常了事务就会回滚,我们来看一个例子:
package com.example.springdemo1.service;
import com.example.springdemo1.pojo.User;
public interface UserService{
void insertUser2(User user) throws Exception;
}
package com.example.springdemo1.service.impl;
import com.example.springdemo1.dao.UserMapper;
import com.example.springdemo1.pojo.User;
import com.example.springdemo1.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.sql.SQLException;
@Service
public class UserServiceImpl implements UserService{
@Resource
private UserMapper userMapper;
@Override
@Transactional
public void insertUser2(User user) throws Exception {
userMapper.insertUser(user);
throw new SQLException("数据库异常");
}
}
我们看上面这个代码,其实并没有什么问题,手动抛出一个SQLException来模拟实际中操作数据库发生的异常,在这个方法中,既然跑出了异常,那么事务应该回滚,实际却不如此,写个测试类测试一下
package com.example.springdemo1.controller;
import com.example.springdemo1.pojo.User;
import com.example.springdemo1.service.UserService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/test")
public class testController11 {
@Resource
private UserService userService;
@PostMapping("/addUser2")
public String adddUser2(@RequestBody User user) throws Exception{
if(user != null){
userService.insertUser2(user);
return "success";
}else{
return "failure";
}
}
}
通过postman测试一下,就会发现,仍然是可以插入一条用户数据的,那么问题出在哪呢?因为SpringBoot默认的事务规则是遇到运行异常和程序错误才会回滚,比如上述例子中抛出的RuntimeException就没有问题,但是抛出SQLException就无法回滚了,针对非运行时异常,如果要进行事务回滚的话,可以在 @Transactional注解中使用rollbackFor属性来指定异常,比如@Transactional(rollbackFor = Exception.class) ,这样就没有问题了。
3.2 异常被“吃”掉
我们在处理异常时,有两种方式,要么抛出去,让上一层来捕获处理,要么把异常try catch掉,在异常出现的地方给处理掉,就因为有这种try catch,所以导致异常被“吃”掉,事务无法回滚 ,我们还是看上面那个例子,简单修改一下代码:
package com.example.springdemo1.service;
import com.example.springdemo1.pojo.User;
public interface UserService{
void insertUser3(User user);
}
package com.example.springdemo1.service.impl;
import com.example.springdemo1.dao.UserMapper;
import com.example.springdemo1.pojo.User;
import com.example.springdemo1.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.sql.SQLException;
@Service
public class UserServiceImpl implements UserService{
@Resource
private UserMapper userMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void insertUser3(User user) {
user = new User(7,"lyh7","123456");
try{
userMapper.insertUser(user);
throw new SQLException("数据库异常");
}catch(Exception e){
}
}
}
package com.example.springdemo1.controller;
import com.example.springdemo1.pojo.User;
import com.example.springdemo1.service.UserService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/test")
public class testController11 {
@Resource
private UserService userService;
@PostMapping("/addUser3")
public String addUser3(@RequestBody User user){
if (user != null){
userService.insertUser3(user);
return "success";
}else{
return "failure";
}
}
}
通过postman测试一下,就会发现,仍然是可以插入一条用户数据,说明事务并没有因为抛出异常而回滚,那这种怎么解决呢?直接往上抛,给上一层来处理即可,千万不要在事务中把异常自己“吃”掉。
3.3 事务的范围
举个例子:
package com.example.springdemo1.service.impl;
import com.example.springdemo1.dao.UserMapper;
import com.example.springdemo1.pojo.User;
import com.example.springdemo1.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.sql.SQLException;
@Service
public class UserServiceImpl implements UserService{
@Resource
private UserMapper userMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public synchronized void insertUser4(User user){
userMapper.insertUser(user);
}
}
可以看到,因为要考虑并发问题,在业务层代码的方法上加了个synchronized关键字,从上面方法中可以看到,方法上是加了事务的,那么也就是说,在执行该方法开始时,事务启动,执行完了之后,事务关闭,但是synchronized没有起作用,其实根本原因是因为事务的范围比锁的范围大,也就是说,在加锁的那部分代码执行完之后,锁释放掉了,但是事务还没有结束,此时另一个线程进来了,事务没结束的话,第二个线程进来时,数据库的状态和第一个线程刚进来是一样的,即由于mysql InnoDB引擎的默认隔离级别是可重复读(在同一个事务里,select的结果是事务开始时时间点的状态),线程二事务开始的时候,线程一还没提交完成,导致读取的数据还没更新呢,第二个线程也做了插入动作,导致了脏数据。
这个问题可以避免:
- 把事务去掉即可
- 在调用该service的地方加锁,保证锁的范围比事务的范围大即可
|