准备系列-Mybatis(十五) TKmybatis 一级缓存及缓存问题解决方案

429 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 15 天,点击查看活动详情

前面我们讲解了Mybatis的事务,今天我们着重讲一下缓存,大家都知道缓存很重要,有利于提高我们的查询效率,那么我们到底如何配置mybatis的一级缓存及二级缓存,现在我来带领大家 深入了解一下mybatis的缓存机制

1.Mybatis缓存

  • 什么是缓存 ?
    • 缓存的本质就是用空间换时间,牺牲数据的实时性,以服务器内存中的数据暂时代替从数据库读取最新的数据,减少数据库IO,减轻服务器压力,减少网络延迟,从而提高访问速度
  • 为什么要设置mybatis缓存?
    • 当然是为了减少了与SQL数据库的I/O交互, 提升查询效率。
  • 一级缓存和二级缓存的区别
    • 一级缓存是 SqlSession 级别的缓存。在操作数据库时需要构造 SqlSession 对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的是 SqlSession 之间的缓存数据区(HashMap)是互相不影响
    • 二级缓存 是 Mapper 级别的缓存,多个 SqlSession 去操作同一个 Mapper 的 sql 语句,多个 SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的
  • 缓存的优先级
    • mybatis 发起的查询,作用顺序为:二级缓存 -> 一级缓存 -> 数据库

image.png

1.1 mybatis一级缓存

Mybaits 中一级缓存也就做本地缓存, 同一个 sqlSession 中两次执行相同的 sql 语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取,从而提高查询效率

  • Mybatis 默认开启一级缓存
  • 一级缓存是在会话也就是SqlSession层面实现的
  • 一级缓存的作用范围是在同一个SqlSession中,不同的SqlSession, 即使查询相同的SQL语句也不会走缓存。
  • 当一个 sqlSession 结束后该 sqlSession 中的 一级缓存也就不存在了

根据ID查 数据的一级缓存图,如下所示

image.png

 原理
一级缓存区域是根据 SqlSession 为单位划分的。 每次查询会先从缓存区域找,如果找不到从数据库查询,查询到数据将数据写入缓存。 Mybatis 内部存储缓存使用一个 HashMap

  • key 为 hashCode+sqlId+Sql 语句。
  • value 为 从查询出来映射生成的 java 对象
  • sqlSession 执行 insert、update、delete 等操作 commit 提交后会清空缓存区域

按照这个说法, mybatis默认开启一级缓存, 我们来测试一下

1.2 1级缓存测试

写一个简单的 TestController 测试类, 我们看下 user1 和 user2 两个相同的查询语句, 是否会走 一级缓存, 查了几遍数据库???

/**
 * 探活接口
 */
@RequestMapping("/temp/cache")
@ResponseBody
public Object cache() {

    UserInfoPO user1 = userService.cacheUser("11");
    UserInfoPO user2 = userService.cacheUser("11");
    log.info("user1==" + JSONUtil.toJsonStr(user1));
    log.info("user2==" + JSONUtil.toJsonStr(user2));
    log.info("result:" + (user1 == user2));
    return "pong";
}

Service 层代码

UserInfoPO cacheUser(String uid);
===============================

@Resource
private UserInfoMapper mapper;

@Override
public UserInfoPO cacheUser(String uid){
    return mapper.selectUser(uid);
}

mapper文件实现

@Select("select * from user_info where id =#{uid}")
UserInfoPO selectUser(@Param(value = "uid")String uid);

执行结果-没走缓存逻辑

curl 127.0.0.1:8800/temp/cache, 看下执行结果

  • 创建了 2个SqlSession 明显不是一个SqlSession ,所以不会走缓存
  • result:false 说明两个是不同的内存地址
  • 所以是没有走 mybatis 一级缓存的 ??? 为什么, 不是默认 开启么 ?

原因就是 mybatis与 Spring 整合后进行 mapper 代理开发后,Spring 按照 mapper 的模板去生成 mapper 代理对象, 模板中在最后会统一关闭 SqlSession,每一次的 新的语句就会重新生成SqlSession, 所以是不支持一级缓存,每一次查询都是创建一个SqlSession

那如何才能让多次查询 都是一个SqlSession呢?

事务, 加上事务保证同一个事务内的SqlSession是同一个

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6a61bf39] was not registered for synchronization because synchronization is not active
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1045514f] will not be managed by Spring
==>  Preparing: select * from user_info where id =? 
==> Parameters: 11(String)
<==    Columns: id, user_id, user_name, age, address, order_ids, goods, sort_order, is_del, addtime, modtime
<==        Row: 11, 126, kk, 49, 深圳, <<BLOB>>, <<BLOB>>, 0, 0, 0, 0
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6a61bf39]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38827e24] was not registered for synchronization because synchronization is not active
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@1045514f] will not be managed by Spring
==>  Preparing: select * from user_info where id =? 
==> Parameters: 11(String)
<==    Columns: id, user_id, user_name, age, address, order_ids, goods, sort_order, is_del, addtime, modtime
<==        Row: 11, 126, kk, 49, 深圳, <<BLOB>>, <<BLOB>>, 0, 0, 0, 0
<==      Total: 1

Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38827e24]
2023-02-14 21:51:26.496  INFO 92506 --- [nio-8800-exec-1] c.j.tdmybatis.controller.TestController  : user1=={"address":"深圳","modtime":0,"goods":"{\"deptId\": 3, \"deptName\": \"部门3\", \"deptLeaderId\": 4}","addtime":0,"id":11,"age":49}
2023-02-14 21:51:26.496  INFO 92506 --- [nio-8800-exec-1] c.j.tdmybatis.controller.TestController  : user2=={"address":"深圳","modtime":0,"goods":"{\"deptId\": 3, \"deptName\": \"部门3\", \"deptLeaderId\": 4}","addtime":0,"id":11,"age":49}
2023-02-14 21:51:26.497  INFO 92506 --- [nio-8800-exec-1] c.j.tdmybatis.controller.TestController  : result:false
1.3 加事务,同一个SqlSession 走缓存

因为上面的测试 不是同一个SqlSession 所以他是不会走 mybatis一级缓存的

我们现在 给代码TestController上加上事务 @Transactional 注解, 保证方法内的所有SqlSession都是同一个, 这样我们再次测试下

/**
 * 探活接口
 */
@Transactional
@RequestMapping("/temp/cache")
@ResponseBody
public Object cache() {

    UserInfoPO user1 = userService.cacheUser("11");
    UserInfoPO user2 = userService.cacheUser("11");
    log.info("user1==" + JSONUtil.toJsonStr(user1));
    log.info("user2==" + JSONUtil.toJsonStr(user2));
    log.info("result:" + (user1 == user2));
    return "pong";
}

image.png

再次执行 看看结果 curl 127.0.0.1:8800/temp/cache

  • 只有一次 Creating a new SqlSession
  • 说明事务内用的是同一个SqlSession
  • 只查询了一次SQL : select * from user_info where id =?
  • result:true user1 ,user2的内存地址相同 ,是同一个对象
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5560c53]
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@5c6df755] will be managed by Spring
==>  Preparing: select * from user_info where id =? 
==> Parameters: 11(String)
<==    Columns: id, user_id, user_name, age, address, order_ids, goods, sort_order, is_del, addtime, modtime
<==        Row: 11, 126, kk, 49, 深圳, <<BLOB>>, <<BLOB>>, 0, 0, 0, 0
<==      Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5560c53]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5560c53] from current transaction
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5560c53]
2023-02-14 21:58:46.926  INFO 92552 --- [nio-8800-exec-1] c.j.tdmybatis.controller.TestController  : user1=={"address":"深圳","modtime":0,"goods":"{\"deptId\": 3, \"deptName\": \"部门3\", \"deptLeaderId\": 4}","addtime":0,"id":11,"age":49}
2023-02-14 21:58:46.927  INFO 92552 --- [nio-8800-exec-1] c.j.tdmybatis.controller.TestController  : user2=={"address":"深圳","modtime":0,"goods":"{\"deptId\": 3, \"deptName\": \"部门3\", \"deptLeaderId\": 4}","addtime":0,"id":11,"age":49}
2023-02-14 21:58:46.927  INFO 92552 --- [nio-8800-exec-1] c.j.tdmybatis.controller.TestController  : result:true

同一个SqlSession 的确只会查询1次Mysql数据库

第二次查询根本没有重新查询,因为没有SQL语句,而是走的Mybatis的一级缓存处理的

image.png

2.一级缓存造成问题

2.1 一级缓存的问题

一级缓存会造成脏数据, 很简单的例子

@RequestMapping("/temp/cache")
@ResponseBody
@Transactional
public Object cache() {

    UserInfoPO user1 = userService.cacheUser("11");

    //对user1的属性进行修改,先不入库
    user1.setAge(123);
    user1.setUserName("456");
    user1.setAddress("78910");

    //再次查询user2
    // 看看 user2的数值,按道理我修改的是user1
    //user2 是从db中取出来的,应该还是 原来的值,我们看下改变了每
    UserInfoPO user2 = userService.cacheUser("11");
    log.info("user2==" + JSONUtil.toJsonStr(user2));
    log.info("result:" + (user1 == user2));
    return "pong";
}
  • 第一步我是修改的user1,并没有更新数据库
  • 第二步 从DB中查询出来 user2
  • 发现user2的值被修改了
  • 根本原因是 没有再次查user2,走了user1的mybatis缓存, user1修改的时候,内存地址中的数据已经被修改了
  • user1和user2是同一个内存地址,就导致看似是从db中查询出来的user2,其实还是同一个user1
  • 两个user1和user2全都更改
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7f4a1096]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@65eeed60] will be managed by Spring
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7f4a1096]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7f4a1096] from current transaction
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7f4a1096]
2023-02-14 23:38:18.878  INFO 16972 --- [nio-8800-exec-1] c.j.tdmybatis.controller.TestController  : user2=={"address":"78910","modtime":0,"goods":"{\"deptId\": 3, \"deptName\": \"部门3\", \"deptLeaderId\": 4}","userName":"456","addtime":0,"id":11,"age":123}
2023-02-14 23:38:18.878  INFO 16972 --- [nio-8800-exec-1] c.j.tdmybatis.controller.TestController  : result:true
2.2 一级缓存的解决办法

设置 mybatis.configuration.local-cache-scope=statement

image.png

但是实战没什么效果, 依旧会走一级缓存

加注解 指定方法关闭缓存

在UserMapper 的方法中 加flushCache注解 @Options(flushCache = Options.FlushCachePolicy.TRUE)

可以有效的防止一级缓存的问题

@Options(flushCache = Options.FlushCachePolicy.TRUE)
@Select("select * from user_info where id =#{uid}")
UserInfoPO selectUser(@Param(value = "uid")String uid);

再次执行 方法, 看看是不是同一个

  • 不是同一个 user1,user2了
  • user1是修改过的
  • user2是从db中实时查询出来的
  • user1,user2 不是同一个内存地址,互不影响
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@9345b78]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@637b983a] will be managed by Spring
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@9345b78]
2023-02-14 23:56:47.066  INFO 20572 --- [nio-8800-exec-1] c.j.tdmybatis.controller.TestController  : user1=={"address":"78910","modtime":0,"goods":"{\"deptId\": 3, \"deptName\": \"部门3\", \"deptLeaderId\": 4}","userName":"456","addtime":0,"id":11,"age":123}
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@9345b78] from current transaction
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@9345b78]
2023-02-14 23:56:47.067  INFO 20572 --- [nio-8800-exec-1] c.j.tdmybatis.controller.TestController  : user2=={"address":"深圳","modtime":0,"goods":"{\"deptId\": 3, \"deptName\": \"部门3\", \"deptLeaderId\": 4}","addtime":0,"id":11,"age":49}
2023-02-14 23:56:47.067  INFO 20572 --- [nio-8800-exec-1] c.j.tdmybatis.controller.TestController  : result:false

image.png

至此 我们理解了一级缓存及一级缓存的问题, 并告知大家如何解决问题