011-MyBatis缓存机制

一级缓存

Mybatis对缓存提供支持,但是在没有配置的默认情况下,它只开启一级缓存,一级缓存只在同SqlSession内有效。在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用同一个Mapper方法,往往只执行一次SQL,因为第一次查询后,MyBatis会将结果放入缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会去查询数据库。
c7d54135f0a47c2f3db9661a8df10859
每个SqlSession中持有了Executor,每个Executor中有一个LocalCache。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户。具体实现类的类关系图如下图所示。
31395a7253a656c44efe28da9da64d7a

生命周期

  1. MyBatis在开启一个数据库会话时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象。Executor对象中持有一个新的PerpetualCache对象

  2. 当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉

  3. 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用

  4. 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是SqlSession对象仍可使用。

  5. SqlSession中执行了任何一个DML操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用

    工作流程

    928a3092b9599f5fde7769d34a508a9f

    使用缓存条件

    mybatis认为对于两次查询,如果以下条件都完全一样,那就认为它们是完全相同的两次查询:

  6. 传入的statementId

  7. 查询时要求的结果集中的结果范围

  8. 这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串

  9. 传递给java.sql.Statement要设置的参数值

配置

在MyBatis的配置文件中开启一级缓存
value有两个选项:

  • SESSION(默认):即在同一个会话中执行的所有语句,都会共享这一个缓存
  • STATEMENT:即只对当前执行的这一个Statement有效(相当于无缓存机制)
1
<setting name="localCacheScope" value="SESSION"/>

场景测试

场景一:同一数据库会话中,可以看到仅第一次查询了数据库,后面两次都是从一级缓存获取结果
1f46bab1a772c6dbf0752b0a49d3c25a
场景二:同一数据库会话中,两次查询中间执行了一次插入语句,第二次查询时导致一级缓存失效,重新查询数据库
010b4c36ee04a992db534ab75554835f
场景三:开启两个会话SqlSession(A和B),当会话B更新了数据之后,会话A并没有读取到新的数据,还是从会话A的缓存获取结果,相反会话B读取到了新的数据,因为更新操作会把会话B缓存清空,会话B重新从数据库获取数据。说明一级缓存只在同一数据库会话内部共享。
48b2c8642d1c1d0311c48ed0657aac88

小结

  1. 一级缓存在同一个SqlSession下共用缓存
  2. 一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺
  3. 一级缓存再有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement

二级缓存

一级缓存其最大的共享范围就是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询,具体的工作流程如下所示。
1ca85a507ed0d182d7357a0996dead7e

配置

在MyBatis的配置文件中开启二级缓存

1
<setting name="cacheEnabled" value="true"/>

XML文件配置使用二级缓存,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--开启本mapper的namespace下的二级缓存-->
<!--
eviction:代表的是缓存回收策略,目前MyBatis提供以下策略。
(1) LRU,最近最少使用的,一处最长时间不用的对象
(2) FIFO,先进先出,按对象进入缓存的顺序来移除他们
(3) SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象
(4) WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象。这里采用的是LRU,
移除最长时间不用的对形象

flushInterval:刷新间隔时间,单位为毫秒,这里配置的是100秒刷新,如果你不配置它,那么当
SQL被执行的时候才会去刷新缓存。

size:引用数目,一个正整数,代表缓存最多可以存储多少个对象,不宜设置过大。设置过大会导致内存溢出。
这里配置的是1024个对象

readOnly:只读,意味着缓存数据只能读取而不能修改,这样设置的好处是我们可以快速读取缓存,缺点是我们没有
办法修改缓存,他的默认值是false,不允许我们修改
-->
<cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>

984afbd019e59951505aade2cf3a5a16

场景测试

场景一:关闭了一级缓存的前提下,可以看到会话A第二次查询也没能通过缓存获取数据,因为事物未提交;当关闭了会话A后,会话B第二次查询使用了会话A存下来的缓存。结论:提交完事务后,二级缓存才能对所有会话生效(包括自己)
a17db66f56f07349d23f7f7d67329139
场景二:查询和更新分别在两个不同的namspace,可以看到即使会话C更新且提交了会话,但并没有清除前面查询留下的缓存,导致会话B第二次还是使用了缓存。结论:由于MyBatis的二级缓存是基于namespace的,查询语句所在的namspace无法感应到其他namespace中的语句执行的修改,引发脏数据问题。
5ce53bb6d902ec839e334b56fb22d721
解决方法:在更新的namspace中关联引用查询的namspace,这样两个映射文件对应的Sql操作就能使用的是同一块缓存了
f89a83ca2514cd5936787658acbde62f

小结

  1. 二级缓存在同一个namespace共用缓存
  2. 二级缓存实现了不同SqlSession之间缓存数据的共享,可以通过Cache接口实现类不同的组合,对Cache的可控性也更强
  3. 在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻
  4. 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高