一级缓存
- 基于PerpetualCache的HashMap本地缓存
- 作用域为sqlsession
- 当sqlsession flush或close后,该sqlsession的所有Cache都会清空。
- 默认开启。
介绍
在一次数据库会话中,执行相同SQL语句,会优先命中一级缓存,避免直接去数据库查询,提高性能。
每个SqlSession都持有Executor,每个Executor都有一个LocalCache,当用户发起查询时,Mybatis会根据用户当前执行语句生成MapperStatement,在LocalCache进行查询,如果缓存命中,直接返回结果给用户。如果缓存没有命中,查询数据库,结果写入LocalCache,最后返回结果给用户。
实验1 多次相同查询
public static void main(String[] args) throws IOException {
//获取sqlSession会话
SqlSession sqlSession = sqlSessionFactory.openSession();
//获取Mapper
DeptMapper deptMapper = sqlSession.getMapper(DeptMapper.class);
System.out.println(("查询第1次:{}"+deptMapper.selectDept(5)));
System.out.println(("查询第2次:{}"+deptMapper.selectDept(5)));
System.out.println(("查询第3次:{}"+deptMapper.selectDept(5)));
//关闭会话
sqlSession.close();
}
主动调用的三次查询,实际只有第一次真正的去数据库查询,后续查询使用一级缓存。
实验2 修改数据后再查询
public static void main(String[] args) throws IOException {
//获取sqlSession会话
SqlSession sqlSession = sqlSessionFactory.openSession();
//获取Mapper
DeptMapper deptMapper = sqlSession.getMapper(DeptMapper.class);
System.out.println(("查询第1次:{}"+deptMapper.selectDept(5)));
System.out.println(("查询第2次:{}"+deptMapper.selectDept(5)));
System.out.println(("查询第3次:{}"+deptMapper.selectDept(5)));
Map<String,String> map=new HashMap<>();
map.put("name","执行Update方法");
map.put("id","5");
System.out.println(("执行update方法----------------------------------------------"));
deptMapper.updateDept(map);
System.out.println(("执行update方法----------------------------------------------"));
System.out.println(("查询第4次:{}"+deptMapper.selectDept(5)));
//关闭会话
sqlSession.close();
}
当对数据库发生了修改操作,再执行相同查询时,一级缓存会失效。
实验3 sqlsession会不会缓存共享
public static void main(String[] args) throws IOException {
//获取sqlSession会话
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
//获取Mapper
DeptMapper deptMapper1 = sqlSession1.getMapper(DeptMapper.class);
DeptMapper deptMapper2 = sqlSession2.getMapper(DeptMapper.class);
System.out.println(("sqlSession1查询第1次:{}"+deptMapper1.selectDept(5)));
System.out.println(("sqlSession2查询第1次:{}"+deptMapper2.selectDept(5)));
Map<String,String> map=new HashMap<>();
map.put("deptName","执行Update方法");
map.put("id","5");
System.out.println(("执行update方法----------------------------------------------"));
System.out.println(("sqlSession1修改数据:{}"+deptMapper1.updateDept(map)));
System.out.println(("sqlSession1查询第2次:{}"+deptMapper1.selectDept(5)));
System.out.println(("sqlSession2查询第2次:{}"+deptMapper2.selectDept(5)));
//关闭会话
sqlSession1.close();
sqlSession2.close();
}
可以看出SqlSession2查询到了脏数据,SqlSession1中已经修改了数据,而SqlSession2仍然获取自己缓存的旧数据。证明:一级缓存只在数据库会话内部共享。
一级缓存执行时序图
一级缓存—源码分析
-
SqlSession提供操作数据库的方法,和数据库操作的相关的职责都会委派给Executor。
-
所有的数据库操作都会通过Executor来执行。
-
Executor创建时
-
Executor实现类
-
BaseExecutor: BaseExecutor是一个实现了Executor接口的抽象类,定义若干抽象方法,在执行的时候,把具体的操作委托给子类进行执行。
-
在上面的时序表中可以看出LocalCache的查询和写入都在Executor的内部完成。在BaseExecutor的成员变量中存在LocalCache的变量。
-
当执行SQL时,会通过defaultSqlSession的selectList方法,再调用BaseExecutor的query()方法,根据传入的key,从LocalCache中获取缓存数据。
-
Cachekey的获取过程一个初始乘数和hashcode,共同维护一个updateList,在update方法中通过计算后,将对象添加到updataList中。最后重写equals方法,Key的比对方法,对hashcode,checksum,count进行比较,还对updateList中所有元素遍历比较。只有两条SQL的五个值完全相同,才判断为相同SQL。
Statement ID+Offset + Limit +SQL +Params
-
如果缓存中获取数据为空,就去数据库中查询queryFromDataBase。
-
只要进入数据库查询,就删除当前的之前的缓存,放入新的缓存。
-
最后对一级缓存作用域进行判断,是不是STATEMENT,如果是就清空缓存。
-
执行Update会清空缓存。clearLocalCache();
-
总结:
- 一级缓存的生命周期与SqlSession一致。
- 内部设计简单,采用没有容量限定的HashMap。
- 一级缓存的最大范围就是SqlSession内部,在多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,缓存基本应该设置为Statement。
二级缓存
开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询
当开启缓存后,会先查询二级缓存——>一级缓存——>数据库
开启二级缓存配置
-
Mybatis-config.xml中开启缓存
<setting name="cacheEnabled" value="true"/>
-
MyBatis的映射XML中配置cache或者 cache-ref
- Cache声明这个namespace使用二级缓存
<cache /> type:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。 eviction: 定义回收的策略,常见的有FIFO,LRU。 flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。 size: 最多缓存对象的个数。 readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。 blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
- Cache-ref代表引用别的命名空间的Cache配置
<cache-ref namespace="mapper.DeptMapper"/>
- 二级缓存特点
- sqlsession没有调用commit()方法时,二级缓存没有起作用
- 当提交事务时,sqlSession1查询完数据后,sqlsession2的相同的查询,使用了缓存,缓存的命中率是0.5。
- 更新数据库并提交事务后,sqlsession2的查询走了数据库,没有走Cache。
- MyBatis的二级缓存不适应用于映射文件中存在多表查询的情况,namespace级别,不能感应其他namespace的变化; 可以使用cache-ref使用同一个缓存空间,但是造成缓存粒度变粗。
- Cache声明这个namespace使用二级缓存
Cache接口
- SynchronizedCache:同步Cache,实现比较简单,直接使用synchronized修饰方法。
- LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。
- SerializedCache:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。
- LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value。 PerpetualCache:
作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。
总结
MyBatis更适合单纯作为一个ORM框架使用。