MyBatis(9) MyBaits 一级缓存

MyBatis缓存

缓存就是内存中的数据,常常来自对数据库查询结果的保存,使用缓存,我们可以避免频繁的与数据库进行交互,进而提高响应速度

MyBatis 也提供了对缓存的支持,分为一级缓存和二级缓存,可以通过下图来理解
在这里插入图片描述

  • 一级缓存是 SqlSession 级别的缓存。在操作数据库时需要构建 sqlSession 对象,在对象中有一个数据结构(HashMap)用于存储数据缓存。不同的 sqlSession 之间缓存数据区域(HashMap)是相互不影响的。
  • 二级缓存是 mapper 级别的缓存,多个 SqlSession 去操作同一个 Mapper 的sql语句,多个 SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的

一级缓存

MyBatis 的一级缓存是默认开启的

  • 我们在一个sqlSession 中,对 user 表根据id进行两次查询,查看他们发出sql语句的情况

    public class CacheTest {
    
        private UserMapper userMapper;
    
        @Before
        public void before() throws IOException {
            // 1.Resources工具类,配置文件的加载,把配置文件加载成字节输入流
            InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
            // 2.解析了配置文件,并创建了sqlSessionFactory
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
            // 3.生成sqlSession (默认开启一个事务,但是该事务不会自动提交, 参数为true为自动提交)
            SqlSession sqlSession = sqlSessionFactory.openSession(true);
            userMapper = sqlSession.getMapper(UserMapper.class);
        }
    
        @Test
        public void fistLevelCache(){
            // 第一次查询id为1的用户
            User user1 = userMapper.findById(1);
            System.out.println("*******第一次查询的user对象*******");
            System.out.println(user1);
    
    //        System.out.println("*******修改第一次查询的user对象*******");
    //        user1.setUsername("xxxxxxxx");
    //        userMapper.updateUser(user1);
    
            System.out.println("*******第二次查询*******");
            // 第二次查询id为1的用户
            User user2 = userMapper.findById(1);
    
            System.out.println(user1 == user2);
        }
    }
    
  • 控制台输出如下

    ==>  Preparing: select * from user where id = ? 
    ==> Parameters: 1(Integer)
    <==    Columns: id, username
    <==        Row: 1, xxxxxxxx
    <==      Total: 1
    *******第一次查询的user对象*******
    User{id=1, username='xxxxxxxx', orderList=[], roleList=[]}
    *******第二次查询*******
    true
    

    我们发现,只有第一次查询执行了sql语句,第二次查询并没有执行sql语句,并且user1和user2的地址值是相等的,这时我们判断,user2查询时,使用了一级缓存

  • 我们把上面的代码注释打开,控制台输出如下:

    ==>  Preparing: select * from user where id = ? 
    ==> Parameters: 1(Integer)
    <==    Columns: id, username
    <==        Row: 1, xxxxxxxx
    <==      Total: 1
    *******第一次查询的user对象*******
    User{id=1, username='xxxxxxxx', orderList=[], roleList=[]}
    *******修改第一次查询的user对象*******
    ==>  Preparing: update user set username = ? where id = ? 
    ==> Parameters: xxxxxxxx(String), 1(Integer)
    <==    Updates: 1
    *******第二次查询*******
    ==>  Preparing: select * from user where id = ? 
    ==> Parameters: 1(Integer)
    <==    Columns: id, username
    <==        Row: 1, xxxxxxxx
    <==      Total: 1
    false
    

    我们发现,这时两次查询都执行了sql语句,没有用到缓存,user1和user2地址值也不相等

  • 分布式部署情况下:

    本地模拟下分布式会出现的情况

    public class CacheTest {
    
        private UserMapper userMapper;
    
        @Before
        public void before() throws IOException {
            // 1.Resources工具类,配置文件的加载,把配置文件加载成字节输入流
            InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
            // 2.解析了配置文件,并创建了sqlSessionFactory
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
            // 3.生成sqlSession (默认开启一个事务,但是该事务不会自动提交, 参数为true为自动提交)
            SqlSession sqlSession = sqlSessionFactory.openSession(true);
            userMapper = sqlSession.getMapper(UserMapper.class);
        }
    
        @Test
        public void fistLevelCache() throws IOException {
            // 第一次查询id为1的用户
            User user1 = userMapper.findById(1);
            System.out.println("*******第一次查询的user对象*******");
            System.out.println(user1);
    		// 模拟分布式下另一台服务器的sqlSession操作
            test();
    
            System.out.println("*******第二次查询*******");
            // 第二次查询id为1的用户
            User user2 = userMapper.findById(1);
    		System.out.println(user2);
            System.out.println(user1 == user2);
        }
    
        @Test
        public void test() throws IOException {
            InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
            SqlSession sqlSession = sqlSessionFactory.openSession(true);
            UserMapper mapper = sqlSession.getMapper(UserMapper.class);
            User user = new User();
            user.setId(1);
            user.setUsername("修改xxx");
            mapper.updateUser(user);
            System.out.println("test方法修改user1,修改后为:");
            User user1 = mapper.findById(1);
            System.out.println("*******修改后查询的user对象*******");
            System.out.println(user1);
    
        }
    
    }
    

    执行后发现:

    ==>  Preparing: select * from user where id = ? 
    ==> Parameters: 1(Integer)
    <==    Columns: id, username
    <==        Row: 1, 原始数据
    <==      Total: 1
    *******第一次查询的user对象*******
    User{id=1, username='原始数据', orderList=[], roleList=[]}
    
    ==>  Preparing: update user set username = ? where id = ? 
    ==> Parameters: 修改后的数据(String), 1(Integer)
    <==    Updates: 1
    test方法修改user1,修改后为:
    ==>  Preparing: select * from user where id = ? 
    ==> Parameters: 1(Integer)
    <==    Columns: id, username
    <==        Row: 1, 修改后的数据
    <==      Total: 1
    *******修改后查询的user对象*******
    User{id=1, username='修改后的数据', orderList=[], roleList=[]}
    
    *******第二次查询*******
    User{id=1, username='原始数据', orderList=[], roleList=[]}
    true
    

    可以看到,当存在另一个sqlSession提交事务时,不会影响到其他的sqlSession,第二次查询时sqlSession还是取的一级缓存的数据,这在分布式的部署下,很容易会出现脏读

  • 关闭一级缓存

    在sqlMapperConfig.xml配置

    <settings>
        <!-- 禁用一级缓存 -->
        <setting name="localCacheScope" value="STATEMENT"/>
    </settings>
    
  • 手动清理一级缓存

    sqlSession.clearCache();
    
  • 总结:

    • 第一次发起查询用户id为1的用户信息,MyBatis先去缓存中查找是否有id为1的用户信息,如果没有,就从数据库中查询,并将查询到的用户信息存入一级缓存
    • 如果中介sqlSession执行了事务提交操作(执行插入、更新、删除),则会把sqlSession中的一级缓存清空,这样做的目的是为了让缓存中存储的是最新的信息,避免脏读
    • 第二次发起查询用户id为1的用户信息,MyBatis先去缓存中查找是否有id为1的用户信息,缓存中有,直接从缓存中返回用户信息
      在这里插入图片描述

一级缓存原理探究与源码分析

一级缓存到底是什么?一级缓存什么时候被创建?一级缓存的工作流程是怎样的?我们现在带着问题来查看MyBatis源码

一级缓存是SqlSession级别的,所以我们从SqlSession类入口,看看有没有创建缓存或者与缓存相关的属性或方法
在这里插入图片描述
查看SqlSession类,发现只有 clearCache() 与缓存是相关的,那我们就直接从这个方法入手吧。
分析源码时,我们要看它(此类)是谁,他的父类和子类又分别是谁,对如上关系了解了,才会对这个类有更深入的认识。
在这里插入图片描述
再深入分析,流程走到 Perpetualcache 中的 clear() 方法之后,会调用其 cache.clear()方法,那么这个cache 是什么东西呢?在PerpetualCache发现,

private Map<Object, Object> cache = new HashMap<>();

也就是一个Map,所以 cache.clear() 就是 map.clear(),也就是说,缓存其实就是本地存放的一个 Map 对象,每一个 SqlSession 都会存放一个 Map 对象的引用,那么这个 cache 是什么时候创建的呢?

因为 Executor 是执行器,用来执行SQL请求,而且清除缓存的方法也是在 Executor 中执行,所以缓存很有可能也是在这里创建的。

查看 Executor 接口实现类,发现有 query() 方法里面执行了 createCacheKey()方法:

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

因为一级缓存是一个Map结构,所以这个方法是在创建缓存Map里的key值,由 MappedStatement ,parameterObject 查询参数,RowBounds 分页对象 ,BoundSql sql语句 组成的一个 CacheKey 对象

也就是说,在查询前,会先生成一个 CacheKey , 然后再查询

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ...
	// 如果有缓存,就从缓存取数据
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
        // 这个主要是处理存储过程用的
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
        // 没有缓存则查询数据库
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
    ...
    return list;
}

如果在 localCache.getObject(key) 缓存中没有命中的话,就从 queryFromDatabase()数据库查, 查到结果后再把结果放进缓存 Map 里面

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值