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的地方加锁,保证锁的范围比事务的范围大即可
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐