java mysql 二级缓存_Spring + MySQL + Mybatis + Redis【二级缓存】执行流程分析

一级缓存基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该Session中的所有 Cache 就将清空。

二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache、Hazelcast等。

对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被clear。

MyBatis 的缓存采用了delegate机制 及 装饰器模式设计,当put、get、remove时,其中会经过多层 delegate cache 处理,其Cache类别有:BaseCache(基础缓存)、EvictionCache(排除算法缓存) 、DecoratorCache(装饰器缓存):

BaseCache :为缓存数据最终存储的处理类,默认为 PerpetualCache,基于Map存储;可自定义存储处理,如基于EhCache、Memcached等;

EvictionCache :当缓存数量达到一定大小后,将通过算法对缓存数据进行清除。默认采用 Lru 算法(LruCache),提供有 fifo 算法(FifoCache)等;

DecoratorCache:缓存put/get处理前后的装饰器,如使用 LoggingCache 输出缓存命中日志信息、使用 SerializedCache 对 Cache的数据 put或get 进行序列化及反序列化处理、当设置flushInterval(默认1/h)后,则使用 ScheduledCache 对缓存数据进行定时刷新等。

一般缓存框架的数据结构基本上都是 Key-Value 方式存储,MyBatis 对于其 CacheKey 的生成采取规则为:

[hashcode : checksum : mappedStementId : offset : limit : executeSql : queryParams]。

对于并发 Read/Write 时缓存数据的同步问题,MyBatis 默认基于 JDK/concurrent中的ReadWriteLock,使用 ReentrantReadWriteLock 的实现,从而通过 Lock 机制防止在并发 Write Cache 过程中线程安全问题。

测试User user=userService.get(55);

通常我们在service层最终都会调用Mapper的接口方法,实现对数据库的操作,本例中是通过id查询user对象。

我们知道Mapper是一个接口,接口是没有对象的,更不能调用方法了,而我们调用的其实是mybatis框架的mapper动态代理对象MapperProxy,而MapperProxy中有封装了配置信息的DefaultSqlSession中的Configuration。调用mapper方法的具体代码如下。

在执行mapperMethod的execute的时候,不仅传递了方法参数,还传递了sqlSession。在执行execute,其实是通过判断配置文件的操作类型,来调用sqlSession的对应方法的。本例中,由于是select,而返回值不是list,所以下一步执行的是sqlSession的selectOne

selectOne其实调用了selectList,只不过是取了第一个。

selectList经过层层调用,最终交给执行器执行。具体执行器的结构待会我们会分析。注意这里的ms参数,其实就是从Configration中得到的一些配置信息,包括mapper文件里的sql语句。具体代码如下:

这里的执行器execute,其实是spring注入的。excute是一个接口,而到时候具体是哪个execute执行,是看配置文件的。如果启动用了Cache 才调用 CachingExecutor.query,反之则使用 BaseExcutor.query 进行数据库查询

而我们的一级缓存和二级缓存其实都是execute中的一种。接下来,我们遍分析一下执行器(Executor)。

4276c851969186e16642a280f0996450.png

b72a32cc9e047c3c07d04b6d51daadda.png

f52e090e657716a80b5bdf75d40d405d.png

b0a3240309502fdf3550b60660fe4bee.png

886ef0633913b722ac16e9054a325d84.png

1192786ac1fd1c129410c1cc465fab44.png

c5904842ddf51c68c8f397579d60258e.png

0ca2685ceac502fa25226d8b42838e1b.png

515b9ea0eef08fb264837dd79a936e24.png

98fc7dbb9abb9abca0cfd0d9fc65dbab.png

278869451e2e5f3a30702c79cb188d1c.png

二、Executor框架

bebd3a0bc89790f2c6ea944bb13d5949.png

解析器:结合mybatis-spring框架,读取spring关于mybatis的配置文件。具体看是否开启缓存(这里指二级缓存),如果开启,生成的执行器为CachingExecutor。

动态代理:实现调用mapper接口的时候执行mybatis逻辑

执行器:执行缓存处理逻辑。在这里二级缓存和一级缓存有所区别。

46021649b5c093403732b74c6b055c44.png

2a9c15a007b71e85fda9706ac53c0de0.pngBatchExcutor、ReuseExcutor、SimpleExcutor: 这几个就没什么好说的了,继承了 BaseExcutor 的实现其 doQuery、doUpdate 等方法,同样都是采用 JDBC 对数据库进行操作;三者区别在于,批量执行、重用 Statement 执行、普通方式执行。具体应用及场景在Mybatis 的文档上都有详细说明。 

CachingExecutor: 二级缓存执行器。个人觉得这里设计的不错,灵活地使用 delegate机制。其委托执行的类是 BaseExcutor。 当无法从二级缓存获取数据时,同样需要从 DB 中进行查询,于是在这里可以直接委托给 BaseExcutor 进行查询。其大概流程为:

908dda35e68186fdc47471d396bca4d1.png

流程为: 从二级缓存中进行查询 -> [如果缓存中没有,委托给 BaseExecutor] -> 进入一级缓存中查询 -> [如果也没有] -> 则执行 JDBC 查询,其 query 代码如下:

public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throwsSQLException {

BoundSql boundSql=ms.getBoundSql(parameterObject);

CacheKey key= this.createCacheKey(ms, parameterObject, rowBounds, boundSql);return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

}public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throwsSQLException {

Cache cache=ms.getCache();//当前 Statement 是否启用了二级缓存

if (cache != null) {this.flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {this.ensureNoOutParams(ms, parameterObject, boundSql);

List list = (List)this.tcm.getObject(cache, key);if (list == null) {//未找到缓存,很委托给 BaseExecutor 执行查询

list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);this.tcm.putObject(cache, key, list);

}returnlist;

}

}//没有启动用二级缓存,直接委托给 BaseExecutor 执行查询

return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

}publicCacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {//将创建 cache key 委托给 BaseExecutor 创建

return this.delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);

}

Cache 委托链构建:

正如最开始的缓存概述所描述道,其缓存类的设计采用 装饰模式,基于委托的调用机制。

缓存实例构建:

缓存实例的构建 ,Mybatis 在解析其 Mapper 配置文件时就已经将该实现初始化,在 org.apache.ibatis.builder.xml.XMLMapperBuilder 类中可以看到:

Java代码  81935a422c339aa2cc77434da4433223.png

private void cacheElement(XNode context) throwsException {if (context != null) {//基础缓存类型

String type = context.getStringAttribute("type", "PERPETUAL");

Class typeClass=typeAliasRegistry.resolveAlias(type);//排除算法缓存类型

String eviction = context.getStringAttribute("eviction", "LRU");

Class evictionClass=typeAliasRegistry.resolveAlias(eviction);//缓存自动刷新时间

Long flushInterval = context.getLongAttribute("flushInterval");//缓存存储实例引用的大小

Integer size = context.getIntAttribute("size");//是否是只读缓存

boolean readWrite = !context.getBooleanAttribute("readOnly", false);

Properties props=context.getChildrenAsProperties();//初始化缓存实现

builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);

}

}

以下是  useNewCache 方法实现:

Java代码  81935a422c339aa2cc77434da4433223.png

publicCache useNewCache(Class typeClass,

Class evictionClass,

Long flushInterval,

Integer size,booleanreadWrite,

Properties props) {

typeClass= valueOrDefault(typeClass, PerpetualCache.class);

evictionClass= valueOrDefault(evictionClass, LruCache.class);//这里构建 Cache 实例采用 Builder 模式,每一个 Namespace 生成一个 Cache 实例

Cache cache = newCacheBuilder(currentNamespace)//Builder 前设置一些从XML中解析过来的参数

.implementation(typeClass)

.addDecorator(evictionClass)

.clearInterval(flushInterval)

.size(size)

.readWrite(readWrite)

.properties(props)//再看下面的 build 方法实现

.build();

configuration.addCache(cache);

currentCache=cache;returncache;

}publicCache build() {

setDefaultImplementations();//创建基础缓存实例

Cache cache =newBaseCacheInstance(implementation, id);

setCacheProperties(cache);//缓存排除算法初始化,并将其委托至基础缓存中

for (Class extends Cache>decorator : decorators) {

cache=newCacheDecoratorInstance(decorator, cache);

setCacheProperties(cache);

}//标准装饰器缓存设置,如LoggingCache之类,同样将其委托至基础缓存中

cache =setStandardDecorators(cache);//返回最终缓存的责任链对象

returncache;

}

最终生成后的缓存实例对象结构:

f1fbc291131854c13d88204927dee2a7.png 

可见,所有构建的缓存实例已经通过责任链方式将其串连在一起,各 Cache 各负其责、依次调用,直到缓存数据被 Put 至 基础缓存实例中存储。

Cache 实例解剖:

实例类:SynchronizedCache

说   明:用于控制 ReadWriteLock,避免并发时所产生的线程安全问题。

解   剖:

对于 Lock 机制来说,其分为 Read 和 Write 锁,其 Read 锁允许多个线程同时持有,而 Write 锁,一次能被一个线程持有,如果当 Write 锁没有释放,其它需要 Write 的线程只能等待其释放才能去持有。

其代码实现:

public synchronized voidputObject(Object key, Object object) {this.delegate.putObject(key, object);

}public synchronizedObject getObject(Object key) {return this.delegate.getObject(key);

}

其具体原理可以看看 jdk concurrent 中的 ReadWriteLock 实现。

实例类:LoggingCache

说   明:用于日志记录处理,主要输出缓存命中率信息。

解   剖:

说到缓存命中信息的统计,只有在 get 的时候才需要统计命中率:

Java代码  81935a422c339aa2cc77434da4433223.png

publicObject getObject(Object key) {

requests++; //每调用一次该方法,则获取次数+1

final Object value =delegate.getObject(key);if (value != null) { //命中! 命中+1

hits++;

}if(log.isDebugEnabled()) {//输出命中率。计算方法为: hits / requets 则为命中率

log.debug("Cache Hit Ratio [" + getId() + "]: " +getHitRatio());

}returnvalue;

}

实例类:SerializedCache

说   明:向缓存中 put 或 get 数据时的序列化及反序列化处理。

解   剖:

序列化在Java里面已经是最基础的东西了,这里也没有什么特殊之处:

Java代码  81935a422c339aa2cc77434da4433223.png

public voidputObject(Object key, Object object) {//PO 类需要实现 Serializable 接口

if (object == null || object instanceofSerializable) {

delegate.putObject(key, serialize((Serializable) object));

}else{throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " +object);

}

}publicObject getObject(Object key) {

Object object=delegate.getObject(key);//获取数据时对 二进制数据进行反序列化

return object == null ? null : deserialize((byte[]) object);

}

其 serialize 及 deserialize 代码:

private byte[] serialize(Serializable value) {try{

ByteArrayOutputStream bos= newByteArrayOutputStream();

ObjectOutputStream oos= newObjectOutputStream(bos);

oos.writeObject(value);

oos.flush();

oos.close();returnbos.toByteArray();

}catch(Exception var4) {throw new CacheException("Error serializing object. Cause: " +var4, var4);

}

}private Serializable deserialize(byte[] value) {try{

ByteArrayInputStream bis= newByteArrayInputStream(value);

ObjectInputStream ois= newSerializedCache.CustomObjectInputStream(bis);

Serializable result=(Serializable)ois.readObject();

ois.close();returnresult;

}catch(Exception var5) {throw new CacheException("Error deserializing object. Cause: " +var5, var5);

}

}

实例类:LruCache

说   明:最近最少使用的:移除最长时间不被使用的对象,基于LRU算法。

解   剖:

这里的 LRU 算法基于 LinkedHashMap 覆盖其 removeEldestEntry 方法实现。好象之前看过 XMemcached 的 LRU 算法也是这样实现的。

初始化 LinkedHashMap,默认为大小为 1024 个元素:

Java代码  81935a422c339aa2cc77434da4433223.png

publicLruCache(Cache delegate) {this.delegate =delegate;

setSize(1024); //设置 map 默认大小

}public void setSize(final intsize) {//设置其 capacity 为size, 其 factor 为.75F

keyMap = new LinkedHashMap(size, .75F, true) {//覆盖该方法,当每次往该map 中put 时数据时,如该方法返回 True,便移除该map中使用最少的Entry//其参数 eldest 为当前最老的 Entry

protected booleanremoveEldestEntry(Map.Entry eldest) {boolean tooBig = size() >size;if(tooBig) {

eldestKey= eldest.getKey(); //记录当前最老的缓存数据的 Key 值,因为要委托给下一个 Cache 实现删除

}returntooBig;

}

};

}public voidputObject(Object key, Object value) {

delegate.putObject(key, value);

cycleKeyList(key);//每次 put 后,调用移除最老的 key

}//看看当前实现是否有 eldestKey, 有的话就调用 removeObject ,将该key从cache中移除

private voidcycleKeyList(Object key) {

keyMap.put(key, key);//存储当前 put 到cache中的 key 值

if (eldestKey != null) {

delegate.removeObject(eldestKey);

eldestKey= null;

}

}publicObject getObject(Object key) {

keyMap.get(key);//便于 该 Map 统计 get该key的次数

returndelegate.getObject(key);

}

实例类:PerpetualCache

说   明:这个比较简单,直接通过一个 HashMap 来存储缓存数据。所以没什么说的,直接看下面的 MemcachedCache 吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值