-
2022-04-03 20:36:19
目录
为什么要用缓存
在计算机的世界中,缓存无处不在,操作系统有操作系统的缓存,数据库也会有数据库的缓存,各种中间件如Redis也是用来充当缓存的作用,编程语言中又可以利用内存来作为缓存。 MyBatis作为一款优秀的ORM框架,也用到了缓存,本文的目的就是探究一下MyBatis的缓存是如何实现的。尤其是I/O操作,除了那种CPU密集型的系统,其余大部分的业务系统性能瓶颈最后或多或少都会出现在I/O操作上,所以为了减少磁盘的I/O次数,缓存是必不可少的,通过缓存的使用我们可以大大减少I/O操作次数,从而在一定程度上弥补了I/O操作和CPU处理速度之间的鸿沟。在ORM框架中引入缓存的目的就是为了减少读取数据库的次数,从而提升查询的效率。
一级缓存
前面了解了SqlSessionFactory、SqlSession、Excutor以及Mpper执行SQL过程,下面来了解下myabtis的缓存,它的缓存分为一级缓存和二级缓存。 使用MyBatis开启一次和数据库的会话,MyBatis会创建出一个SqlSession对象表示一次数据库会话,在对数据库的一次会话中, 有可能会反复地执行完全相同的查询语句,每一次查询都会去查一次数据库,为了减少资源浪费,mybaits提供了一种缓存的方式(一级缓存)。 mybatis的SQL执行最后是交给了Executor执行器来完成的,看下BaseExecutor类的源码:
@Override public <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); } @SuppressWarnings("unchecked") @Override public <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;//localCache 本地缓存 if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); //如果缓存没有就走DB } } finally { queryStack--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } // issue #601 deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; } private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key);//清空现有缓存数据 } localCache.putObject(key, list);//新的结果集存入缓存 if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
它的本地缓存使用的是PerpetualCache类,内部是一个HashMap作了一个封装来存数据。缓存Key的生成:
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); @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(Integer.valueOf(rowBounds.getOffset())); cacheKey.update(Integer.valueOf(rowBounds.getLimit())); cacheKey.update(boundSql.getSql()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); // mimic DefaultParameterHandler logic for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); 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; }
通过statementId,params,rowBounds,BoundSql来构建一个key值,根据这个key值去缓存Cache中取出对应缓存结果。
一级缓存的生命周期
比如要执行一个查询操作时,Mybatis会创建一个新的SqlSession对象,SqlSession对象找到具体的Executor, Executor持有一个PerpetualCache对象;当查询结束(会话结束)时,SqlSession、Executor、PerpetualCache对象占有的资源一并释放掉。
如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用。
一级缓存失效情况
注意:一级缓存为sqlSession级别的缓存,默认开启的,不能关闭。一级缓存失效的四种情况: 1)sqlSession不同,缓存失效。 2)sqlSession相同,查询条件不同,缓存失效,因为缓存中可能还没有相关数据。 3)sqlSession相同,在两次查询期间,执行了增删改操作,缓存失效。 4)sqlSession相同,但是手动清空了一级缓存,缓存失效。 清除缓存情况: 1、就是获取缓存之前会先进行判断用户是否配置了flushCache=true属性(参考一级缓存的创建代码截图), 如果配置了则会清除一级缓存。 2、MyBatis全局配置属性localCacheScope配置为Statement时,那么完成一次查询就会清除缓存。 3、在执行commit,rollback,update方法时会清空一级缓存
二级缓存
Mybatis默认对二级缓存是关闭的,一级缓存默认开启,如果需要开启只需在mapper上加入配置就好了。Executor是执行查询的最终接口,它有两个实现类一个是BaseExecutor另外一个是CachingExecutor。CachingExecutor(二级缓存查询),一级缓存因为只能在同一个SqlSession中共享,所以会存在一个问题,在分布式或者多线程的环境下,不同会话之间对于相同的数据可能会产生不同的结果,因为跨会话修改了数据是不能互相感知的,所以就有可能存在脏数据的问题,正因为一级缓存存在这种不足,需要一种作用域更大的缓存,这就是二级缓存。
CachingExecutor实现类里面的query查询方法:@Override ublic List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache();//二级缓存对象 if (cache != null) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, parameterObject, boundSql); @SuppressWarnings("unchecked") List list = (List) tcm.getObject(cache, key);//从缓存中读取 if (list == null) { //这段走到一级缓存或者DB list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 //数据放入缓存 } return list; } } return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
一个事务方法运行时,数据查询出来,缓存在一级缓存了,但是没有到二级缓存,当事务提交后(sqlSession.commit()),数据才放到二级缓存。查询的顺序是,先查二级缓存再查一级缓存然后才去数据库查询。
一级缓存作用域是SqlSession级别,所以它存储的SqlSession中的BaseExecutor之中,但是二级缓存目的要实现作用范围更广,所以要实现跨会话共享,MyBatis二级缓存的作用域是namespace,专门用了一个装饰器来维护,这就是:CachingExecutor。
二级缓存相关的配置有三个地方: 1、mybatis-config中有一个全局配置属性,这个不配置也行,因为默认就是true。 <setting name="cacheEnabled" value="true"/> 想详细了解mybatis-config的可以点击这里。 2、在Mapper映射文件内需要配置缓存标签: <cache/> 或 <cache-ref namespace="com.lonelyWolf.mybatis.mapper.UserAddressMapper"/> 想详细了解Mapper映射的所有标签属性配置可以点击这里。 3、在select查询语句标签上配置useCache属性,如下: <select id="selectUserAndJob" resultMap="JobResultMap2" useCache="true"> select * from lw_user </select> 以上配置第1点是默认开启的,也就是说我们只要配置第2点就可以打开二级缓存了, 而第3点是当我们需要针对某一条语句来配置二级缓存时候则可以使用。 1、需要commit事务之后才会生效 2、如果使用的是默认缓存,那么结果集对象需要实现序列化接口(Serializable) 如果不实现序列化接口则会报如下错误 * 二级缓存工作机制(?) 1)一个会话,查询一条数据,该数据会放在当前会话的一级缓存中。 2)如果当前会话关闭,对应的一级缓存会被保存到二级缓存中,新的会话查询信息,就可以参照二级缓存。 3)不同namespace查询出的数据会放在自己对应的缓存中。 注意:查出的数据都会默认放在一级缓存中,只有会话提交或关闭后,一级缓存的数据才会被转移到二级缓存中。 * 需要注意的是在事务提交之前,并不会真正存储到二级缓存,而是先存储到一个临时属性, * 等事务提交之后才会真正存储到二级缓存。(?) Mybatis缓存包装汇总: PerpetualCache 缓存默认实现类 - 基本功能,默认携带 LruCache LRU淘汰策略缓存(默认淘汰策略) 当缓存达到上限,删除最近最少使用缓存 eviction=“LRU” FifoCache FIFO淘汰策略缓存 当缓存达到上限,删除最先入队的缓存 eviction=“FIFO” SoftCache JVM软引用淘汰策略缓存 基于JVM的SoftReference对象 eviction=“SOFT” WeakCache JVM弱引用淘汰策略缓存 基于JVM的WeakReference对象 eviction=“WEAK” LoggingCache 带日志功能缓存 输出缓存相关日志信息 基本功能,默认包装 SynchronizedCache 同步缓存 基于synchronized关键字实现,用来解决并发问题 基本功能,默认包装 BlockingCache 阻塞缓存 get/put操作时会加锁,防止并发,基于Java重入锁实现 blocking=true SerializedCache 支持序列化的缓存 通过序列化和反序列化来存储和读取缓存 readOnly=false(默认) ScheduledCache 定时调度缓存 操作缓存时如果缓存已经达到了设置的最长缓存时间时会移除缓存 flushInterval属性不为空 TransactionalCache 事务缓存 在TransactionalCacheManager中用于维护缓存map的value值
二级缓存失效
所有的update操作(insert,delete,uptede)都会触发缓存的刷新,从而导致二级缓存失效,所以二级缓存适合在读多写少的场景中开启。
二级缓存针对的是同一个namespace,所以建议是在单表操作的Mapper中使用,或者是在相关表的Mapper文件中共享同一个缓存。
自定义缓存
一级缓存可能存在脏读情况,那么二级缓存是否也可能存在呢?是的,默认的二级缓存也是存储在本地缓存,对于微服务下是可能出现脏读的情况的,这时可能会需要自定义缓存,比如利用redis来存储缓存,而不是存储在本地内存当中。
MyBatis官方也提供了第三方缓存的支持引入pom文件: <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-redis</artifactId> <version>1.0.0-beta2</version> </dependency> 然后缓存配置如下: <cache type="org.mybatis.caches.redis.RedisCache"></cache> 然后在默认的resource路径下新建一个redis.properties文件: host=localhost port=6379
延迟加载原理
延迟加载原理:调用的时候触发加载,不是在初始化的时候就加载信息。MyBatis支持延迟加载,设置lazyLoadingEnabled=true即可。
比如:a.getB().getName(),发现a.getB()的值为null,此时会单独触发事件,将保存好的关联B对象的SQL查询出来, 然后再调用a.setB(b),这时再调用a.getB().getName()就有值了。
Cache组件
MyBatis 中缓存模块相关的代码位于 org.apache.ibatis.cache 包 下,其中 Cache 接口 是缓存模块中最核心的接口,它定义了所有缓存的基本行为。
public interface Cache { /** * 获取当前缓存的 Id */ String getId(); /** * 存入缓存的 key 和 value,key 一般为 CacheKey对象 */ void putObject(Object key, Object value); /** * 根据 key 获取缓存值 */ Object getObject(Object key); /** * 删除指定的缓存项 */ Object removeObject(Object key); /** * 清空缓存 */ void clear(); /** * 获取缓存的大小 */ int getSize(); /** * !!!!!!!!!!!!!!!!!!!!!!!!!! * 获取读写锁,可以看到,这个接口方法提供了默认的实现!! * 这是 Java8 的新特性!!只是平时开发时很少用到!!! * !!!!!!!!!!!!!!!!!!!!!!!!!! */ default ReadWriteLock getReadWriteLock() { return null; } }
Cache接口的实现类有很多,大部分都是装饰器,只有PerpetualCache 提供了 Cache 接口 的基本实现
PerpetualCache
PerpetualCache(Perpetual:永恒的,持续的)在缓存模块中扮演着被装饰的角色,其实现比较简单,底层使用 HashMap 记录缓存项,也是通过该 HashMap 对象 的方法实现的 Cache 接口 中定义的相应方法。
public class PerpetualCache implements Cache { // Cache对象 的唯一标识 private final String id; // 其所有的缓存功能实现,都是基于 JDK 的 HashMap 提供的方法 private Map<Object, Object> cache = new HashMap<>(); public PerpetualCache(String id) { this.id = id; } @Override public String getId() { return id; } @Override public int getSize() { return cache.size(); } @Override public void putObject(Object key, Object value) { cache.put(key, value); } @Override public Object getObject(Object key) { return cache.get(key); } @Override public Object removeObject(Object key) { return cache.remove(key); } @Override public void clear() { cache.clear(); } /** * 其重写了 Object 中的 equals() 和 hashCode()方法,两者都只关心 id字段 */ @Override public boolean equals(Object o) { if (getId() == null) { throw new CacheException("Cache instances require an ID."); } if (this == o) { return true; } if (!(o instanceof Cache)) { return false; } Cache otherCache = (Cache) o; return getId().equals(otherCache.getId()); } @Override public int hashCode() { if (getId() == null) { throw new CacheException("Cache instances require an ID."); } return getId().hashCode(); } }
cache.decorators包下提供的装饰器,它们都直接实现了Cache接口,扮演着装饰器的角色。这些装饰器会在 PerpetualCache 的基础上提供一些额外的功能,通过多个组合后满足一个特定的需求。
BlockingCache
BlockingCache 是阻塞版本的缓存装饰器,它会保证只有一个线程到数据库中查找指定 key 对应的数据
public class BlockingCache implements Cache { // 阻塞超时时长 private long timeout; // 持有的被装饰者 private final Cache delegate; // 每个 key 都有其对应的 ReentrantLock锁对象 private final ConcurrentHashMap<Object, ReentrantLock> locks; // 初始化 持有的持有的被装饰者 和 锁集合 public BlockingCache(Cache delegate) { this.delegate = delegate; this.locks = new ConcurrentHashMap<>(); } }
假设线程A在BlockingCache中未查找到 keyA 对应的缓存项时,线程 A 会获取 keyA 对应的锁,这样,线程 A 在后续查找 keyA 时,其它线程会被阻塞。
// 根据 key 获取锁对象,然后上锁 private void acquireLock(Object key) { // 获取 key 对应的锁对象 Lock lock = getLockForKey(key); // 获取锁,带超时时长 if (timeout > 0) { try { boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS); if (!acquired) { // 超时,则抛出异常 throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId()); } } catch (InterruptedException e) { // 如果获取锁失败,则阻塞一段时间 throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e); } } else { // 上锁 lock.lock(); } } private ReentrantLock getLockForKey(Object key) { // Java8 新特性,Map系列类 中新增的方法 // V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) // 表示,若 key 对应的 value 为空,则将第二个参数的返回值存入该 Map集合 并返回 return locks.computeIfAbsent(key, k -> new ReentrantLock()); }
假设 线程 A 从数据库中查找到 keyA 对应的结果对象后,将结果对象放入到 BlockingCache 中,此时 线程 A 会释放 keyA 对应的锁,唤醒阻塞在该锁上的线程。其它线程即可从 BlockingCache 中获取 keyA 对应的数据,而不是再次访问数据库。
@Override public void putObject(Object key, Object value) { try { // 存入 key 和其对应的缓存项 delegate.putObject(key, value); } finally { // 最后释放锁 releaseLock(key); } } private void releaseLock(Object key) { ReentrantLock lock = locks.get(key); // 锁是否被当前线程持有 if (lock.isHeldByCurrentThread()) { // 是,则释放锁 lock.unlock(); } }
FifoCache和LruCache
为了控制缓存的大小,系统需要按照一定的规则清理缓存。FifoCache 是先入先出版本的装饰器,当向缓存添加数据时,如果缓存项的个数已经达到上限,则会将缓存中最老(即最早进入缓存)的缓存项删除。
public class FifoCache implements Cache { // 被装饰对象 private final Cache delegate; // 用一个 FIFO 的队列记录 key 的顺序,其具体实现为 LinkedList private final Deque<Object> keyList; // 决定了缓存的容量上限 private int size; // 国际惯例,通过构造方法初始化自己的属性,缓存容量上限默认为 1024个 public FifoCache(Cache delegate) { this.delegate = delegate; this.keyList = new LinkedList<>(); this.size = 1024; } @Override public String getId() { return delegate.getId(); } @Override public int getSize() { return delegate.getSize(); } public void setSize(int size) { this.size = size; } @Override public void putObject(Object key, Object value) { // 存储缓存项之前,先在 keyList 中注册 cycleKeyList(key); // 存储缓存项 delegate.putObject(key, value); } private void cycleKeyList(Object key) { // 在 keyList队列 中注册要添加的 key keyList.addLast(key); // 如果注册这个 key 会超出容积上限,则把最老的一个缓存项清除掉 if (keyList.size() > size) { Object oldestKey = keyList.removeFirst(); delegate.removeObject(oldestKey); } } @Override public Object getObject(Object key) { return delegate.getObject(key); } @Override public Object removeObject(Object key) { return delegate.removeObject(key); } // 除了清理缓存项,还要清理 key 的注册列表 @Override public void clear() { delegate.clear(); keyList.clear(); } }
LruCache 是按照"近期最少使用算法"(Least Recently Used, LRU)进行缓存清理的装饰器,在需要清理缓存时,它会清除最近最少使用的缓存项。
public class LruCache implements Cache { // 被装饰者 private final Cache delegate; // 这里使用的是 LinkedHashMap,它继承了 HashMap,但它的元素是有序的 private Map<Object, Object> keyMap; // 最近最少被使用的缓存项的 key private Object eldestKey; // 国际惯例,构造方法中进行属性初始化 public LruCache(Cache delegate) { this.delegate = delegate; // 这里初始化了 keyMap,并定义了 eldestKey 的取值规则 setSize(1024); } public void setSize(final int size) { // 初始化 keyMap,同时指定该 Map 的初始容积及加载因子,第三个参数true 表示 该LinkedHashMap // 记录的顺序是 accessOrder,即,LinkedHashMap.get()方法 会改变其中元素的顺序 keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) { private static final long serialVersionUID = 4267176411845948333L; // 当调用 LinkedHashMap.put()方法 时,该方法会被调用 @Override protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) { boolean tooBig = size() > size; if (tooBig) { // 当已达到缓存上限,更新 eldestKey字段,后面将其删除 eldestKey = eldest.getKey(); } return tooBig; } }; } // 存储缓存项 @Override public void putObject(Object key, Object value) { delegate.putObject(key, value); // 记录缓存项的 key,超出容量则清除最久未使用的缓存项 cycleKeyList(key); } private void cycleKeyList(Object key) { keyMap.put(key, key); // eldestKey 不为空,则表示已经达到缓存上限 if (eldestKey != null) { // 清除最久未使用的缓存 delegate.removeObject(eldestKey); // 制空 eldestKey = null; } } @Override public Object getObject(Object key) { // 访问 key元素 会改变该元素在 LinkedHashMap 中的顺序 keyMap.get(key); //touch return delegate.getObject(key); } @Override public String getId() { return delegate.getId(); } @Override public int getSize() { return delegate.getSize(); } @Override public Object removeObject(Object key) { return delegate.removeObject(key); } @Override public void clear() { delegate.clear(); keyMap.clear(); } }
SoftCache和WeakCache
在分析 SoftCache 和 WeakCache 实现之前,我们再温习一下 Java 提供的 4 种引用类型,强引用 StrongReference、软引用 SoftReference、弱引用 WeakReference 和虚引用 PhantomReference。
- 强引用 平时用的最多的,如 Object obj = new Object(),新建的 Object 对象 就是被强引用的。如果一个对象被强引用,即使是 JVM 内存空间不足,要抛出 OutOfMemoryError 异常,GC 也绝不会回收该对象。
- 软引用 仅次于强引用的一种引用,它使用类 SoftReference 来表示。当 JVM 内存不足时,GC 会回收那些只被软引用指向的对象,从而避免内存溢出。软引用适合引用那些可以通过其他方式恢复的对象,例如, 数据库缓存中的对象就可以从数据库中恢复,所以软引用可以用来实现缓存,下面要介绍的 SoftCache 就是通过软引用实现的。
另外,由于在程序使用软引用之前的某个时刻,其所指向的对象可能己经被 GC 回收掉了,所以通过 Reference.get()方法 来获取软引用所指向的对象时,总是要通过检查该方法返回值是否为 null,来判断被软引用的对象是否还存活。 - 弱引用 弱引用使用 WeakReference 表示,它不会阻止所引用的对象被 GC 回收。在 JVM 进行垃圾回收时,如果指向一个对象的所有引用都是弱引用,那么该对象会被回收。 所以,只被弱引用所指向的对象,其生存周期是 两次 GC 之间 的这段时间,而只被软引用所指向的对象可以经历多次 GC,直到出现内存紧张的情况才被回收。
- 虚引用 最弱的一种引用类型,由类 PhantomReference 表示。虚引用可以用来实现比较精细的内存使用控制,但很少使用。
- 引用队列(ReferenceQueue ) 很多场景下,我们的程序需要在一个对象被 GC 时得到通知,引用队列就是用于收集这些信息的队列。在创建 SoftReference 对象 时,可以为其关联一个引用队列,当 SoftReference 所引用的对象被 GC 时, JVM 就会将该 SoftReference 对象 添加到与之关联的引用队列中。当需要检测这些通知信息时,就可以从引用队列中获取这些 SoftReference 对象。不仅是 SoftReference,弱引用和虚引用都可以关联相应的队列。
SoftCache 的具体实现
public class SoftCache implements Cache { // 这里使用了 LinkedList 作为容器,在 SoftCache 中,最近使用的一部分缓存项不会被 GC // 这是通过将其 value 添加到 hardLinksToAvoidGarbageCollection集合 实现的(即,有强引用指向其value) private final Deque<Object> hardLinksToAvoidGarbageCollection; // 引用队列,用于记录已经被 GC 的缓存项所对应的 SoftEntry对象 private final ReferenceQueue<Object> queueOfGarbageCollectedEntries; // 持有的被装饰者 private final Cache delegate; // 强连接的个数,默认为 256 private int numberOfHardLinks; // 构造方法进行属性的初始化 public SoftCache(Cache delegate) { this.delegate = delegate; this.numberOfHardLinks = 256; this.hardLinksToAvoidGarbageCollection = new LinkedList<>(); this.queueOfGarbageCollectedEntries = new ReferenceQueue<>(); } private static class SoftEntry extends SoftReference<Object> { private final Object key; SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) { // 指向 value 的引用是软引用,并且关联了 引用队列 super(value, garbageCollectionQueue); // 强引用 this.key = key; } } @Override public void putObject(Object key, Object value) { // 清除已经被 GC 的缓存项 removeGarbageCollectedItems(); // 添加缓存 delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries)); } private void removeGarbageCollectedItems() { SoftEntry sv; // 遍历 queueOfGarbageCollectedEntries集合,清除已经被 GC 的缓存项 value while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) { delegate.removeObject(sv.key); } } @Override public Object getObject(Object key) { Object result = null; @SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache // 用一个软引用指向 key 对应的缓存项 SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key); // 检测缓存中是否有对应的缓存项 if (softReference != null) { // 获取 softReference 引用的 value result = softReference.get(); // 如果 softReference 引用的对象已经被 GC,则从缓存中清除对应的缓存项 if (result == null) { delegate.removeObject(key); } else { synchronized (hardLinksToAvoidGarbageCollection) { // 将缓存项的 value 添加到 hardLinksToAvoidGarbageCollection集合 中保存 hardLinksToAvoidGarbageCollection.addFirst(result); // 如果 hardLinksToAvoidGarbageCollection 的容积已经超过 numberOfHardLinks // 则将最老的缓存项从 hardLinksToAvoidGarbageCollection 中清除,FIFO if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) { hardLinksToAvoidGarbageCollection.removeLast(); } } } } return result; } @Override public Object removeObject(Object key) { // 清除指定的缓存项之前,也会先清理被 GC 的缓存项 removeGarbageCollectedItems(); return delegate.removeObject(key); } @Override public void clear() { synchronized (hardLinksToAvoidGarbageCollection) { // 清理强引用集合 hardLinksToAvoidGarbageCollection.clear(); } // 清理被 GC 的缓存项 removeGarbageCollectedItems(); // 清理最底层的缓存项 delegate.clear(); } @Override public String getId() { return delegate.getId(); } @Override public int getSize() { removeGarbageCollectedItems(); return delegate.getSize(); } public void setSize(int size) { this.numberOfHardLinks = size; } }
WeakCache 的实现与 SoftCache 基本类似,唯一的区别在于其中使用 WeakEntry(继承了 WeakReference)封装真正的 value 对象,其他实现完全一样。
另外,还有 ScheduledCache、LoggingCache、SynchronizedCache、SerializedCache 等。ScheduledCache 是周期性清理缓存的装饰器,它的 clearInterval 字段 记录了两次缓存清理之间的时间间隔,默认是一小时,lastClear 字段 记录了最近一次清理的时间戳。 ScheduledCache 的 getObject()、putObject()、removeObject() 等核心方法,在执行时都会根据这两个字段检测是否需要进行清理操作,清理操作会清空缓存中所有缓存项。
LoggingCache 在 Cache 的基础上提供了日志功能,它通过 hit 字段 和 request 字段 记录了 Cache 的命中次数和访问次数。在 LoggingCache.getObject()方法 中,会统计命中次数和访问次数 这两个指标,井按照指定的日志输出方式输出命中率。
SynchronizedCache 通过在每个方法上添加 synchronized 关键字,为 Cache 添加了同步功能,有点类似于 JDK 中 Collections 的 SynchronizedCollection 内部类。
SerializedCache 提供了将 value 对象 序列化的功能。SerializedCache 在添加缓存项时,会将 value 对应的 Java 对象 进行序列化,井将序列化后的 byte[]数组 作为 value 存入缓存 。 SerializedCache 在获取缓存项时,会将缓存项中的 byte[]数组 反序列化成 Java 对象。不使用 SerializedCache 装饰器 进行装饰的话,每次从缓存中获取同一 key 对应的对象时,得到的都是同一对象,任意一个线程修改该对象都会影响到其他线程,以及缓存中的对象。而使用 SerializedCache 每次从缓存中获取数据时,都会通过反序列化得到一个全新的对象。 SerializedCache 使用的序列化方式是 Java 原生序列化。
CacheKey
在 Cache 中唯一确定一个缓存项,需要使用缓存项的 key 进行比较,MyBatis 中因为涉及 动态 SQL 等多方面因素, 其缓存项的 key 不能仅仅通过一个 String 表示,所以 MyBatis 提供了 CacheKey 类 来表示缓存项的 key,在一个 CacheKey 对象 中可以封装多个影响缓存项的因素。 CacheKey 中可以添加多个对象,由这些对象共同确定两个 CacheKey 对象 是否相同。
public class CacheKey implements Cloneable, Serializable { private static final long serialVersionUID = 1146682552656046210L; public static final CacheKey NULL_CACHE_KEY = new NullCacheKey(); private static final int DEFAULT_MULTIPLYER = 37; private static final int DEFAULT_HASHCODE = 17; // 参与计算hashcode,默认值DEFAULT_MULTIPLYER = 37 private final int multiplier; // 当前CacheKey对象的hashcode,默认值DEFAULT_HASHCODE = 17 private int hashcode; // 校验和 private long checksum; private int count; // 由该集合中的所有元素 共同决定两个CacheKey对象是否相同,一般会使用一下四个元素 // MappedStatement的id、查询结果集的范围参数(RowBounds的offset和limit) // SQL语句(其中可能包含占位符"?")、SQL语句中占位符的实际参数 private List<Object> updateList; // 构造方法初始化属性 public CacheKey() { this.hashcode = DEFAULT_HASHCODE; this.multiplier = DEFAULT_MULTIPLYER; this.count = 0; this.updateList = new ArrayList<>(); } public CacheKey(Object[] objects) { this(); updateAll(objects); } public void update(Object object) { int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); // 重新计算count、checksum和hashcode的值 count++; checksum += baseHashCode; baseHashCode *= count; hashcode = multiplier * hashcode + baseHashCode; // 将object添加到updateList集合 updateList.add(object); } public int getUpdateCount() { return updateList.size(); } public void updateAll(Object[] objects) { for (Object o : objects) { update(o); } } /** * CacheKey重写了 equals() 和 hashCode()方法,这两个方法使用上面介绍 * 的 count、checksum、hashcode、updateList 比较两个 CacheKey对象 是否相同 */ @Override public boolean equals(Object object) { // 如果为同一对象,直接返回 true if (this == object) { return true; } // 如果 object 都不是 CacheKey类型,直接返回 false if (!(object instanceof CacheKey)) { return false; } // 类型转换一下 final CacheKey cacheKey = (CacheKey) object; // 依次比较 hashcode、checksum、count,如果不等,直接返回 false if (hashcode != cacheKey.hashcode) { return false; } if (checksum != cacheKey.checksum) { return false; } if (count != cacheKey.count) { return false; } // 比较 updateList 中的元素是否相同,不同直接返回 false for (int i = 0; i < updateList.size(); i++) { Object thisObject = updateList.get(i); Object thatObject = cacheKey.updateList.get(i); if (!ArrayUtil.equals(thisObject, thatObject)) { return false; } } return true; } @Override public int hashCode() { return hashcode; } @Override public String toString() { StringJoiner returnValue = new StringJoiner(":"); returnValue.add(String.valueOf(hashcode)); returnValue.add(String.valueOf(checksum)); updateList.stream().map(ArrayUtil::toString).forEach(returnValue::add); return returnValue.toString(); } @Override public CacheKey clone() throws CloneNotSupportedException { CacheKey clonedCacheKey = (CacheKey) super.clone(); clonedCacheKey.updateList = new ArrayList<>(updateList); return clonedCacheKey; } }
MyBatis的缓存模块, Cache接口以及多个实现类的具体实现,是 Mybatis 中一级缓存和二级缓存的基础。
更多相关内容 -
MyBatis 二级缓存 关联刷新实现
2021-01-20 02:19:46MyBatis 二级缓存 关联刷新实现1、MyBatis缓存介绍2、二级缓存问题2.1、数据不一致问题验证2.2、问题处理思路3、关联缓存刷新实现 1、MyBatis缓存介绍 Mybatis提供对缓存的支持,但是在没有配置的默认情况下,它只... -
深入理解MyBatis中的一级缓存与二级缓存
2020-08-30 04:26:37主要给大家深入的介绍了关于MyBatis中一级缓存与二级缓存的相关资料,文中详细介绍MyBatis中一级缓存与二级缓存的工作原理及使用,对大家具有一定的参考性学习价值,需要的朋友们下面来一起看看吧。 -
MyBatis关于二级缓存问题
2020-08-31 00:42:05本篇文章主要介绍了MyBatis关于二级缓存问题,二级缓存是Mapper级别的缓存,多个sqlSession操作同一个Mapper,其二级缓存是可以共享的。 -
mybatis一级缓存和二级缓存简单示例
2019-01-01 15:31:36配套博客内容,mybatis一级缓存和二级缓存简单示例,供初学童鞋予以参考。 博客地址 https://blog.csdn.net/magi1201/article/details/85524712 -
Redis用作二级缓存
2018-06-04 21:51:03这个主要是用作在Mybatis中的Mapping文件中的二级缓存 -
Mybatis的以及缓存和二级缓存(案例).mhtml
2020-03-25 23:30:40此文章是对Mybatis框架的一级缓存一级二级缓存的描述,包括他们的作用,案例,以及部分源码分析,对一级缓存以及二级缓存进行了详细的描述以及概括,还有如何使用等内容 -
mybatis的一级缓存和二级缓存
2022-04-14 17:29:43在分布式的环境中,mybatis的一二级缓存非常危险,二级缓存一定要关闭,一级缓存视情况关闭 一级缓存失效的四种情况: sqlSession不同。 sqlSession相同,查询条件不同。因为缓存条件不同,缓存中还没有数据。 sq一级缓存
1、说明
mybatis默认开启一级缓存,一级缓存的作用域是
SqlSession
范围的,当在同一个sqlSession
中执行两次相同的sql语句时,第一次执行完毕会将数据库中查询的数据写到缓存,第二次查询时会从缓存中获取数据,不再去底层数据库查询,从而提高查询效率。在分布式的环境中,mybatis的一二级缓存非常危险,二级缓存一定要关闭,一级缓存视情况关闭
一级缓存失效的四种情况:
- sqlSession不同。
- sqlSession相同,查询条件不同。因为缓存条件不同,缓存中还没有数据。
- sqlSession相同,在两次相同查询条件中间执行过增删改操作。(因为中间的增删改可能对缓存中数据进行修改,所以不能用)
- sqlSession相同,手动清空了一级缓存。
2、关闭方法
(1)注解形式
使用注解
@Options(flushCache = Options.FlushCachePolicy.TRUE)
可指定仅仅某个Mapper关闭注解)@Options(flushCache = Options.FlushCachePolicy.TRUE) @Select("select * from ge_jdbc_datasource where id = #{id,jdbcType=BIGINT} and status = 1") @ResultMap("resultMap") JdbcDataSource find(Long id);
(2)传入随机数
比如sql传参random()数值 或者 sql传入当前时间毫秒数,切记一定要从方法形参传过去而不要在sql中拼写,否则无效
举例说明:下面方式无效
select id from ge_jdbc_datasource where id = 1 and STATUS = 1 AND NOW()=NOW()
(3)设置 statementType
在
mapper
的select
标签中设置statementType=STATEMENT
statementType
的设置有3种:STATEMENT
:直接操作sql,不进行预编译,获取数据PREPARED
:(默认)预处理,参数,进行预编译,获取数据CALLABLE
:执行存储过程————CallableStatement
(4)设置 flushCache
在
mapper
的select
标签中设置flushCache=“true”
(5)设置 localCacheScope
全局设置
localCacheScope=STATEMENT
参考:https://blog.csdn.net/u011649691/article/details/116058056
2、案例
我们在
getPaymentById
方法中写了两次查询,并且保证方法内的sqlSession
是同一个。在两次查询期间对数据进行修改,将serial=001
修改为serial=002
。package com.scy.springcloud.service.impl; import com.scy.springcloud.dao.PaymentDao; import com.scy.springcloud.entities.Payment; import com.scy.springcloud.service.PaymentService; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.List; @Service public class PaymentServiceImpl implements PaymentService { @Resource private PaymentDao paymentDao; //这里注入的sqlSession是org.mybatis.spring.SqlSessionTemplate /*@Resource public SqlSession sqlSession;*/ @Resource SqlSessionFactory sqlSessionFactory; @Override public Payment getPaymentById2(Long id) { //org.apache.ibatis.session.defaults.DefaultSqlSession SqlSession sqlSession = sqlSessionFactory.openSession(); Payment payment = sqlSession.selectOne("getPaymentById", id); System.out.println(payment.toString()); try { Thread.sleep(10*1000); } catch (InterruptedException e) { e.printStackTrace(); } Payment payment2 = sqlSession.selectOne("getPaymentById", id); System.out.println(payment2.toString()); sqlSession.close(); return payment2; } }
查询1(开启缓存)
两次查询是同样的,没有改变,说明开启了一级缓存。
Payment(id=31, serial=001) Payment(id=31, serial=001)
查询2(关闭缓存)
在
select
标签中加入flushCache="true"
后,使一级缓存失效。<select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap" flushCache="true"> select * from payment where id = #{id}; </select>
第二次查询结果变为serial=002,说明一级缓存失效。
Payment(id=31, serial=001) Payment(id=31, serial=002)
二级缓存
1、说明
MyBatis
的二级缓存是Application级别的缓存,二级缓存的作用域默认为mapper(namespace)
工作机制:
-
一个会话,查询一条数据,这个数据会被放在当前会话的一级缓存中。
-
如果会话被关闭了,一级缓存中的数据会被保存到二级缓存。新的会话查询信息就会参照二级缓存。
-
sqlSession > Employee>employee
sqlSession >DepartmentMapper=>Department
不同的namespace查出的数据会放在自己对应的缓存中。
效果:
查出的数据首先放在一级缓存中,只有一级缓存被关闭或者提交以后,一级缓存数据才会转移到二级缓存
2、开启方法
(1)方式一
在
mybatis
的配置文件中进行配置<configuration> <settings> <!-- 二级缓存开启 --> <setting name="cacheEnabled" value="true"/> </settings> </configuration>
(2)方式二
在
application.yml
文件中进行配置开启mybatis的二级缓存mybatis.configuration.cache-enabled=true
# 开启二级缓存 mybatis: configuration: cache-enabled: true # 开启sql打印(可以方便查看是否使用了缓存) logging: level: com.scy.springcloud.dao: debug
3、开启缓存配置
开启二级缓存的分开关。
要加入cache的配置二级缓存才会开启。
<mapper namespace="com.scy.springcloud.dao.PaymentDao"> <!-- 开启二级缓存 --> <cache eviction="LRU" flushInterval="20000" readOnly="true" size="1024"> </cache> </mapper>
cache 相关属性:
-
eviction
: 缓存的回收策略LRU
(默认):最近最少使用,移除最长时间不被使用的对象FIFO
:先进先出,安对象进入缓存的顺序来移除它们SOFT
:软引用,移除基于垃圾回收器的状态和软引用规则的对象WEAK
: 弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象
-
flushInterval
:缓存刷新间隔 缓存多长时间清空一次,默认不清空,设置一个毫秒值 -
readOnly
:是否只读 。true
:只读,mybatis
认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。mybatis
为了加快获取速度,直接会将数据在缓存中的引用交给用户,不安全,但速度快。false
:非只读,mybatis
觉得获取的数据可能会被修改。会利用序列化&反序列化的技术克隆一份新的数据给你,安全,但速度慢。
-
size
:缓存最多存放多少个引用。默认1024。 -
type
:指定自定义缓存的全类名,实现Mybatis
提供的Cache
接口即可。
注意事项:
实体对象要
implements Serializable
,否则报错public class User implements Serializable{ }
启动工程,如果出现
Cache Hit Ratio . # 表示缓存开启了.....
参考:
Mybatis二级缓存失效及二级缓存使用简介
https://blog.csdn.net/sao_jie/article/details/119297811浅谈一下mybatis中@CacheNamespace和@CacheNamespaceRef的区别以及使用
https://blog.csdn.net/lovely960823/article/details/111277801
-
mybatis 一级缓存和二级缓存
2022-04-05 22:13:22} } 在一个事务内,多次读取数据只会查询一次数据库 二级缓存 二级缓存作用的在同一个mapper作用域下,需手动开启,一个mapper对应的缓存,多个sqlSession可以共享 手动配置: 1、配置 2、需要在二级缓存生效的...配置mybatis
maven:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency>
application.yml:
server: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mybatisstu?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: 123456 mybatis: #mybatis配置 mapper-locations: classpath:mapper/*.xml type-aliases-package: com.fen.dou.entity config-location: classpath:config/mybatis.xml
classpath:config/mybatis.xml 其中 <setting name="logImpl" value="STDOUT_LOGGING"/>打印执行sql
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> </configuration>
一级缓存
一级缓存作用在同一个sqlSession内的,是默认开启的,添加事务才会生效,一个事务内及一个业务逻辑之内应该不会重复去读取事务(跟可重复读事务相匹配,不会产生脏数据,如果为读未提交和读已提交事务,则会产生脏数据),所以没必要有一级缓存
@Override @Transactional public User findUser() { userMapper.findUser(); userMapper.findUser(); return userMapper.findUser(); }
import com.fen.dou.Application; import com.fen.dou.service.IProductService; import com.fen.dou.service.IUserService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest(classes = {Application.class}) public class TestJunit { @Autowired private IUserService userService; @Test public void testSay() throws Exception { userService.findUser(); } }
在一个事务内,多次读取数据只会查询一次数据库
二级缓存
二级缓存作用的在同一个mapper作用域下,需手动开启,一个mapper对应的缓存,多个sqlSession可以共享
手动配置:
1、配置<setting name="cacheEnabled" value="true" />
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> <setting name="cacheEnabled" value="true" /> </configuration>
2、需要在二级缓存生效的mapper中配置:<cache></cache>
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.fen.dou.mapper.ProductMapper"> <cache></cache> <select id="selectCountById" parameterType="int" resultType="int"> select `count` from product where id=#{productId} </select> <update id="reduceCount" parameterType="map"> UPDATE product SET count=count-1 WHERE id=1; </update> </mapper>
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.fen.dou.mapper.UserMapper"> <cache></cache> <resultMap id="ResultMap" type="com.fen.dou.entity.User"> <id column="id" property="id" /> </resultMap> <resultMap id="ResultMap1" type="com.fen.dou.entity.UserVo"> </resultMap> <select id="findUser" resultMap="ResultMap"> select id,username,password from user where id = 1; </select> <update id="updateUser" > update user set username = 'yangcai' where id = 1 </update> <select id="findUserProduct" resultMap="ResultMap1" useCache="false"> select product.count,username,password from user left join product on user.id = product.userId where user.id = 1; </select> </mapper>
3、需要在sql返回结果的实体中添加序列化
@Data public class UserVo implements Serializable { private Integer count; private String username; private String password; }
@Data public class User implements Serializable { private Integer id; private String username; private String password; public User(Integer id, String username, String password) { this.id = id; this.username = username; this.password = password; } }
现在来测试二级缓存
@GetMapping(value = "/test1") public User test1(){ return this.userService.findUser(); }
<select id="findUser" resultMap="ResultMap"> select id,username,password from user where id = 1; </select>
请求两次, 两个会话只查询了一次
当执行关联表的sql
@GetMapping(value = "/test2") public UserVo test5(){ return this.userService.findUserProduct(); }
<select id="findUserProduct" resultMap="ResultMap1"> select product.count,username,password from user left join product on user.id = product.userId where user.id = 1; </select>
这个缓存作用在userMapper作用域下,此时数据库中的count的值确实是5
当我在productMapper作用域下更新count值
@GetMapping(value = "/update1") public void test3() throws Exception { this.productService.reduceCount(1,1); }
<update id="reduceCount" parameterType="map"> UPDATE product SET count=count-1 WHERE id=1; </update>
数据库中的值变成4了,再次请求http://localhost:8080/test2,count的值仍然是5,造成数据不一致了(数据库和缓存的数据不相同)
所以,这个sql不能生成二级缓存,否则会造成数据不一致,可以对一个mapper下单个查询设置不生成二级缓存,配置 useCache="false",则每次都是从数据库里面去查询数据
<select id="findUserProduct" resultMap="ResultMap1" useCache="false"> select product.count,username,password from user left join product on user.id = product.userId where user.id = 1; </select>
二级缓存对单表查询可以设置设置二级缓存,对多表联合查询,需要禁止开启二级缓存,否则会造成数据不一致
-
Mybatis 中的一级缓存与二级缓存
2022-01-19 23:57:32Mybatis 中的一级缓存与二级缓存一,Mybatis中为什么要有缓存
缓存的意义是将用户经常查询的数据放入缓存(内存)中去,用户去查询数据的时候就不需要从磁盘(关系型数据库)中查询,直接从缓存中查询,从而提高了查询效率,解决了高并发中系统的性能问题。Mybatis中提供一级缓存与二级缓存。
Mybatis的一级缓存是一个SqlSession
级别的缓存,只能访问自己的一级缓存数据,而二级缓存是Mapper
级别的缓存,是跨SqlSession的
,不同的SqlSession
是可以共享缓存数据的。
二,一级缓存
Mybatis 一级缓存原理:
第一次发出查询请求,sql 查询的结果写入SqlSession的一级缓存当中,缓存使用的数据结构是一个map<key, value>
- key : hashcode + sql + sql输入参数 + 输出参数 (sql的唯一标识)
- value : 用户信息
同一个
SqlSession
再次发出相同的sql,就会从缓存中读取而不走数据库,如果两次操作之间出现commit(修改、输出、添加)
操作,那么本SqlSession
中一级缓存区域全部清空,下次再去缓存中查不到所以要从数据库中查询,从数据库再写入一级缓存。@Test public void createTable(){ SqlSession sqlSession = DBUtil.getSqlSession(); List<Map<String, Object>> list1 = sqlSession.selectList("com.snow.xml.SnowOracle.getEmployeeByName", "周康"); System.out.println(list); List<Map<String, Object>> list2 = sqlSession.selectList("com.snow.xml.SnowOracle.getEmployeeByName", "周康"); System.out.println(list2); }
在数据库中有一张Employee表,里面有一条数据,通过selectList的方法查询,结果如下
[{ID=70DD7D10-9FC6-4B79-ADAF-8B408DE1E048, EMPNAME=周康, AGE=36, DMGRP=男, BIRTHDATE=1987-07-31 00:00:00.0, SALARY=17000, ADDRESS=湖南长沙, GRADE=高级架构师}]
此时,手动修改数据库该人的年龄,手动修改为35,然后保存
此时,在运行代码,查看list2的值,两次结果一致,都是36 并非 35,说明第二次相同的查询走的是SqlSession中的一级缓存。
Mybatis 中一级缓存需要注意的点 :Mybatis
中一级缓存是默认开启的,不需要手动配置。Mybatis
和Spring
整合后进行mapper
代理开发后,不支持一级缓存。Mybatis
和Spring
整合,Spring
按照mapper
的模板去生成mapper
代理对象,模板中在最后会统一关闭SqlSession
。
三、二级缓存
Mybatis二级缓存原理:
二级缓存的范围是mapper级别(mapper同一个命名空间),mapper以命名空间为单位创建缓存数据结构,结构是map<key, value>。每次查询前看是否开启了二级缓存,如果开启则从二级缓存的数据结构中取缓存数据,如果二级缓存中没有取到,再从一级缓存中取,如果一级缓存也没有,那就从数据库中查询。- 二级缓存配置
需要在Mybatis的配置文件中
<settings>
标签中配置二级缓存:<settings> <setting name="cacheEnabled" value="true"/> <!--Mybatis的二级缓存配置--> </settings>
Mybatis的二级缓存的范围是mapper级别的,因此我们mapper如果想要使用二级缓存,还需要在对应的映射文件中配置
<cache>
标签<mapper namespace="com.snow.xml.SnowOracle"> <cache></cache> <!--Mybatis的二级缓存配置--> </mapper>
测试:
@Test public void test(){ SqlSession sqlSession1 = DBUtil.getSqlSession(); List<Map<String, Object>> list1 = sqlSession1.selectList("getEmployeeByName", "周康"); System.out.println("list1=" + list1); sqlSession1.commit(); sqlSession1.close(); DBUtil.closeSqlsession(); SqlSession sqlSession2 = DBUtil.getSqlSession(); List<Map<String, Object>> list2 = sqlSession2.selectList("getEmployeeByName", "周康"); System.out.println("list2=" + list2); sqlSession2.commit(); sqlSession2.close(); }
在SqlSession2 创建处打断点,观看此时输出:
list1=[{ID=70DD7D10-9FC6-4B79-ADAF-8B408DE1E048, EMPNAME=周康, AGE=35, DMGRP=男, BIRTHDATE=1987-07-31 00:00:00.0, SALARY=17000, ADDRESS=湖南长沙, GRADE=高级架构师}]
此时去修改数据库中周康此人的年龄,改为37提交,代码继续往下执行,会看到SqlSession2 与SqlSession1 是两个不同的SqlSession,观看此时输出:list2=[{ID=70DD7D10-9FC6-4B79-ADAF-8B408DE1E048, EMPNAME=周康, AGE=35, DMGRP=男, BIRTHDATE=1987-07-31 00:00:00.0, SALARY=17000, ADDRESS=湖南长沙, GRADE=高级架构师}]
两次结果一致,均为35岁,说明SqlSession2 的查询没有走数据库,而是用了Mybatis的二级缓存,从里面拿到的数据,虽然是两个不同的SqlSession,但是二级缓存是mapper级别的,SqlSession1 只执行了查询操作没有增改删,所以不会清空二级缓存中的数据。此处如果关闭了二级缓存的配置,查询出来的结果会是实时的,因为一级缓存默认开启,一级缓存的作用是SqlSession级别的,不同的SqlSession缓存数据不共享。这里就不演示一级缓存效果了。
- 禁用二级缓存
有些情况下,我们需要打开二级缓存的配置,但是某个sql语句的查询变化频率较高,则需要针对该sql禁用二级缓存。在xml中statement中设置useCache=false 则可以禁用当前select语句的二级缓存,即每次查询都会发出sql去查询,默认是true(使用二级缓存)
<select id="getEmployeeByName" parameterType="string" resultType="java.util.LinkedHashMap" useCache="false"> SELECT E.ID, E.EMPNAME, E.AGE, GB01.DMGRP, E.BIRTHDATE, E.SALARY, E.ADDRESS, GB02.DMGRP AS GRADE FROM EMPLOYEE E LEFT JOIN GB01 ON E.SEX = GB01.ID LEFT JOIN GB02 ON E.GRADE = GB02.ID WHERE E.EMPNAME = #{name} </select>
测试:
操作与上面一样,SqlSession2处打断点,更改数据库中该人的年龄,查看两次输出结果。
list1=[{ID=70DD7D10-9FC6-4B79-ADAF-8B408DE1E048, EMPNAME=周康, AGE=37, DMGRP=男, BIRTHDATE=1987-07-31 00:00:00.0, SALARY=17000, ADDRESS=湖南长沙, GRADE=高级架构师}]
list2=[{ID=70DD7D10-9FC6-4B79-ADAF-8B408DE1E048, EMPNAME=周康, AGE=35, DMGRP=男, BIRTHDATE=1987-07-31 00:00:00.0, SALARY=17000, ADDRESS=湖南长沙, GRADE=高级架构师}]
可以看到操作虽然与第一次一样,可结果却变了,虽然二级缓存中有周康该人的信息,但是SqlSession2 还是从数据库中查询到了此人最新的数据,因为我们禁用了二级缓存。
useCache=false
- 增删改的二级缓存
二级缓存其实大部分都是为查询服务的,对于它们而言,如果我们缓存的数据不是最新的那么就会读到脏数据了。增删改之后之所以二级缓存会被清空是因为它们有一个默认的
flushCache=true
,默认在sql结束后刷新二级缓存,可以通过修改配置值达到不刷新缓存的目的(不建议使用)。<update id="updateAgeByName" parameterType="string" flushCache="false"> UPDATE EMPLOYEE SET AGE = '40' WHERE EMPNAME = #{EMPNAME} </update>
四、了解Mybatis缓存的一些参数
mybatis 的cache 参数只适用于mybatis 维护缓存。
flushInterval
: 刷新间隔,可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式时间段,默认情况是不设置,也就是没有刷新间隔,缓存仅仅在调用语句时刷新。size
: 引用数目,可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的可用内存资源数目默认值为1024。readOnly
: 只读属性,可被设置为true or false,只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。可读写的缓存会返回缓存对象的拷贝(通过序列化)。这样会导致效率慢一些,但是安全,默认值为false。<cache eviction="FIFO" flushInterval="6000" size="512" readOnly="true" />
这样的二级缓存配置,创建了一个
FIFO
的缓存,并且每隔60秒刷新,存数结果对象或列表的512个引用,而且返回的对象被认为是只读的,因此在不同线程中的调用者之间修改它们会导致冲突。默认的回收策略有(默认为LRU):
- LRU:最近最少使用的,移除最长时间不被使用的对象。
- FIFO:先进先出,俺对象进入缓存的顺序来移除它们。
- SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象。
- WEAK:弱引用,更积极地移除给予垃圾回收器状态和弱引用规则的对象。
-
hibernate的一级缓存和二级缓存
2018-01-07 00:46:58hibernate的一级缓存和二级缓存,hibernate的一级缓存和二级缓存,hibernate的一级缓存和二级缓存,hibernate的一级缓存和二级缓存,hibernate的一级缓存和二级缓存 -
JPA-5 使用二级缓存
2017-04-08 19:51:51使用JPA的二级缓存的具体实现步骤 -
mybatis原理分析(三)---一级缓存和二级缓存
2020-09-20 17:00:09一级缓存2.1 一级缓存的命中场景2.2 触发清空一级缓存2.3 一级缓存源码分析3 二级缓存3.1 二级缓存的设计3.2 二级缓存的使用3.3 二级缓存的命中场景3.4 二级缓存源码分析3.4.1 query查询操作。3.4.2 commit提交操作... -
mybatis二级缓存架构原理
2022-03-14 16:06:05一级缓存 与springboot集成时一级缓存不生效原因以及解决方案 如何解决这个问题? 要知道如何解决这个问题, ...二级缓存 ...为什么mybatis默认不开启二级缓存? 不推荐使用。 ... -
MyBatis 的一级缓存和二级缓存
2020-04-22 18:26:11文章目录一、前言二、一级缓存三、二级缓存3.1、开启二级缓存:四、测试一下五、总结: 一、前言 先说缓存,合理使用缓存是优化中最常见的,将从数据库中查询出来的数据放入缓存中,下次使用时不必从数据库查询,... -
Mybatis缓存(四)-二级缓存实现原理
2021-09-07 19:50:22二级缓存实现原理概述CachingExecutor类TransactionalCacheManagerCachingExecutor#queryCachingExecutor#update 概述 MyBatis二级缓存在默认情况下是关闭的,因此需要通过设置cacheEnabled参数值为true来开启二级... -
面试官:Mybatis一级缓存和二级缓存同时开启,先查询哪个缓存?
2020-11-08 18:35:34要想回答这个问题,必须把一级缓存和二级缓存的实现搞明白,详细介绍一下 我们知道Mybatis有一级缓存和二级缓存,底层都是用HashMap实现的 key为CacheKey对象(后续说原因),value为从数据库中查出来的值。 Mybatis... -
Hibernate二级缓存
2017-11-03 16:08:34Hibernate二级缓存Hibernate二级缓存Hibernate二级缓存Hibernate二级缓存Hibernate二级缓存 -
Mybatis的一级缓存和二级缓存机制原理和区别
2022-01-19 16:05:00Mybatis中的一级缓存和二级缓存到底缓存了什么,缓存了以后又有什么效果,缓存的数据什么时候会被清空? 一级缓存:它指的是Mybatis中sqlSession对象的缓存,当我们执行查询以后,查询的结果会同时存入到... -
MyBatis-一级缓存与二级缓存
2020-11-22 20:37:23MyBatis是一个简单,小巧但功能非常强大的ORM开源框架,它的功能强大也体现在它的缓存机制上。 每当我们使用MyBatis开启一次和数据库的会话,MyBatis会创建出一个SqlSession对象表示一次数据库会话。 在对数据库的一... -
Mybatis实现二级缓存
2021-12-14 16:04:07目录 一、Mybatis实现Ehcache作为二级缓存 ...5 、mybaits的二级缓存是mapper范围级别,除了在SqlMapConfig.xml设置二级缓存的总开关,还要在具体的mapper.xml中开启二级缓存 ①、开启mybatis的二级缓存... -
hibernate二级缓存(一)一级缓存与二级缓存
2019-03-24 15:47:38hibernate二级缓存(一)一级缓存与二级缓存 1.hibernate一级缓存 hibernate的一级缓存是session级别的缓存,一级缓存hibernate默认启用且不能被卸载,一个事务内有效。 特点: 使用一级缓存的目的是为了... -
MyBatis——关于一级缓存 & 二级缓存的案例详解
2021-06-22 21:09:10文章目录: 1.写在前面 2.关于MyBatis中的缓存 3.一级缓存案例详解 3.1 首先写一个实体Bean 3.2 dao接口、对应的mapper映射文件 ...3.3 mybatis配置文件、...4.1 开启二级缓存之后,实体类需要实现序列化 4.2 dao -
一级缓存、二级缓存、三级缓存区别是什么 详解它们的区分方法
2020-02-17 16:39:43大家都知道CPU缓存很重要,但对于缓存的具体细分却知之甚少,本文只要是关于CPU缓存的介绍,并着重描述了一级缓存、二级缓存、三级缓存区别方法。 CPU缓存 CPU缓存(Cache Memory)是位于CPU与内存之间的临时存储器... -
CPU一级缓存,二级缓存和三级缓存是什么意思? CPU缓存有什么用?
2021-07-17 06:16:21所谓的CPU缓存是CPU内部缓存的运行频率. 缓存的大小和结构对CPU速度的影响更大,因此缓存的大小也是CPU的重要性能指标之一. CPU缓存的工作效率可以远远超过内存和硬盘的速度. 以下安装主页主要介绍有关CPU缓存的知识... -
JSP 开发之hibernate配置二级缓存的方法
2021-01-08 19:03:27JSP 开发之hibernate配置二级缓存的方法 hibernate二级缓存也称为进程级的缓存或SessionFactory级的缓存。 二级缓存是全局缓存,它可以被所有的session共享。 二级缓存的生命周期和SessionFactory的生命周期一致... -
一级缓存和二级缓存的理解
2020-07-18 10:39:00Hibernate在运行时才会把它的实例加入到二级缓存中 管理缓存的方式:二 级缓存的物理介质可以使内存和硬盘,因此第二级缓存可以存放大容量的数据,数据过期策略的maxElementsInMemory属性可以控制内存中的对 象数目...