缓存
程序与数据库交互时,是造成性能瓶颈的一个主要原因。数据是存储在磁盘上的,存储大量数据不利于查询。还有要进行网络通信,IO操作等。所以一种解决方案是在程序与数据库之间加入一层就是缓存。
首先缓存是内存,内存是有限的,不可能像使用磁盘一样使用内存,所以内存通常是有限的,这就决定了我们无法把所有的数据放在内存中,一是大小限制,二十数据不安全,放内存中容易丢失。
内存又可以分为两种,一种是外部的,比如redis,memcache等,也可以是在jvm中的一块空间,各有优劣。外部的缓存空间更加充分,但是需要进行网络通信,jvm由于各种限制,内存空间不如外部的,但是读取快。
代码实现
基于缓存的原理,数据结构可选map实现,当然也可以使用redis等存储数据。
在实现的过程中,会发现逻辑有些是重复的
public User queryUserById(){
//对缓存的操作
// ---> Redis 获取 取到 直接返回
// 没取到 访问数据库 执行JDBC
JDBC ----> DB
}
考虑后续要修改的问题,所以每个查询方法都实现这块逻辑是有问题的。所以要把这部分逻辑抽出去,可以使用代理模式。
Mybatis Cache
核心的实现是impl包中的PerpetualCache类,底层是HashMap实现的。
decorators包中的是装饰器增强的Cache类,为了让PerpetualCache拥有其他的一些功能,功能增强。
内存换出策略:由于内存空间的有限性,当内存空间不够时,需要一定的策略来释放内存。
FIFO: 先入先出
LRU: 最少使用
LoggingCache: Cache增加日志功能
BlockingCache: 保证同一时间只有一个线程查询key
ScheduledCache: 设置时间间隔清空缓存
SerializedCache: 自动完成key,value的序列化和反序列化
TransactionalCache: 只在事务操作成功时,把对应的数据放置在缓存中
装饰器设计模式
mybatis中使用了大量装饰器模式
装饰器模式的作用是为目标扩展功能(本职功能、核心功能)
代理设计模式是为目标增加额外功能
装饰器和代理模式的类图是一样的
本质区别就是
装饰器:增加核心功能,和被装饰的对象完成的是同一件事情
代理模式:增加额外功能,和被代理对象做的是不同的事情
一级缓存
mybatis默认开启了一级缓存
注意:一级缓存对同一个SqlSession生效,换SqlSession不能查询原有的SqlSesison缓存中的数据。
每一请求或者每个线程使用的Connection、SqlSession都应该是独立的,因为其中会涉及到事务的处理,不能共用一个,如果共用会造成事务的混乱,比如事务的提交时机。
查询有些情况下也需要事务
- 加悲观锁
- 二级缓存
这里使用了适配器设计模式
适配器设计模式:在实现一个接口的过程中,只想或者只能实现其部分方法,考虑使用适配器设计模式
一级缓存功能主要体现在BaseExecutor中的PerpetualCache localCache
二级缓存
完成二级缓存的类是CachingExecutor,是SimpleExecutor,ReuseExecutor的装饰器,增强了核心的Cache功能。
// CachingExecutor
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 对应了<cache/>
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
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);
}
CachingExecutor创建时机
mybatis解析配置文件封装成Configuration类时创建,newExecutor(),CachingExecutor ce = new CachingExecutor(simpleExecutor),默认创建的Executor就是CachingExecutor。
创建过程
解析配置文件中的cache标签时进行创建,useNewCache,构建者模式
- 声明了默认缓存
- 创建了新的实现
Cache cache = newBaseCacheInstance(implementation, id);
- 读取 整合<cache 增加额外的参数(内置缓存不用,自定义缓存 Redis OsCache Ehcache)
- 增加装饰器
<cache/> PerpetualCache LruCache
<cache blocking="" readOnly="" size="" flushInterval=""/>
evication="FIFO" 表示将默认的LRUCache替换为FIFOCache
创建好的Cache最终存在MappedStatement中,Configuration二级缓存操作从MappedStatement中获取Cache缓存
二级缓存、一级缓存的查询顺序
CachingExecutor -> BaseExecutor -> SimpleExecutor Configuration默认创建的是CachingExecutor,所以是先查询二级缓存的,然后在CachingExecutor中装饰了BaseExecutor,在BaseExector中查询了一级缓存
cache-ref标签
mybatis的二级缓存当有数据更新时,整个Cache都会被清空,防止脏数据的产生。
实际上每个Mapper正常都有自己的cache对象,那就会产生一个问题,当有两个表A、B关联操作时,假如存在了AMapper的Cache中,那么当更新B的数据时,其实是不会清空AMapper的Cache数据的,这时就会造成B表的脏数据。所以要在BMapper中使用cache-ref标签使AMapper合BMapper使用同一个Cache,就解决了脏数据的问题。
所以AB无论是谁更新了数据都会清空这个共同的Cache,所以要想办法根据CacheKey的规律去清空Cache,需要去自定义清空规则,mybatis中是修改不了的,必须自己定义。
可以考虑使用缓存中间件redis、memcache,或者使用jvm级别的ehcache,oschache等