SpringBoot+Mybatis保证读写事务隔离性的三种实现方式

SpringBoot+Mybatis保证读写事务隔离性的三种实现方式

实际开发中经常会有这样的需求,注册用户,如果用户名存在则失败,否则注册成功。

在单线程下,逻辑很简单,但是高并发下需要保证事务隔离性,这里举一个简化版的例子来讲述自己的实现方法。

问题

在实际开发的时候,我们经常会做这种事情:

  1. 先查询数据库中的数据,得到一些临时结果
  2. 根据一些临时结果做判断,进行增删改查操作

也就是说,第二个阶段的增删改查操作依赖于在第一个阶段的结果

举个例子,我们的表结构很简单

image-20210302163200742

查询是否存在dname=TEST的部门,如果不存在插入一个部门名为TEST,如果存在则不操作。

要求必须不能使数据库中存在两个dname相同的行

我们知道SpringBoot中的Controller、Service都是单例的,在实际环境下,面对高并发量的请求,每个请求会起一个线程来进行操作,那么就会发生一些问题

举一个类似“脏读”的例子

@Service
public class DeptServiceImpl implements DeptService{
    @Autowired
    DeptDAO deptDAO;

    @Override
    public int concurrent() {
        Dept dept = deptDAO.queryByName("TEST");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (dept == null) {
            System.out.println("不存在TEST,插入");
            Dept nDept = new Dept().setDName("TEST");
            deptDAO.addDept(nDept);
        } else {
            System.out.println("存在");
        }
        return 1;
    }
}

如果不加以处理,我们对以下接口连着发两个请求调用这个方法试试

    @PostMapping("/concurrent")
    public int concurrent(){
        return deptService.concurrent();
    }

结果明显是有问题的

image-20210302163724061

image-20210302163758834

方法一:加synchronized锁

最简单方法,牺牲一些性能,加synchronized锁即可

    @Override
    public synchronized int concurrent() {
        Dept dept = deptDAO.queryByName("TEST");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (dept == null) {
            System.out.println("不存在TEST,插入");
            Dept nDept = new Dept().setDName("TEST");
            deptDAO.addDept(nDept);
        } else {
            System.out.println("存在");
        }
        return 1;
    }

执行结果没问题

image-20210302163933812

注意,这里为了实验使用了Thread.sleep(3000);,切记不能使用object.wait(3000);,因为wait会释放锁

方法二:使用dual表写sql

出现这个问题的原因在于,我们使用了两次mapper,是否能在一个语句里实现需求呢?

其实是可以的,使用dual表

修改Mybatis语句

    <insert id="addDept" parameterType="com.cpaulyz.PO.Dept">
        insert into dept(dname, db_source) select #{dName},DATABASE() from dual where not exists(
          select * from dept where dname = #{dName}
        );
    </insert>

实际上就是

insert into dept(dname, db_source) select "TEST",DATABASE() from dual where not exists(
  select * from dept where dname = "TEST"
);

然后直接插入即可

    @Override
    public synchronized int concurrent() {
        Dept nDept = new Dept().setDName("TEST");
        deptDAO.addDept(nDept);
        return 1;
    }

方法三:行锁+@Transactional

分析一下出现问题的原因,主要在于Dept dept = deptDAO.queryByName("TEST");时,默认使用的是快照读,即select * from dept where xxxx;

我们可以进行当前读,类似MySQL的锁策略

修改mybatis映射

    <select id="queryByName" resultType="com.cpaulyz.PO.Dept" resultMap="DeptMap">
        select * from dept where dname=#{name} for update;
    </select>

在方法头上加上@Transactional注解(该注解还可以进行隔离级别的配置,这里不再赘述)

    @Override
    @Transactional
    public  int concurrent() {
        Dept dept = deptDAO.queryByName("TEST");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (dept == null) {
            System.out.println("不存在TEST,插入");
            Dept nDept = new Dept().setDName("TEST");
            deptDAO.addDept(nDept);
        } else {
            System.out.println("存在");
        }
        return 1;
    }

测试,成功

image-20210302171607298

原文地址:https://www.cnblogs.com/cpaulyz/p/14470049.html