赞
踩
缓存:存在于内存中的临时数据。
为什么使用缓存?
减少和数据库的交互次数,提高执行效率。(因为查询数据库是一件很费时很费效率的事,还涉及一些硬盘等IO操作,而缓存是存在内存中的,读取都很快,而且效率高)
缓存的作用:减少数据库查询次数,减轻数据库查询压力,提高查询的速度!
缓存的缺点:以空间换时间技术,需要额外内存开销。如果数据库中的数据和缓存中的数据不同步,那么可能出现脏数据,这个问题缓存的一致性问题!
浏览器缓存:缓存静态资源,减轻Web服务器访问压力!
缓存中有,先查询缓存。缓存中没有,那么查询数据库。这样的话不用每次都查询数据库。减轻数据库的压力。提高查询效率!!!
第一次查询的时候,由于缓存中没有,那么去查询数据库返回给客户端。同时还会把这个次查询的数据放入缓存。
第二次查询同样的数据时候,发现缓存中曾经有查询过的数据,那么直接从缓存中读取。不必再次查询数据库,减轻数据库服务器压力,缓存中有就查缓存,缓存中没有就查数据库!
如果数据库中数据发生了修改,那么缓存就会清空,保持数据缓存的一致性!防止脏数据!
mybatis提供查询缓存,如果缓存中有数据就不用从数据库中获取,用于减轻数据压力,提高系统性能。
一级缓存是SqlSession级别的缓存。在操作数据库时需要构造 sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。
二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。
一级缓存指的就是在sqlsession中有一个map结构的数据区域,这个区域就是一级缓存区域。
一级缓存中的key,是由sql语句、条件、statement等信息组成一个唯一值。
一级缓存中的value,就是查询出的结果对象。
一级缓存是session级别的,同一个session! 1级缓存是系统自带,不需要手动开启!
相同的 sql 和 参数
必须是在一个会话 Session
当中
必须是执行 相同的方法
必须是相同的 namespace
(同一个命名空间 -> 同一个mapper文件)
不能够在查询之前执行 clearCache
中间不能执行 任何 update
,delete
,insert
(会将SqlSession中的数据全部清空)
一级缓存是SqlSession
级别的缓存。我们都知道在操作数据库时需要构造 sqlSession对象,而在sqlSession对象中有一个数据结构(HashMap)用于存储缓存数据。 从图上,我们可以看出,一级缓存区域是根据SqlSession为单位划分的。每次查询都会先从缓存区域找,如果找不到就会从数据库查询数据,然后将查询到的数据写入一级缓存中。Mybatis内部存储缓存使用的是一个HashMap对象,key为 hashCode + sqlId + sql 语句。而value值就是从查询出来映射生成的java对象。而为了保证缓存里面的数据肯定是准确数据避免脏读,每次我们进行数据修改后(增、删、改操作)就会执行commit操作,清空缓存区域(无论执行的是哪一行数据下吗面的数据,一级缓存里面的数据都会被清空)。
- /**
- * 测试1级缓存
- */
- public class TestCache1 {
-
- @Test
- public void test1() throws Exception{
- try {
- String resources = "mybatis-config.xml";
- InputStream inputStream = Resources.getResourceAsStream(resources);
- SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
- SqlSession sqlSession = sqlSessionFactory.openSession(true);
- UserMapper um = sqlSession.getMapper(UserMapper.class);
- User user1 = um.findById(1);
- User user2 = um.findById(1);
- System.out.println(user1==user2);
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- }
- }
- }
运行结果: 用两张图来总结: 第一次:查数据库,放入到缓存中。 第二次:直接从缓存中获取。 下面这段代码中就使用不到缓存
- @Test
- public void test2() throws Exception{
- try {
- String resources = "mybatis-config.xml";
- InputStream inputStream = Resources.getResourceAsStream(resources);
- SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
-
- SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
- SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
-
- UserMapper um1 = sqlSession1.getMapper(UserMapper.class);
- UserMapper um2 = sqlSession2.getMapper(UserMapper.class);
-
- User user1 = um1.findById(1);
- User user2 = um2.findById(1);
-
- System.out.println(user1==user2);
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- }
- }
运行结果:
用两张图来总结:
第一次查询:sqlSession1查询数据库,放入到缓存中。 第二次查询:sqlSession2查询数据库,放入到缓存中。 记住是一级缓存只能是同一个SqlSession对象就行了。
一级缓存也叫本地缓存(LocalCache),Mybatis的一级缓存是会话级别(SqlSession)层面进行缓存的。Mybatis的一级缓存是默认开启的。我们开发项目中不需要做任何配置,但是如果想关闭一级缓存,可以在mybatis配置文件的settings下面使用localCacheScopde=statement
来关闭。
- <settings>
- <!-- localCacheScope是本地缓存(一级缓存)的作用域,只有两种取值:SESSION和STATEMENT,取STATEMENT意味着关闭一级缓存-->
- <setting name="localCacheScope" value="STATEMENT"/>
- </settings>
SqlSession和Executor都是接口 ,而实现是 DefaultSqlSession 和 CacheExecutor
Client
相当于测试方法 Test1
User@Proxy
动态代理
Executor
接口 才是去数据库中拿数据的跑腿的
SqlSession
是接口,一级缓存的实现是通过 CacheExecutor
实现的
程序入口处断点
执行MapperProxy动态代理invoke MapperMethod调用sqlsession的selectOne方法 本质调用DefaultSqlSession的selectList方法 调用BaseExecutor的query方法 BaseExecutor的query方法生成缓存的key
可以看出缓存key中是包含了方法和namespace和会话 这些必须相同才会去做一个缓存命中 这里面封装了缓存唯一的key
继续执行BaseExecutor的query方法判断缓存是否存在 如果缓存中不存在,执行查询数据库 缓存中查询到数据存入缓存中 如果缓存中存在,执行查询缓存,PerpetualCache调用getObject方法
DefaultSqlSession中有一个CacheExecutor
CacheExecutor 中有一个 Simpleexexutor
Simpleexexutor 中有一个叫 LocalCache (PerpetualCache类型)
LocalCache才是真正的存储缓存的地方
LocalCache 中有一个叫cache (Hashmap <Object,Object>类型的)
在执行update、insert、delete、flushCache="true"、commit、rollback、LocalCacheScope.STATEMENT等情况下,一级缓存就都会被清空。
- @Override
- public void clearLocalCache() {
- if (!closed) {
- localCache.clear();
- localOutputParameterCache.clear();
- }
- }
update时,一级缓存会被清空。delete和insert都是调用这个update。可以从SqlSession的insert、update、delete方法跟踪。 LocalCacheScope.STATEMENT时,一级缓存会被清空。在BaseExecutor里的query方法中: 事务提交回滚时,一级缓存会被清空。 flushCache="true"时,一级缓存会被清空。
下面就是一级缓存key的创建过程
- @Override
- public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
- if (closed) {
- throw new ExecutorException("Executor was closed.");
- }
- CacheKey cacheKey = new CacheKey();
- cacheKey.update(ms.getId());
- cacheKey.update(rowBounds.getOffset());
- cacheKey.update(rowBounds.getLimit());
- cacheKey.update(boundSql.getSql());
- List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
- TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
- // mimic DefaultParameterHandler logic
- for (ParameterMapping parameterMapping : parameterMappings) {
- if (parameterMapping.getMode() != ParameterMode.OUT) {
- Object value;
- String propertyName = parameterMapping.getProperty();
- if (boundSql.hasAdditionalParameter(propertyName)) {
- value = boundSql.getAdditionalParameter(propertyName);
- } else if (parameterObject == null) {
- value = null;
- } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
- value = parameterObject;
- } else {
- MetaObject metaObject = configuration.newMetaObject(parameterObject);
- value = metaObject.getValue(propertyName);
- }
- cacheKey.update(value);
- }
- }
- if (configuration.getEnvironment() != null) {
- // issue #176
- cacheKey.update(configuration.getEnvironment().getId());
- }
- return cacheKey;
- }
key的生成策略:id + offset + namespace+ sql + param value + environment id,这些值都相同,生成的key就相同。
一级缓存的生命周期和SqlSession对象的生命周期一致。所以缓存维护在SqlSession中的属性executor里。
一级缓存默认开启。可以通过修改配置项把一级缓存关掉。
清空一级缓存的方式有:
- 执行update、insert、delete、rollback这些方法
-
- --------------------------------------
- 执行sqlSession.clearCache();
-
- --------------------------------------
-
- 在mybatis的配置文件中的setting里面 设置LocalCacheScope属性为STATEMENT
二级缓存指的就是同一个namespace下的mapper,二级缓存中,也有一个map结构,这个区域就是二级缓存区域。二级缓存中的key是由sql语句、条件、statement等信息组成一个唯一值。二级缓存中的value,就是查询出的结果对象。
二级缓存,可以跨session!二级缓存是要配置,然后手动开启!
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace级别的,可以被多个SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库。
二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是可以横跨跨SqlSession的。
作为一个作用范围更广的缓存,它肯定是在SqlSession 的外层,否则不可能被多个SqlSession 共享。而一级缓存是在SqlSession 内部的,所以第一个问题,肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。第二个问题,二级缓存放在哪个对象中维护呢? 要跨会话共享的话,SqlSession 本身和它里面的BaseExecutor 已经满足不了需求了,那我们应该在BaseExecutor 之外创建一个对象。
实际上MyBatis 用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis 在创建Executor 对象的时候会对Executor 进行装饰。CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器Executor 实现类,比如SimpleExecutor 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。
二级缓存区域是根据mapper的namespace划分的,相同namespace的mapper查询数据放在同一个区域,可以理解为二级缓存区域是根据mapper划分,也就是根据命名空间来划分的,如果两个mapper文件的命名空间一样,那样,不同的SqlSession之间就可以共享一个mapper缓存。
在 mybatis 中,二级缓存有全局开关和分开关, 全局开关, 在 mybatis-config.xml
中如下配置:
- <settings>
- <!-- cacheEnabled是二是级缓存的总开关,置为false代表关闭二级缓存 -->
- <setting name="cacheEnabled" value="true"/>
- </settings>
默认是为 true, 即默认开启总开关。
由于mybaits的二级缓存是mapper范围级别,所以除了在 mybatis-config.xml
设置二级缓存的总开关外,还要在具体的mapper.xml中开启二级缓存。
设置cache标签,同时在查询标签里面设置useCache的属性(通产来说useCache的默认值为true,但是可以通过设置为false来操作某条语句不支持缓存)
但是二级缓存还是有缺点, 如果当前操作是多表查询,查询之后结果也会缓存,但是如果是多线程场景下,另外一个线程将多表中的某一张表做了更新? 二级缓存数据不会过更行,会引发脏读问题。解决办法:在映射文件中引入
<cache-ref namespace="com.it.mapper.EmployeeMapper"/>标签
该标签的作用是共享namespace值得mappper文件
mappper.xml设置如下:
- <mapper namespace="com.it.mapper.EmployeeMapper">
-
- <cache-ref namespace="com.it.mapper.EmployeeMapper"/>
- <cache type="org.apache.ibatis.cache.impl.PerpetualCache" eviction="LRU" flushInterval="40000" size="1024" readOnly="false" blocking="false"/>
-
- <select id="finEmployeeById" resultMap="empMap" useCache="true">
- select * from tb_employee where id=#{id}
- </select>
-
- </mapper>
缓存的配置和缓存实例会被绑定到 SQL 映射文件的命名空间中。 因此,同一命名空间中的所有语句和缓存将通过命名空间绑定在一起。 每条语句可以自定义与缓存交互的方式,或将它们完全排除于缓存之外,这可以通过在每条语句上使用两个简单属性来达成。 默认情况下,语句会这样来配置:
- <select ... flushCache="false" useCache="true"/>
-
- <insert ... flushCache="true"/>
- <update ... flushCache="true"/>
- <delete ... flushCache="true"/>
鉴于这是默认行为,显然你永远不应该以这样的方式显式配置一条语句。但如果你想改变默认的行为,只需要设置 flushCache 和 useCache 属性。比如,某些情况下你可能希望特定 select 语句的结果排除于缓存之外,或希望一条 select 语句清空缓存。类似地,你可能希望某些 update 语句执行时不要刷新缓存。
要用到二级缓存的每一个实体类都需要实现Serializable接口
- public class Employee implements Serializable {
-
- private static final long serialVersionUID = -5150219292948138785L;
-
- //标识主键自增
- @TableId(value = "ID",type = IdType.AUTO)
- private Integer id;
- }
这里要记住一个地方,第二次查询必须要等第一次的session关闭以后才可以。否则二级缓存没有作用,依然还会执行二次select语句发送至数据库。
- @Test
- public void testCache2() throws Exception {
- String resource = "mybatis-config.xml";
- Reader reader = Resources.getResourceAsReader(resource);
- SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
-
- SqlSession sqlSession1 = sqlSessionFactory.openSession();
- EmployeeMapper employeeMapper1 = sqlSession1.getMapper(EmployeeMapper.class);
- Employee employee1 = employeeMapper1.finEmployeeById(2);
- Employee employee2 = employeeMapper1.finEmployeeById(2);
- System.out.println(employee1);
-
- // 这里要记住一个地方,第二次查询必须要等第一次的session关闭以后才可以。否则二级缓存没有作用,依然还会执行二次select语句发送至数据库。
- sqlSession1.close();
- System.out.println(employee1==employee2);
-
-
- SqlSession sqlSession2 = sqlSessionFactory.openSession();
- EmployeeMapper employeeMapper2 = sqlSession2.getMapper(EmployeeMapper.class);
- Employee employee3 = employeeMapper2.finEmployeeById(2);
- System.out.println(employee3);
- sqlSession2.close();
- System.out.println(employee1==employee3);
- }
test的运行结果。
以上结果, 分几个过程解释:
第一阶段:
在第一个 SqlSession 中, 查询出 employee对象, 此时发送了 SQL 语句;
SqlSession 再次查询出 employee对象, 此时不发送 SQL 语句, 日志中打印了 「Cache Hit Ratio」, 代表二级缓存使用了, 但是因为此时sqlsession1没有关闭,二级缓存没有起作用,所以没有命中。二级缓存没有起作用,所以一级缓存起作用,
由于是一级缓存, 因此, 此时两个对象是相同的。
调用了 sqlSession.close(), 此时将数据序列化并保持到二级缓存中。
第二阶段:
新创建一个 sqlSession.close() 对象;
查询出 user对象,直接从二级缓存中拿了数据, 因此没有发送 SQL 语句, 此时查了 3 个对象,但只有一个命中, 因此命中率 1/3=0.333333;
type 用于指定缓存的实现类型, 默认是PERPETUAL
, 对应的是 mybatis 本身的缓存实现类org.apache.ibatis.cache.impl.PerpetualCache
。
eviction 对应的是回收策略, 默认为 LRU
。
LRU
: 最近最少使用, 移除最长时间不被使用的对象。
FIFO
: 先进先出, 按对象进入缓存的顺序来移除对象。
SOFT
: 软引用, 移除基于垃圾回收器状态和软引用规则的对象。
WEAK
: 弱引用, 移除基于垃圾回收器状态和弱引用规则的对象
flushInterval
对应刷新间隔, 单位毫秒
, 默认值不设置, 即没有刷新间隔, 缓存仅仅在刷新语句时刷新。
如果设定了之后, 到了对应时间会过期, 再次查询需要从数据库中取数据。
size对应为引用的数量,即最多的缓存对象数据, 默认为 1024
。
readOnly 为只读属性, 默认为 false
false: 可读写, 在创建对象时, 会通过反序列化得到缓存对象的拷贝。 因此在速度上会相对慢一点, 但重在安全。
true: 只读, 只读的缓存会给所有调用者返回缓存对象的相同实例。 因此性能很好, 但如果修改了对象, 有可能会导致程序出问题。
blocking 为阻塞, 默认值为 false。 当指定为 true 时将采用 BlockingCache
进行封装。
使用 BlockingCache
会在查询缓存时锁住对应的 Key,如果缓存命中了则会释放对应的锁,否则会在查询数据库以后再释放锁,这样可以阻止并发情况下多个线程同时查询数据。
如上图所示,当开一个会话时,一个SqlSession
对象会使用一个Executor
对象来完成会话操作,MyBatis的二级缓存机制的关键就是对这个Executor对象做文章。如果用户配置了"cacheEnabled=true
",那么MyBatis在为SqlSession
对象创建Executor对象时,会对Executor对象加上一个装饰者:CachingExecutor
,这时SqlSession
使用CachingExecutor
对象来完成操作请求。CachingExecutor
对于查询请求,会先判断该查询请求在Application级别的二级缓存中是否有缓存结果,如果有查询结果,则直接返回缓存结果;如果缓存中没有,再交给真正的Executor对象来完成查询操作,之后CachingExecutor
会将真正Executor
返回的查询结果放置到缓存中,然后在返回给用户。 CachingExecutor
是Executor
的装饰者,以增强Executor
的功能,使其具有缓存查询的功能,这里用到了设计模式中的装饰者模式,
CachingExecutor
和Executor
的接口的关系如下类图所示:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。