缓存是指内存中的数据,常常来自于对数据库查询结果的缓存,为什么需要缓存?因为频繁的直接对数据库操作是很消耗系统资源的。我们将频繁需要获取的数据进行缓存,可以避免频繁的数据库交互,可以提高系统的响应速度。
mybatis对缓存的支持有一级缓存和二级缓存:
- 一级缓存是SqlSession级别,也就是在操作数据库的时候要构造SqlSeesion对象,对象中有一个数据结构(HashMap)用于存储数据,不同的SqlSession之间的缓存区域是互不影响的。
- 二级缓存为mapper级别缓存,多个SqlSession去操作同一个Mapper的sql语句,这个时候多个SqlSession是共享二级缓存的,二级缓存是跨SqlSession的。
先附上代码链接:https://gitee.com/wzlove521/lagou_study/tree/master/mybatis/mybatis_multitable
一级缓存
一级缓存的测试以及缓存失效,首先要确认一级缓存的存在,我们先配置mybatis的打印sql,之后进行查看结果会更清晰明了。在config的配置文件中添加
<settings><setting name="logImpl" value="STDOUT_LOGGING"/></settings>
编写测试类进行测试:
public class CacheTest {private UserMapper userMapper;private SqlSession sqlSession;@Beforepublic void createUserMapper() throws IOException {InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapperConfig.xml");sqlSession = new SqlSessionFactoryBuilder().build(resourceAsStream).openSession();userMapper = sqlSession.getMapper(UserMapper.class);}@Testpublic void testFirstLevelCache() {User user = userMapper.selectById(2);User user1 = userMapper.selectById(2);System.out.println(user == user1);}}
debug测试可以看到在 testFirstLevelCache 中,执行第一个查询的时候会打开连接,然后查询数据库;第二次查询的时候是直接返回结果的,并且最后的输出为true。
缓存的生成就是这么一个过程,第一次查询,会去一级缓存中查询有没有缓存,没有去数据库查询,将查询结果进行缓存;如果存在缓存,则直接返回。
缓存失效
缓存失效或者说手动清除缓存,手动清除缓存直接使用 sqlSession.cleanCache(); 手动清除缓存。那什么情况下会自动清除缓存呢,那就是出现了增删改操作并进行了事务提交。如下
@Testpublic void testFirstLevelCacheClean() {User user = userMapper.selectById(2);// sqlSession.clearCache();User user2 = new User();user.setId(1);user.setName("王智");userMapper.updateUser(user2);sqlSession.commit();User user1 = userMapper.selectById(2);System.out.println(user == user1);}
可以看到只要有增删改操作就会清除一级缓存,无论你的操作是否影响了当前缓存的数据,只要发生,就会清除,这样做的目的是避免脏读。
额外说一点,SqlSession中保存一级缓存使用的数据结构是HashMap,存在的键值对的值肯定是查询的结果,那键呢?键的组成比较复杂,包括下面几个部分:
- statementId:唯一标识:namespace + id
- params:参数
- boundSql:封装了当前执行的sql
- rowBounds:分页对象
源码分析
一级缓存是基于SqlSession的,所以源码的分析也从SqlSession开始。
这就是源码查找的整个流程图,下面展示一下比较重要大代码:
// 首先SqlSeesion的接口就不用看了,从实现类的代码看起public class DefaultSqlSession implements SqlSession {private final Executor executor;@Overridepublic void clearCache() {executor.clearLocalCache();}}// Executor是执行器,实现类看BaseExecutor,该类实现了clearLocalCache方法public abstract class BaseExecutor implements Executor {protected PerpetualCache localCache;@Overridepublic void clearLocalCache() {if (!closed) {localCache.clear();localOutputParameterCache.clear();}}}// PerpetualCache就是最后一层了,可以看到具体的数据结构public class PerpetualCache implements Cache {private final String id;private final Map<Object, Object> cache = new HashMap<>();@Overridepublic void clear() {// 执行的就是HashMap的清空方法cache.clear();}}
根据clean方法找到了整个线路,那具体的缓存的实现在哪一步呢?我们在执行查询的时候才会放缓存,所以缓存的实现肯定是放在执行器的步骤,接下来肯定看执行器的查询方法:
// 查看query方法public abstract class BaseExecutor implements Executor {@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameter);CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);return query(ms, parameter, rowBounds, resultHandler, key, boundSql);}}// 看到生成了CacheKey,接下来看看CacheKey是如何生成的,包含了哪几个部分@Overridepublic CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {if (closed) {throw new ExecutorException("Executor was closed.");}CacheKey cacheKey = new CacheKey();// statementId = namespace.idcacheKey.update(ms.getId());// 分页数据cacheKey.update(rowBounds.getOffset());cacheKey.update(rowBounds.getLimit());// 执行的sqlcacheKey.update(boundSql.getSql());// 存放参数List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();// mimic DefaultParameterHandler logicfor (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 当前使用的数据库的idcacheKey.update(configuration.getEnvironment().getId());}return cacheKey;}// 最后再看query方法@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());if (closed) {throw new ExecutorException("Executor was closed.");}if (queryStack == 0 && ms.isFlushCacheRequired()) {clearLocalCache();}List<E> list;try {queryStack++;list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}if (queryStack == 0) {for (DeferredLoad deferredLoad : deferredLoads) {deferredLoad.load();}// issue #601deferredLoads.clear();if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482clearLocalCache();}}return list;}// query方法的操作就很熟悉,先查询缓存,缓存为空,查询数据库,在 queryFromDatabase 方法中查询库并进行了返回。
以上就是缓存的使用,至于清除缓存我们可以看到commit方法中调用了 clearLocalCache() 方法,所以只要进行了增删改触发了事务的提交,那就一定会清空缓存。
二级缓存
二级缓存是跨sqlSession的,是基于mapper接口的,准确的来说是基于namespace的,只要namespace相同,就共用一个二级缓存。二级缓存需要手动开启,开启的方式根据注解开发和xml开发是不同的。
注解开发,需要在接口上添加注解: @CacheNamespace
xml开发,需要在xml文件中添加标签cache:
其次,需要在配置文件Configuration中添加setting:
<settings><setting name="cacheEnabled" value="true"/></settings>
成功开启二级缓存之后,就可以正常使用。原理同一级缓存相同,首先查询缓存,没有命中,查库,保存到缓存。
一级缓存和二级缓存的执行顺序
对于这里只能说我自己验证的结果,如果存在误差还请和我一起交流。
经过验证,实际的执行顺序是:二级缓存 - 一级缓存 - 数据库。操作流程是先查询二级缓存,没有查询到结果,会查询一级缓存,没有查询到结果会查询数据库。从数据库中查询到结果会放到一级缓存,二级缓存中是没有的,只有当前的sqlSession关闭或者commit之后,才会放到二级缓存,并且当数据存在在二级缓存中时,cleanCache是无法清空二级缓存的。
这里需要注意,也正是我开始了解缓存的时候所疑惑的地方。验证代码如下:
@Testpublic void testSecondLevelCache() {SqlSession sqlSession1 = build.openSession();UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);User user = mapper1.findById(2);// sqlSession1不关闭,sqlSession2无法命中缓存,sqlSession1.close();SqlSession sqlSession2 = build.openSession();UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);User user1 = mapper2.findById(2);System.out.println(user == user1);// sqlSession2清空缓存只能清空自己的一级缓存sqlSession2.clearCache();SqlSession sqlSession3 = build.openSession();UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);User user2 = mapper3.findById(2);}
既然cleanCache无法清除二级缓存,那怎样清除呢?两个办法
- 配置入手,flushCache设置为true
- 进行增删改操作并进行了commit
测试
需要注意的是在进行二级缓存操作的时候,需要是pojo类实现Serializable接口,因为缓存可能存在在硬盘上。
准备工作做好之后编写测试类: ```java @Test public void testSecondLevelCache() { SqlSession sqlSession1 = build.openSession(); UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class); User user = mapper1.findById(2); // sqlSession1不关闭,sqlSession2无法命中缓存,那一级缓存和二级缓存的顺序是咋样的呢?是不是意味着一级缓存在关闭的时候会将数据放入二级缓存呢? // 研究一下 sqlSession1.close(); SqlSession sqlSession2 = build.openSession(); UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class); User user1 = mapper2.findById(2); System.out.println(user == user1); }
// 需要说明的是,运行完成之后可以看到 Cache Hit Ratio [com.wangzhi.mapper.UserMapper]: 0.5,说明二级缓存生效。 // 但是会发现最后的输出结果是false,也就说明了二级缓存存入的是数据,而不是对象,这也是为什么pojo要实现Serializable。
至于缓存失效也会有所不同,使用clearCache方法并不能清除二级缓存,需要SqlSession进行了增删改并提交了事务,或者通过配置 flushCache = true。<a name="EaYDj"></a>#### 其他配置useCache和flushCache,这两个配置是用来给单独的方法或者标签使用的。```java// xml中使用<select id="selectById" useCache="true" flushCache="true" resultMap="userMap" parameterType="int">SELECTuser.id AS userId,user.name AS userName,orders.id AS orderId,orders.name AS orderName,orders.user_id AS orderUserId,orders.create_time AS orderCreateTimeFROM user LEFT JOIN orders ON user.id = orders.user_id WHERE user.id = #{id}</select>// 注解使用@Options(useCache = true, flushCache = Options.FlushCachePolicy.DEFAULT)@Select("SELECT id, name FROM user WHERE id = #{id} ")User findById(int id);
useCache = true表示是否从缓存中读取? true为是,false会直接查询数据库,跳过一级缓存和二级缓存。
flushCache为是否刷新缓存,一定要设置为true,不能设置为false,不然会出现脏读。注解使用的时候flushCache有三个可选值
- DEFAULT:默认,在进行查询的时候不会刷新缓存,进行增删改操作会刷新缓存。
- TRUE:进行任何操作都会户刷新缓存
- FALSE:不会刷新缓存
redis实现二级缓存
既然mybatis本身支持二级缓存,为什么还需要使用redis来进行实现呢?原因就在于分布式环境,mybatis自身的二级缓存并不能支持分布式环境,所以就需要使用支持分布式环境的数据库redis或者其它实现。mybatis提供了redis的实现,我们只需要使用对应的jar包就好。
引入依赖:
<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-redis --><dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-redis</artifactId><version>1.0.0-beta2</version></dependency>// 确认实现类,接口上注解实现@CacheNamespace(implementation = RedisCache.class)// xml标签实现<cache type="org.mybatis.caches.redis.RedisCache"/>
源码查看
// 首先,如果不进行redis的配置文件的编写会默认连接本地的redis,默认的配置如下:public final class Protocol {private static final String ASK_RESPONSE = "ASK";private static final String MOVED_RESPONSE = "MOVED";private static final String CLUSTERDOWN_RESPONSE = "CLUSTERDOWN";public static final String DEFAULT_HOST = "localhost";public static final int DEFAULT_PORT = 6379;public static final int DEFAULT_SENTINEL_PORT = 26379;public static final int DEFAULT_TIMEOUT = 2000;public static final int DEFAULT_DATABASE = 0;......}// 其次,如果需要配置远端redis,就需要配置文件,为什么配置文件的名字一定要是redis.properties呢final class RedisConfigurationBuilder {/*** This class instance.*/private static final RedisConfigurationBuilder INSTANCE = new RedisConfigurationBuilder();private static final String SYSTEM_PROPERTY_REDIS_PROPERTIES_FILENAME = "redis.properties.filename";private static final String REDIS_RESOURCE = "redis.properties";......}// 接下来就看具体的实现public final class RedisCache implements Cache {// 读写锁private final ReadWriteLock readWriteLock = new DummyReadWriteLock();// hset表示使用的数据结构是哈希@Overridepublic void putObject(final Object key, final Object value) {execute(new RedisCallback() {@Overridepublic Object doWithRedis(Jedis jedis) {jedis.hset(id.toString().getBytes(), key.toString().getBytes(), SerializeUtil.serialize(value));return null;}});}@Overridepublic Object getObject(final Object key) {return execute(new RedisCallback() {@Overridepublic Object doWithRedis(Jedis jedis) {return SerializeUtil.unserialize(jedis.hget(id.toString().getBytes(), key.toString().getBytes()));}});}}
基本上实现就是这样,如果需要自定义实现缓存,操作的流程就是实现Cache接口,完成实现方法的编写就好。
