mybatis缓存机制
缓存概述
MyBatis提供在执行过程中查询缓存,如果缓存中有数据,则直接从缓存中获取,没有则从数据库查询,用以减轻数据压力,提高系统性能。
MyBatis的缓存分为一级缓存和二级缓存。默认情况下一级缓存和二级缓存都是默认开启的。
一级缓存是SqlSession级别的,与SqlSession生命周期是相同的。
二级缓存是全局的,可以跨线程,不同的会话之间是可以共享的。
mybatis缓存结构
一级缓存
概要
第一级缓存,也叫会话级缓存。
指的是在同一会话内如果有两次相同的查询(Sql和参数均相同),那么第二次就会命中缓存。一级缓存通过会话进行存储(其底层用hashMap存储),当会话关闭,缓存也就没有了。此外如果会话进行了修改(增删改) 操作,缓存也会被清空。
结构
源码
一级缓存命中的条件
以下数据必须全部相同才可以命中缓存(而且两次必须相邻的):
1.相同的statement id
2.相同的Session
3.相同的Sql与参数
4.返回行范围相同
1.示例代码
@Test
public void yiji() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybats.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByPrimaryKey(1);
User user2 = mapper.selectByPrimaryKey(1);
System.out.println(user==user2);
}
1.1 结果
DEBUG 08-13 00:47:03,712 ==> Preparing: select id, user_name, pass_word, email, td_id from t_user where id = ? (BaseJdbcLogger.java:137)
DEBUG 08-13 00:47:03,760 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 08-13 00:47:03,795 <== Total: 1 (BaseJdbcLogger.java:137)
**true**
1.2我们debug查看源码中放入hashMap的数据(两次是一样的):
2.不同的mappedStatementId不能命中缓存,即使sql与参数一致
<select id="findById" resultType="com.lxy.entity.User" parameterType="Integer">
SELECT * FROM user WHERE id=#{id}
</select>
<select id="findById2" resultType="com.lxy.entity.User" parameterType="Integer">
SELECT * FROM user WHERE id=#{id}
</select>
----------------------------------
@Test
public void baseCacheTest(){
SqlSession session = factory.openSession(true);
UserDao mapper = session.getMapper(UserDao.class);
User user1 = mapper.findById(4);
User user2 = mapper.findById2(4);
System.out.println(user1 == user2);
}
2.1 运行结果:false
3.RowBounds必须相同,否则不会命中缓存
@Test
public void baseCacheTest(){
SqlSession session = factory.openSession(true);
UserDao mapper = session.getMapper(UserDao.class);
RowBounds rowBounds = new RowBounds(0,10);//返回前10行
User user1 = mapper.findById(4);
List<User> list = session.selectList("com.lxy.dao.UserDao.findById",4, rowBounds);
System.out.println(user1 == list.get(0));
}
3.1运行结果:false;
4.不同session不会命中缓存
@Test
public void baseCacheTest() {
SqlSession session = factory.openSession(true);
UserDao mapper = session.getMapper(UserDao.class);
SqlSession session2 = factory.openSession(true);
UserDao mapper2 = session2.getMapper(UserDao.class);
User user1 = mapper.findById(4);
User user2 = mapper2.findById(4);
System.out.println(user1 == user2);
}
4.1运行结果:false
一级缓存失效的情况
缓存失效的情况:
1.手动清空缓存
2.配置flushCache=true
3.执行update
4.设置作用域为statement
1、手动清空缓存
1.1、测试代码
@Test
public void yiji() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybats.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByPrimaryKey(1);
//清空
sqlSession.clearCache();
User user2 = mapper.selectByPrimaryKey(1);
System.out.println(user==user2);
}
1.2、结果
DEBUG 08-13 01:00:12,170 ==> Preparing: select id, user_name, pass_word, email, td_id from t_user where id = ? (BaseJdbcLogger.java:137)
DEBUG 08-13 01:00:12,217 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 08-13 01:00:12,246 <== Total: 1 (BaseJdbcLogger.java:137)
DEBUG 08-13 01:00:12,248 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 08-13 01:00:12,250 <== Total: 1 (BaseJdbcLogger.java:137)
false
2、更新
2.1、测试代码
@Test
public void yiji() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybats.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByPrimaryKey(1);
//更新
mapper.updateByPrimaryKey(new User(6, "dfgtb", "fsdfsf", null, null));
User user2 = mapper.selectByPrimaryKey(1);
System.out.println(user==user2);
}
2.2、结果
DEBUG 08-13 01:00:49,224 ==> Preparing: select id, user_name, pass_word, email, td_id from t_user where id = ? (BaseJdbcLogger.java:137)
DEBUG 08-13 01:00:49,287 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 08-13 01:00:49,338 <== Total: 1 (BaseJdbcLogger.java:137)
DEBUG 08-13 01:00:49,342 ==> Preparing: update t_user set user_name = ?, pass_word = ?, email = ?, td_id = ? where id = ? (BaseJdbcLogger.java:137)
DEBUG 08-13 01:00:49,346 ==> Parameters: dfgtb(String), fsdfsf(String), null, null, 6(Integer) (BaseJdbcLogger.java:137)
DEBUG 08-13 01:00:49,373 <== Updates: 1 (BaseJdbcLogger.java:137)
DEBUG 08-13 01:00:49,374 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 08-13 01:00:49,381 <== Total: 1 (BaseJdbcLogger.java:137)
false
3、配置flushCache=true
3.1、测试代码
mapper.xml中
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap" flushCache="true">
select id, user_name, pass_word, email, td_id
from t_user
where id = #{id,jdbcType=INTEGER}
</select>
@Test
public void yiji() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybats.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByPrimaryKey(1);
User user2 = mapper.selectByPrimaryKey(1);
System.out.println(user==user2);
}
3.2、结果
DEBUG 08-13 01:05:07,610 ==> Preparing: select id, user_name, pass_word, email, td_id from t_user where id = ? (BaseJdbcLogger.java:137)
DEBUG 08-13 01:05:07,656 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 08-13 01:05:07,694 <== Total: 1 (BaseJdbcLogger.java:137)
DEBUG 08-13 01:05:07,697 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 08-13 01:05:07,699 <== Total: 1 (BaseJdbcLogger.java:137)
false
4、设置作用域为statement
4.1、测试代码
mybatis配置文件中
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="cacheEnabled" value="true"/>
<setting name="defaultExecutorType" value="REUSE"/>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
@Test
public void yiji() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybats.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByPrimaryKey(1);
User user2 = mapper.selectByPrimaryKey(1);
System.out.println(user==user2);
}
4.2、结果
DEBUG 08-13 01:07:27,022 ==> Preparing: select id, user_name, pass_word, email, td_id from t_user where id = ? (BaseJdbcLogger.java:137)
DEBUG 08-13 01:07:27,075 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 08-13 01:07:27,111 <== Total: 1 (BaseJdbcLogger.java:137)
DEBUG 08-13 01:07:27,113 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 08-13 01:07:27,114 <== Total: 1 (BaseJdbcLogger.java:137)
false
查看源码中失效情况的位置
查看BaseExecutor源码
1.1更新的位置
1.2配置的
1.2.1queryStack
1.3配置statement
1.4手动清空
一级缓存性能
1、Session级别的一级缓存设计比较简单,只使用了HashMap来维护,并没有对HashMap的容量和大小进行限制。
2、 SqlSession的生存时间很短。使用一个SqlSession对象执行的操作不会太多,执行完就会消亡;
3、对于某一个SqlSession对象而言,只要执行commit操作(update、insert、delete),都会将这个SqlSession对象中对应的一级缓存清空掉,所以一般情况下不会出现缓存过大,影响JVM内存空间的问题;
作者:云芈山人
链接:https://www.jianshu.com/p/196c50e7f322
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
一级缓存执行的过程(源码)
执行流程
1.会话(DefaultSQLSession)内执行器(BaseExecutor)执行对应的查询、更新等
2.执行器对于查询,首先会查看一级缓存是否有对应的结果,否则就交给statementHandler进行数据库查询,保存到一级缓存中。(在执行更新时,方法内部会清空一级缓存。)
源码分析
1.DefaultSqlSession类中执行selectList方法
2.BaseExecutor类中执行query方法
3.看看update方法
spring-mybatis整合后一级缓存失效的分析
示例
1.示例
@Test
public void test(){
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("conf/spring.xml");
UserDaoMapper mapper = context.getBean(UserDaoMapper.class);
User user = mapper.getUser(1); //每次都会构造一个新的会话
User user2 =mapper.getUser(1);
System.out.println(user==user2);
}
结果:false
2.用事务包装后便可
@Test
public void test(){
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("conf/spring.xml");
UserDaoMapper mapper = context.getBean(UserDaoMapper.class);
//手动获取事务管理器
DataSourceTransactionManager manager =(DataSourceTransactionManager) context.getBean("dataSourceTransactionManager");
TransactionStatus transaction = manager.getTransaction(new DefaultTransactionDefinition());
User user = mapper.getUser(1); //每次都会构造一个新的会话
User user2 =mapper.getUser(1);
System.out.println(user==user2);
}
结果:true
查看源码
1.整合后访问数据库的流程(mybatis中我们可以发现默认是执行defaultSqlsession,里面是有Executor的属性,直接调用Executor,而整合后它会调用sqlSessionTemplate这个类,这里面使用了jdk的动态代理【可以查看->jdk动态代理】生成SqlsessionProxy类来间接执行defaultSqlSession中的方法。)
2.源码
public class SqlSessionTemplate implements SqlSession, DisposableBean {
private final SqlSessionFactory sqlSessionFactory;
private final ExecutorType executorType;
//SqlSession动态代理对象
private final SqlSession sqlSessionProxy;
private final PersistenceExceptionTranslator exceptionTranslator;
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType());
}
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) {
this(sqlSessionFactory, executorType, new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true));
}
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
Assert.notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
Assert.notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
//jdk动态代理的调用,我们可以看到下面的增删改查都是调用this.sqlSessionProxy的方法
/*
jdk的动态代理生成SqlsessionProxy类来间接执行defaultSqlSession中的方法
如果这里不使用动态代理的话,它需要再下面的每个方法都生成defaultSqlSession,然后调用其方法
所以这里使用动态代理代理当前这个类中的所有方法,通过InvocationHandler的invoke执行这些中的每个方法
*/
this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[]{SqlSession.class}, new SqlSessionTemplate.SqlSessionInterceptor());
}
public SqlSessionFactory getSqlSessionFactory() {
return this.sqlSessionFactory;
}
public ExecutorType getExecutorType() {
return this.executorType;
}
public PersistenceExceptionTranslator getPersistenceExceptionTranslator() {
return this.exceptionTranslator;
}
public <T> T selectOne(String statement) {
return this.sqlSessionProxy.selectOne(statement);
}
public <T> T selectOne(String statement, Object parameter) {
return this.sqlSessionProxy.selectOne(statement, parameter);
}
.........
private class SqlSessionInterceptor implements InvocationHandler {
private SqlSessionInterceptor() {
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//每次执行方法都会新获得一个SqlSession对象,这就是为什么一级缓存在整合中会失效
SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
Object unwrapped;
try {
//执行目标方法(当前类中的每个方法)
Object result = method.invoke(sqlSession, args);
if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
unwrapped = result;
} catch (Throwable var11) {
unwrapped = ExceptionUtil.unwrapThrowable(var11);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw (Throwable)unwrapped;
} finally {
if (sqlSession != null) {
SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
return unwrapped;
}
}
}
二级缓存
概要
二级缓存是应用级的缓存,即作用于整个应用的生命的周期。可以跨线程使用,相对一级缓存会有更高的命中率。所以在顺序上是先访问二级然后在是一级和数据库。
由于生命周期长,跨会话访问的因素所以二级在使用上要更谨慎,如果用的不好就会造成脏读。
二级缓存命中的条件
MyBatis一二级缓存的CacheKey是一至的,必须满足以条件才可以命中缓:
1.相同的statement id
2.相同的Sql与参数
3.返回行范围相同
缓存更新与关闭
二级缓存的数据默认是存在很久的,那怎么保证数据的一至性?有以下几种方式:
默认的update操作会清空该namespace下的缓存(可设定flushCache=false 来禁止)。
设定缓存的失效时间(默认一小时)。
将指定查询的缓存关闭即设置useCache=false。
为指定Statement设定 flushCache=true清空缓存
二级缓存划分
-
为每一个mapper分配一个缓存对象(对于每一个mapper.xml,在其中使用 节点)。
-
-
多个mapper共用一个缓存对象(使用节点,来指定你的这个Mapper使用到了哪一个Mapper的Cache缓存)。
二级缓存为什么必须提交后才能命中
1.示例:
开启二级缓存
@Test
public void twst() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybats.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//开启两个会话
SqlSession sqlSession = sessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByPrimaryKey(1);
System.out.println(user);
SqlSession sqlSession2 = sessionFactory.openSession();
UserMapper mapper2 = sqlSession.getMapper(UserMapper.class);
User user2 = mapper2.selectByPrimaryKey(1);
System.out.println(user2);
}
1.2.结果:
DEBUG 08-13 21:57:32,993 ==> Preparing: select id, user_name, pass_word, email, td_id from t_user where id = ? (BaseJdbcLogger.java:137)
DEBUG 08-13 21:57:33,027 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 08-13 21:57:33,060 <== Total: 1 (BaseJdbcLogger.java:137)
User{id=1, userName='cheyuan', passWord='123456', email='dafa@qq.com', tdId=1}
//命中为0,没有命中
DEBUG 08-13 21:57:33,080 Cache Hit Ratio [com.me.dao.UserMapper]: 0.0 (LoggingCache.java:60)
User{id=1, userName='cheyuan', passWord='123456', email='dafa@qq.com', tdId=1}
2.手动提交
@Test
public void twst() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybats.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByPrimaryKey(1);
System.out.println(user);
sqlSession.commit();
SqlSession sqlSession2 = sessionFactory.openSession();
UserMapper mapper2 = sqlSession.getMapper(UserMapper.class);
User user2 = mapper2.selectByPrimaryKey(1);
System.out.println(user2);
}
2.2结果:
DEBUG 08-13 21:59:20,729 ==> Preparing: select id, user_name, pass_word, email, td_id from t_user where id = ? (BaseJdbcLogger.java:137)
DEBUG 08-13 21:59:20,775 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:137)
DEBUG 08-13 21:59:20,823 <== Total: 1 (BaseJdbcLogger.java:137)
User{id=1, userName='cheyuan', passWord='123456', email='dafa@qq.com', tdId=1}
WARN 08-13 21:59:20,859 As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66 (SerialFilterChecker.java:46)
//命中 50%
DEBUG 08-13 21:59:20,864 Cache Hit Ratio [com.me.dao.UserMapper]: 0.5 (LoggingCache.java:60)
User{id=1, userName='cheyuan', passWord='123456', email='dafa@qq.com', tdId=1}
3.原因:
两个会话,第二次查询时,先执行修改,而此时二级缓存中还是上次保存的数据,并不是修改后的,这样机会造成脏读。这里的update只是临时清空的,只有提交后,这个操作才会到二级缓存,清空二级缓存。
如图所示,在增删改查操作都是暂存在暂存区的,暂存区和sqlsession生命周期是相同的,只有在提交后,才能将暂存区的内容取出,放入二级缓存中,所以只有在提交后,才会命中。
二级缓存的设计思想
1.实现
MyBatis 对于二级缓存的实现非常灵活,自己内部实现了Cache的一系列的实现类,并提供了各种缓存刷新策略LRU、FIFO等。同时它也允许自定义Cache接口实现,需实现org.apache.ibatis.cache.Cache接口,然后将Cache的实现类配置在节点的type属性上。此外,它也支持与第三方缓存库如Memecached的集成。
2.二级缓存采用了装饰器模式和责任链模式
2.1不同的功能由不同缓存装饰器实现
装饰器 | 描述 |
---|---|
SynchronizedCache | 同步锁,用于保证对指定缓存区的操作都是同步的 |
LoggingCache | 统计器,记录缓存命中率 |
BlockingCache | 阻塞器,基于key加锁,防止缓存穿透 |
ScheduledCache | 时效检查,用于验证缓存有效器,并清除无效数据 |
LruCache | 溢出算法,淘汰闲置最久的缓存。 |
FifoCache | 溢出算法,淘汰加入时间最久的缓存 |
WeakCache | 溢出算法,基于java弱引用规则淘汰缓存 |
SoftCache | 溢出算法,基于java软引用规则淘汰缓存 |
PerpetualCache | 实际存储,内部采用HashMap进行存储。 |
这些装饰器是如何组织起来的呢?查看源码可得知,每个装饰器都会通过属性引用下一个装饰器,从而组成一个链条。缓存逻辑基于链条进行传递。
二级缓存的执行流程
执行流程
源码
CachingExecutor中