Mysql数据库一级缓存对业务双重检查锁的影响

Mysql数据库一级缓存对业务双重检查锁的影响

背景介绍

业务上,会有多人可能同时对同一笔申请进行审核的情况发生,为了防止出现重复审批的情况,我首先想到了单例模式的双重锁检查。然后就套用,一直没有出现问题。直到某项功能测试准备上线时发生的一个诡异问题,才让我意识到,基于Mysql数据库的状态检查,是业务的双重检查与单例模式的双重锁检查的最大不同。而恰恰就是微小这个的,一开始并没有引起我注意的不同,差点导致线上重大问题的发生。

场景还原

类似的业务双重检查代码如下:

//审核方法 
@Transactional
public Boolean audit(AuditRequest request) {
		//第一层检查
        A a = checkService.checkAudit(request);
        A result;
        // 通过工厂类获取一个分布式锁
        DistLock distLock = distLock.buildLock(LOCK_KEY + a.getId());
        try {
            // 限时获取锁,给定超时时间后则会抛出异常
            distLock.tryAcquire(1, TimeUnit.MINUTES);
            // 获取锁后执行业务逻辑
            //执行第二层检查
            A a = checkService.checkAudit(request);
            result = audit(request, a);
        } catch (Exception e) {
            LOGGER.error("audit error, errorMsg -> {}", e);
            throw new BusinessRuntimeException(FAIL);
        } finally {
            // 正常执行或发生异常均释放锁
            distLock.release();
        }
        return true;
    }

//检查方法
public A checkAudit(auditRequest auditRequest) {
        //查询数据库,并判断状态
        A a = ADao.selectById(auditRequest.getId());
        if (a == null || a.getStatus() != Status.TO_AUDIT) {
            throw new BusinessRuntimeException(NOT_EXIST);
        }
}

通过以上代码可以看出来,完全是按照单例模式的思路去做的。

但是实际上,调试下来发现,在执行两次ADao.selectById()方法之后的A对象实例a的内存地址是一样的。通过debug日志,发现,只有第一次执行的时候,真正执行了数据库查询,之后第二次执行的时候,并没有打印出sql语句。

原因剖析及原理解析

​ 和大多数持久层框架一样,MyBatis同样提供了一级缓存和二级缓存的支持。一级缓存是基于 PerpetualCache 的 HashMap 的本地缓存,其缓存的作用域为 Session,当 Session flush 或 close 之后,该Session中的所有缓存就会被清空。二级缓存与一级缓存机制相同,默认也是采用PerpetualCache的HashMap进行存储,不同之处在于其存储作用域为 Mapper(即同一个Namespace),并且可自定义存储源,如 Ehcache、Hazelcast等,本文不做详细介绍。

​ MyBatis默认是开启一级缓存的,它是SqlSession级别的,也即同一个SqlSession对象调用相同的Select语句,就会从一级缓存中直接拿到结果,而不是再去查询一次数据库。并且默认Select会使用缓存,增删改不使用。在同一个SqlSession中,使用同样的查询条件调用同一个查询方法(调不同的方法即使SQL完全相同也不会使用一级缓存)时只有第一次会发送SQL到数据库进行查询,后续都会直接从一级缓存中获取缓存结果。示意图如下:

在这里插入图片描述

​ 一级缓存的原理:MyBatis内部缓存使用了一个HashMap,HashMap是键值对的结构,键值对中的key是hashCode + 查询的SqlId(即XxxDao.xml中的id) + 编写的sql查询语句,键值对的值是执行查询后所获得Java对象。一级缓存的作用域是SqlSession,每次查询先找缓存,找到了就使用,找不到再从数据库查询,查询到数据后会将数据写入一级缓存。

​ 一级缓存失效的方法:
​ 1,处于事务中的方法中查询使用的SqlSession对象不是同一个,比如在方法中执行了sqlSession.close()之后又新开一个SqlSession

​ 2,是同一个SqlSession对象,但执行过sqlSession.clearCache();清空了缓存
​ 3,增删改操作之后,一旦方法中有增删改的操作,SqlSession就会清空缓存

而本次遇到的问题,是sqlSession被spring代理了,处于同一个事务中的sqlSession都是同一个,所以一定会存在缓存。

问题解决

​ 了解到以上信息之后,针对开头的问题,我的处理方式是:

​ 1,在第二次检查的时候,新建一个sqlSession重新进行数据库查询,保证缓存失效
或者将org.mybatis.spring提供的SqlSessionTemplate替换为org.apache.ibatis.session.SqlSessionFactory,这样就避开了spring代理中的缓存,也能保证每次使用都是一个新的sqlSession
​ 2,执行sqlSession.clearCache()方法,清除现有缓存
3,还有一种方法,将第一次检查放在事务外

参考资料:

https://blog.csdn.net/luanlouis/article/details/41280959

https://blog.csdn.net/rubulai/article/details/96106754

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值