可以理解为存储在内存中的数据,mybatis用在对数据库交互后产生结果的一个存储,避免与数据库的频繁交互,mybatis中的缓存分为一级缓存和二级缓存
实例:
InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询 首先在sqlSession的HashMap中查找是否存在 当前查询的key值
// 如果存在 返回, 我们这是第一次查看所以肯定不会存在,
// 发送sql语句 然后讲查询结果以key + value 的形式存储到当前sqlSession的缓存中
User user1 = userMapper.findUserById(1L);
System.out.println("user1 = " + user1);
// 第二次查询 同理在缓存中查找是否存在当前查询的key值, 目前我们知道sqlSession一直
// 我们的sql以及请求参数也一置,直接在缓存中读取,不需要与数据库交互
User user2 = userMapper.findUserById(1L);
System.out.println("user2 = " + user2);
// 这里我们看到 两次的查询结果同时指向了一个地址 ,那么也就说明第二次查询到的就是第一次查询结果的实例
System.out.println(user1 == user2);
控制台结果打印
// 这里表示与sql交互的过程
14:17:31,399 DEBUG findUserById:159 - ==> Preparing: select user_id userId,user_name userName from t_user where user_id = ?
14:17:31,429 DEBUG findUserById:159 - ==> Parameters: 1(Long)
14:17:31,453 DEBUG findUserById:159 - <== Total: 1
user1 = User{
userId=1, userName='彭于晏1111', userPhone='null', merits=[], movies=[]}
// 这里并没有sql交互 而是直接在缓存中拿到了查询结果
user2 = User{
userId=1, userName='彭于晏1111', userPhone='null', merits=[], movies=[]}
// 两个user的地址相同
true
产生疑问,如果我们在第一次查询之后更改了数据,那第二次查询是否还会在缓存中读取吗
两次查询中加入update方法之后
// 第一次查询的sql交互
14:32:01,779 DEBUG findUserById:159 - ==> Preparing: select user_id userId,user_name userName from t_user where user_id = ?
14:32:01,821 DEBUG findUserById:159 - ==> Parameters: 1(Long)
14:32:01,844 DEBUG findUserById:159 - <== Total: 1
// 第一次查询的结果
user1 = User{
userId=1, userName='彭于晏1111', userPhone='null', merits=[], movies=[]}
// 调用修改方法之后commit
14:32:01,845 DEBUG updateUser:159 - ==> Preparing: update t_user set user_name=? where user_id = ?
14:32:01,846 DEBUG updateUser:159 - ==> Parameters: 彭于晏(String), 1(Long)
14:32:01,846 DEBUG updateUser:159 - <== Updates: 1
14:32:01,847 DEBUG JdbcTransaction:70 - Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@48503868]
// 第二次查询的sql交互
14:32:01,859 DEBUG findUserById:159 - ==> Preparing: select user_id userId,user_name userName from t_user where user_id = ?
14:32:01,859 DEBUG findUserById:159 - ==> Parameters: 1(Long)
14:32:01,860 DEBUG findUserById:159 - <== Total: 1
// 第二次的查询结果
user2 = User{
userId=1, userName='彭于晏', userPhone='null', merits=[], movies=[]}
false
由此我们可知道,修改数据的过程中一定清空了缓存数据
通过查看源码我们可知
在调用提交commit方法时清空了缓存,具体代码如下
// sqlSession.commit()方法在其实现类DefaultSqlSession中具体实现如下
public void commit(boolean force) {
try {
this.executor.commit(this.isCommitOrRollbackRequired(force));
this.dirty = false;
} catch (Exception var6) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + var6, var6);
} finally {
ErrorContext.instance().reset();
}
}
// 能够看出 在实现中调用了执行器executor的commit()方法 在其实现类BaseExecutor中被实现
public void commit(boolean required) throws SQLException {
if (this.closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
} else {
this.clearLocalCache();
this.flushStatements();
if (required) {
this.transaction.commit();
}
}
}
public void clearLocalCache() {
if (!this.closed) {
this.localCache.clear();
this.localOutputParameterCache.clear();
}
}
// 这里使用了PerpetualCache的clear()方法
private Map<Object, Object> cache = new HashMap();
public void clear() {
this.cache.clear();
}
// 能够看到最终这个PerpetualCache的clear方法清空了HashMap的值
看到这里我们是不是就可以说mybatis的一级缓存就是一个HashMap
接下来我们查看一下这个
HashMap是怎样组成的,又是怎样被创建的
我们通过上一篇的自定义持久层框架中知道我们的sql是在executor的query中执行的
如果我们想要将查询结果存放到hashMap中那么一定要先获得查询结果,我们直接看query的源代码
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 这里boundSql就是我们的sql
BoundSql boundSql = ms.getBoundSql(parameter);
// createCacheKey这个方法字面意思就是创建一个缓存key
CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
很明显了 createCacheKey这个方法一定创建了我们HashMap中的key,那么这个key具体是什么
// 首先先看参数
//MappedStatement mapper.xml容器对象
//parameterObject 参数
//rowBounds 分页对象
//boundSql sql容器
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
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();
Iterator var8 = parameterMappings.iterator();
while(var8.hasNext()) {
ParameterMapping parameterMapping = (ParameterMapping)var8.next();
if (parameterMapping.getMode() != ParameterMode.OUT) {
String propertyName = parameterMapping.getProperty();
Object value;
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 = this.configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (this.configuration.getEnvironment() != null) {
cacheKey.update(this.configuration.getEnvironment().getId());
}
return cacheKey;
}
}
通过以上代码我们可知 这个key的组成就是
statementId 、params、sql、rowBounds
回到query方法 能够看到这个生成好的key被带入了另一个query方法
CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
我们截取这个query的部分代码
// sql在执行之前我们 这里先在内存中读取是否存在当前key
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
// 当我们的list为null时也就是说当前内存中没有我们的缓存,否则直接返回
if (list != null) {
this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 这里也就是完成了我们的sql操作 并把结果封装到 key的Map中
list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
附上queryFromDatabase的部分代码
try {
// 数据库交互结果返回一个List结果集
list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 将缓存中value为null 的当前key清除
this.localCache.removeObject(key);
}
// 将此次查询结果 以及我们的cacheKey 放入我们的缓存中 最终返回我们的结果list
this.localCache.putObject(key, list);
一级缓存到这里就介绍结束了
⼆级缓存的原理和⼀级缓存原理⼀样,每次查询都去缓存中查找有就返回没有就sql交互然后存储。但是⼀级缓存是基于sqlSession的,⽽⼆级缓存是基于mapper⽂件的namespace的,也 就
是说多个sqlSession可以共享⼀个mapper中的⼆级缓存区域
白话的意思就是,多个sqlSession公用一个map
mybatis二级缓存需要我们手动打开,在核心配置文件下加入如下代码开启二级缓存
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
还需要在mapper.xml中开启缓存 加入
注解开发使用@CacheNamespace
测试
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
User userById1 = userMapper1.findUserById(1L);
User userById2 = userMapper2.findUserById(1L);
System.out.println(userById2 == userById1);
控制台打印
21:03:43,775 DEBUG findUserById:159 - ==> Preparing: select user_id userId,user_name userName from t_user where user_id = ?
21:03:43,810 DEBUG findUserById:159 - ==> Parameters: 1(Long)
21:03:43,833 DEBUG findUserById:159 - <== Total: 1
21:03:43,834 DEBUG UserMapper:62 - Cache Hit Ratio [com.hen84.mapper.UserMapper]: 0.0
21:03:43,835 DEBUG JdbcTransaction:137 - Opening JDBC Connection
21:03:43,852 DEBUG PooledDataSource:406 - Created connection 1025309396.
21:03:43,852 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3d1cfad4]
21:03:43,853 DEBUG findUserById:159 - ==> Preparing: select user_id userId,user_name userName from t_user where user_id = ?
21:03:43,853 DEBUG findUserById:159 - ==> Parameters: 1(Long)
21:03:43,855 DEBUG findUserById:159 - <== Total: 1
这里我们看到 竟然完成了两次sql交互,人傻了,配置什么的都对,然后求助学习群里的大佬我得知
当会话提交或关闭之后才会填充二级缓存
也就是当sqlSession1查询到结果之后必须要commit一下或者close一下才会将结果填充到二级缓存,
测试2
User userById1 = userMapper1.findUserById(1L);
sqlSession1.commit();
User userById2 = userMapper2.findUserById(1L);
System.out.println(userById2 == userById1);
控制台打印
21:14:14,871 DEBUG UserMapper:62 - Cache Hit Ratio [com.hen84.mapper.UserMapper]: 0.0
21:14:14,877 DEBUG JdbcTransaction:137 - Opening JDBC Connection
21:14:15,105 DEBUG PooledDataSource:406 - Created connection 32863545.
21:14:15,105 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f57539]
21:14:15,110 DEBUG findUserById:159 - ==> Preparing: select user_id userId,user_name userName from t_user where user_id = ?
21:14:15,148 DEBUG findUserById:159 - ==> Parameters: 1(Long)
21:14:15,166 DEBUG findUserById:159 - <== Total: 1
21:14:15,234 DEBUG UserMapper:62 - Cache Hit Ratio [com.hen84.mapper.UserMapper]: 0.5
false
OK这次就对了
我们还看到这两个user的地址并不相等
这是因为第二次查询拿到的并不是第一次查询的实例
而是通过反序列化拿到的第一次查询的数据
那么我的mybatis的二级缓存有没有弊端呢,下面考虑分布式的情况
两台服务器,我们这里起名服务器1和服务器2
假设用户第一次请求访问的是服务器1那么数据就被缓存到了服务器1的主机上
第二次请求访问的是服务器2,那么就拿不到缓存数据,还得完成一次sql交互
解决办法,使用redis实现二级缓存
让我们的第一次请求的数据储存到redis中,第二次请求去redis中获取
我们也不需要手动去写这个实现步骤,mybais已经为我们准备好了一个包
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
导入之后再根目录下加入redis的配置文件 名称必须为redis.properties
redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0
那么怎么使用呢
我们这边用的注解开发 所以需要在UserMapper顶部加入注解
@CacheNamespace(blocking = true,implementation = RedisCache.class)
由此我们知道redis实现二级缓存是基于RedisCache这个类去实现的
控制台结果
22:19:06,591 DEBUG UserMapper:62 - Cache Hit Ratio [com.hen84.mapper.UserMapper]: 0.0
22:19:06,596 DEBUG JdbcTransaction:137 - Opening JDBC Connection
22:19:06,849 DEBUG PooledDataSource:406 - Created connection 520232556.
22:19:06,849 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1f021e6c]
22:19:06,853 DEBUG findUserById:159 - ==> Preparing: select user_id userId,user_name userName from t_user where user_id = ?
22:19:06,891 DEBUG findUserById:159 - ==> Parameters: 1(Long)
22:19:06,913 DEBUG findUserById:159 - <== Total: 1
22:19:06,978 DEBUG UserMapper:62 - Cache Hit Ratio [com.hen84.mapper.UserMapper]: 0.5
false
redis库内容
127.0.0.1:6379> keys *
1) "com.hen84.mapper.UserMapper"
ok到此mybatis的缓存学习就先告一段落了
1、mybatis缓存储存机制以及刷新机制
2、redis+二级缓存可以解决分布式数据缓存