前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文彻底读懂优秀开源产品MyBatis一级缓存设计!

一文彻底读懂优秀开源产品MyBatis一级缓存设计!

作者头像
孙玄@奈学教育
发布2020-11-09 10:08:46
4961
发布2020-11-09 10:08:46
举报
文章被收录于专栏:架构之美架构之美

- 前言 -

缓存是 MyBatis 中非常重要的特性。合理使用缓存能够减少数据库 IO,显著提升系统性能。但是在分布式环境下,如果使用不当,则可能会带来数据一致性问题。MyBatis 提供了一级缓存和二级缓存,其中一级缓存基于 SqlSession 实现,而二级缓存基于 Mapper,本文将会详细讲解一级缓存。

- CACHE 缓存 -

MyBatis 跟缓存相关的类都在 Cache 包里面,其中有一个 Cache 接口,只有一个默认的实现类 PerpetualCache,它是用 HashMap 实现的。除此之外,还有很多的装饰器,通过这些装饰器可以额外实现很多的功能:回收策略、日志记录、定时刷新等等。

1、包结构

如图是缓存类所在源码中所处的位置,从包名中我们可以知道 decorators 包中存放的是一些装饰类。

2、查看装饰后的结果

但是无论怎么装饰,经过多少层装饰,最后使用的还是基本的实现类(默认PerpetualCache)。我们 debug 看一下经层层装饰后的结果如图:

CachingExecutor中Debug查看:缓存的层层装饰

我们后面说的一级缓存缓存就是存放到这个 PerpetualCache 里面。

3、对装饰器的分类

- 一级缓存的特点 -

1、一级缓存默认是开启的,而且不能关闭

至于一级缓存为什么不能关闭,MyBatis 核心开发人员做出了解释:

MyBatis的一些关键特性(例如通过<association>和<collection>建立级联映射、避免循环引用(circular references)、加速重复嵌套查询等)都是基于MyBatis一级缓存实现的,而且MyBatis结果集映射相关代码重度依赖CacheKey,所以目前MyBatis不支持关闭一级缓存。

虽然我们不能关闭一级缓存,但是我们可以更改他的作用范围:

MyBatis提供了一个配置参数localCacheScope,用于控制一级缓存的级别,该参数的取值为SESSION、STATEMENT,当指定localCacheScope参数值为SESSION时,缓存对整个SqlSession有效,只有执行DML语句(更新语句)时,缓存才会被清除。当localCacheScope值为STATEMENT时,缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。

代码语言:javascript
复制
<settings>
  <setting name="localCacheScope" value="STATEMENT"/>
</settings>

能更改一级缓存的作用范围这一点很重要后面我们讲解中会用到这一特性。

2、一级缓存默认是SqlSession级别的

在操作数据库时需要构造 sqlSession 对象,在对象中有一个(内存区域)数据结构(HashMap)用于存储缓存数据。不同的 sqlSession 之间的缓存数据区域(HashMap)是互相不影响的。用一张图来表示一下一级缓存,其中每一个 SqlSession 的内部都会有一个一级缓存对象。

- 实验验证一级缓存的作用范围 -

1、一级缓存同一个会话共享数据

模拟思路:打开一个会话,进行两次查询通过日志查看第二次是否走数据库。

代码如下:

代码语言:javascript
复制
   @Test
    public void testSession() throws IOException {
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class);
            List<Order> orders = orderMapper.queryById(620898339119480832L);
            System.out.println("第一次查询:" + JSON.toJSONString(orders));
            OrderMapper orderMapper2 = sqlSession.getMapper(OrderMapper.class);
            List<Order> orders2 = orderMapper2.queryById(620898339119480832L);
            System.out.println("第二次查询:" + JSON.toJSONString(orders2));
        }
    }

日志信息如下:

分析:第一次查询打印了 sql 日志信息,说明是通过数据库获取到数据,第二次也查询到数据但是没有打印日志信息,说明走了缓存。

结论:一级缓存同一个会话共享数据。

2、同一个会话如果有更新操作则缓存清除

模拟思路:打开一个会话,先进行查询,然后进行更新操作,最后再次查询刚才的语句,看是否打印查询数据库的 sql 日志(这些操作都是对一条数据的操作)。

代码如下:

代码语言:javascript
复制
@Test
public void testUpdate() throws IOException {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
        // 同一个会话  第一次查询
        System.out.println("第一次会会话的  第一次查询");
        OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class);
        List<Order> orders = orderMapper.queryById(620898339119480832L);
        Order order = orders.get(0);
        order.setAmount(12L);
        // 进行更新
        orderMapper.updateByPrimaryKey(order);
        // 同一个会话  第二次查询
        System.out.println("第一次会会话的  第二次查询");
        List<Order> orders2 = orderMapper.queryById(620898339119480832L);
        System.out.println(JSON.toJSONString(orders2));
    }
}

日志信息如下:

分析:第一次查询打印了 sql 日志,然后进行数据更新,最后进行第二次查询发现仍旧查询数据库,说明缓存已经失效。

结论:同一个会话如果有更新操作则缓存清除。

3、导致脏数据

模拟思路:打开两个会话,第一个会话查询数据库获取数据后 ,接着第二个会话修改数据,最后第一个会话再查询数据,那最后这次查询如果和第一次查询相同,那说明一级缓存会导致脏数据问题。

代码如下:

代码语言:javascript
复制
 @Test
    public void testDirtyData() throws IOException {
        SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
        try {
            // 同一个会话  第一次查询
            OrderMapper orderMapper1 = sqlSession1.getMapper(OrderMapper.class);
            OrderMapper orderMapper2 = sqlSession2.getMapper(OrderMapper.class);
            List<Order> orders = orderMapper1.queryById(620898339119480832L);
            System.out.println("第一次会会话第一次查询的结果" + orders);
            Order order1 = getOrder(orders);
            System.out.println("=========更新数据======");
            orderMapper2.updateByPrimaryKey(order1);
            sqlSession2.commit();
            List<Order> orders1 =  orderMapper1.queryById(620898339119480832L);
            System.out.println("第二次查询:"+JSON.toJSONString(orders1));

        }catch (Exception e){
            sqlSession1.close();
            sqlSession2.close();
        }
    }

日志信息如下:

分析:第一个会话第一次查询 amount 值是1212,第二会话将 amount 更改为666,第一个会话再次查询数据获取的 amount 值为1212,这说明第一个会话的第二次查询命中缓存导致了脏数据问题。

结论:一级缓存在多会话中会导致脏数据。

解决方式:在配置一级缓存作用范围的时候将其设置为 STATEMENT,那么缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。

设置方式:

代码语言:javascript
复制
<settings>
   <setting name="localCacheScope" value="STATEMENT"/>
</settings>
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 架构之美 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档