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)。
二、Executor框架
解析器:结合mybatis-spring框架,读取spring关于mybatis的配置文件。具体看是否开启缓存(这里指二级缓存),如果开启,生成的执行器为CachingExecutor。
动态代理:实现调用mapper接口的时候执行mybatis逻辑
执行器:执行缓存处理逻辑。在这里二级缓存和一级缓存有所区别。
BatchExcutor、ReuseExcutor、SimpleExcutor: 这几个就没什么好说的了,继承了 BaseExcutor 的实现其 doQuery、doUpdate 等方法,同样都是采用 JDBC 对数据库进行操作;三者区别在于,批量执行、重用 Statement 执行、普通方式执行。具体应用及场景在Mybatis 的文档上都有详细说明。
CachingExecutor: 二级缓存执行器。个人觉得这里设计的不错,灵活地使用 delegate机制。其委托执行的类是 BaseExcutor。 当无法从二级缓存获取数据时,同样需要从 DB 中进行查询,于是在这里可以直接委托给 BaseExcutor 进行查询。其大概流程为:
流程为: 从二级缓存中进行查询 -> [如果缓存中没有,委托给 BaseExecutor] -> 进入一级缓存中查询 -> [如果也没有] -> 则执行 JDBC 查询,其 query 代码如下:
- public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
- BoundSql boundSql = ms.getBoundSql(parameterObject);
- CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
- return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
- }
- public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
- Cache cache = ms.getCache();
- // 当前 Statement 是否启用了二级缓存
- if (cache != null) {
- this.flushCacheIfRequired(ms);
- if (ms.isUseCache() && resultHandler == null) {
- this.ensureNoOutParams(ms, parameterObject, boundSql);
- List<E> 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);
- }
- return list;
- }
- }
- // 没有启动用二级缓存,直接委托给 BaseExecutor 执行查询
- return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
- }
- public CacheKey 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 类中可以看到:
- private void cacheElement(XNode context) throws Exception {
- 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 方法实现:
- public Cache useNewCache(Class typeClass,
- Class evictionClass,
- Long flushInterval,
- Integer size,
- boolean readWrite,
- Properties props) {
- typeClass = valueOrDefault(typeClass, PerpetualCache.class);
- evictionClass = valueOrDefault(evictionClass, LruCache.class);
- // 这里构建 Cache 实例采用 Builder 模式,每一个 Namespace 生成一个 Cache 实例
- Cache cache = new CacheBuilder(currentNamespace)
- // Builder 前设置一些从XML中解析过来的参数
- .implementation(typeClass)
- .addDecorator(evictionClass)
- .clearInterval(flushInterval)
- .size(size)
- .readWrite(readWrite)
- .properties(props)
- // 再看下面的 build 方法实现
- .build();
- configuration.addCache(cache);
- currentCache = cache;
- return cache;
- }
- public Cache 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);
- // 返回最终缓存的责任链对象
- return cache;
- }
最终生成后的缓存实例对象结构:
可见,所有构建的缓存实例已经通过责任链方式将其串连在一起,各 Cache 各负其责、依次调用,直到缓存数据被 Put 至 基础缓存实例中存储。
Cache 实例解剖:
实例类:SynchronizedCache
说 明:用于控制 ReadWriteLock,避免并发时所产生的线程安全问题。
解 剖:
对于 Lock 机制来说,其分为 Read 和 Write 锁,其 Read 锁允许多个线程同时持有,而 Write 锁,一次能被一个线程持有,如果当 Write 锁没有释放,其它需要 Write 的线程只能等待其释放才能去持有。
其代码实现:
其具体原理可以看看 jdk concurrent 中的 ReadWriteLock 实现。
实例类:LoggingCache
说 明:用于日志记录处理,主要输出缓存命中率信息。
解 剖:
说到缓存命中信息的统计,只有在 get 的时候才需要统计命中率:
- public Object 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());
- }
- return value;
- }
实例类:SerializedCache
说 明:向缓存中 put 或 get 数据时的序列化及反序列化处理。
解 剖:
序列化在Java里面已经是最基础的东西了,这里也没有什么特殊之处:
- public void putObject(Object key, Object object) {
- // PO 类需要实现 Serializable 接口
- if (object == null || object instanceof Serializable) {
- delegate.putObject(key, serialize((Serializable) object));
- } else {
- throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
- }
- }
- public Object 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 = new ByteArrayOutputStream();
- ObjectOutputStream oos = new ObjectOutputStream(bos);
- oos.writeObject(value);
- oos.flush();
- oos.close();
- return bos.toByteArray();
- } catch (Exception var4) {
- throw new CacheException("Error serializing object. Cause: " + var4, var4);
- }
- }
- private Serializable deserialize(byte[] value) {
- try {
- ByteArrayInputStream bis = new ByteArrayInputStream(value);
- ObjectInputStream ois = new SerializedCache.CustomObjectInputStream(bis);
- Serializable result = (Serializable)ois.readObject();
- ois.close();
- return result;
- } catch (Exception var5) {
- throw new CacheException("Error deserializing object. Cause: " + var5, var5);
- }
- }
实例类:LruCache
说 明:最近最少使用的:移除最长时间不被使用的对象,基于LRU算法。
解 剖:
这里的 LRU 算法基于 LinkedHashMap 覆盖其 removeEldestEntry 方法实现。好象之前看过 XMemcached 的 LRU 算法也是这样实现的。
初始化 LinkedHashMap,默认为大小为 1024 个元素:
- public LruCache(Cache delegate) {
- this.delegate = delegate;
- setSize(1024); // 设置 map 默认大小
- }
- public void setSize(final int size) {
- // 设置其 capacity 为size, 其 factor 为.75F
- keyMap = new LinkedHashMap(size, .75F, true) {
- // 覆盖该方法,当每次往该map 中put 时数据时,如该方法返回 True,便移除该map中使用最少的Entry
- // 其参数 eldest 为当前最老的 Entry
- protected boolean removeEldestEntry(Map.Entry eldest) {
- boolean tooBig = size() > size;
- if (tooBig) {
- eldestKey = eldest.getKey(); //记录当前最老的缓存数据的 Key 值,因为要委托给下一个 Cache 实现删除
- }
- return tooBig;
- }
- };
- }
- public void putObject(Object key, Object value) {
- delegate.putObject(key, value);
- cycleKeyList(key); // 每次 put 后,调用移除最老的 key
- }
- // 看看当前实现是否有 eldestKey, 有的话就调用 removeObject ,将该key从cache中移除
- private void cycleKeyList(Object key) {
- keyMap.put(key, key); // 存储当前 put 到cache中的 key 值
- if (eldestKey != null) {
- delegate.removeObject(eldestKey);
- eldestKey = null;
- }
- }
- public Object getObject(Object key) {
- keyMap.get(key); // 便于 该 Map 统计 get该key的次数
- return delegate.getObject(key);
- }
实例类:PerpetualCache
说 明:这个比较简单,直接通过一个 HashMap 来存储缓存数据。所以没什么说的,直接看下面的 MemcachedCache 吧。
Spring + MySQL + Mybatis + Redis【二级缓存】执行流程分析的更多相关文章
- Spring Boot + Mybatis + Redis二级缓存开发指南
Spring Boot + Mybatis + Redis二级缓存开发指南 背景 Spring-Boot因其提供了各种开箱即用的插件,使得它成为了当今最为主流的Java Web开发框架之一.Mybat ...
- Spring + MySQL + Mybatis + Redis【二级缓存】
一.Redis环境 Redis 官网 :http://redis.io/ windows下载:https://github.com/dmajkic/redis/downloads 1.文件解压缩 2. ...
- Redis集成到Spring做mybatis做二级缓存
一.原理: 要缓存的 Java 对象必须实现 Serializable 接口,因为 Spring 会将对象先序列化再存入 Redis,比如本文中的 com.defonds.bdp.city.bean. ...
- springboot mybatis redis 二级缓存
前言 什么是mybatis二级缓存? 二级缓存是多个sqlsession共享的,其作用域是mapper的同一个namespace. 即,在不同的sqlsession中,相同的namespace下,相同 ...
- spring+springmvc+mybatis+redis实现缓存
先搭建好redis环境 需要的jar如下: jdbc.driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:330 ...
- 深入浅出Mybatis系列十-SQL执行流程分析(源码篇)
注:本文转载自南轲梦 注:博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 最近太忙了,一直没时间继续更新博客,今天忙里偷闲继续我的Mybatis学习之旅.在前 ...
- Mybatis的二级缓存、使用Redis做二级缓存
目录 什么是二级缓存? 1. 开启二级缓存 如何使用二级缓存: userCache和flushCache 2. 使用Redis实现二级缓存 如何使用 3. Redis二级缓存源码分析 什么是二级缓存? ...
- 使用Redis做MyBatis的二级缓存
使用Redis做MyBatis的二级缓存 通常为了减轻数据库的压力,我们会引入缓存.在Dao查询数据库之前,先去缓存中找是否有要找的数据,如果有则用缓存中的数据即可,就不用查询数据库了. 如果没有才去 ...
- Redis实现Mybatis的二级缓存
一.Mybatis的缓存 通大多数ORM层框架一样,Mybatis自然也提供了对一级缓存和二级缓存的支持.一下是一级缓存和二级缓存的作用于和定义. 1.一级缓存是SqlSession级别的缓存.在操作 ...
随机推荐
- Oracle transport tablespace
本来没想过发布这个文章,只是周边有一朋友工作中遇到合并数据库的情况,他是通过expdp提取出五个库对象,然后impdp到新库里面.我觉得这种方法特别耗时,尤其在数据量比较大的时候.这种时候我觉得采用表 ...
- May 25th 2017 Week 21st Thursday
Follow your heart, but take your brain with you. 跟随你的内心,但是请别失去理智. I was ever told that we should fol ...
- Python3基本数据类型(一、数字类型)
第一次写博客,感觉心情比较紧张,有一种要上台演讲的紧张感(虽然可能大概也许不会有人看).在此立个flag,以后每个学习阶段都要写一篇博客,来记录自己学习成长的这段日子.Fighting! 废话不多说, ...
- python入门9 条件语句
条件语句: if 条件为真: 执行语句块 (执行完结束不执行elif,else) elif 条件为真: 执行语句块 (执行完结束不执行else) else: 执行语句块 #coding:utf-8 # ...
- cocos2dx-打敌人游戏(一)
參照视频: http://v.youku.com/v_show/id_XNjk5MzExNDYw.html 1.參照前一篇文章创建新项目: http://blog.csdn.net/simakongc ...
- 如何使用react-redux
之前学习了react,也学习了redux,那么react-redux是什么呢?实际上他是一个第三方的模块,他可以帮助我们在react之中更加方便的使用redux.首先如果想用react-redux,先 ...
- 使用Sleep方法延迟时间
实现效果: 关键知识:(线程的定义) 实现代码: private void Form1_Load(object sender, EventArgs e) { Thread show = new Thr ...
- 使用TimeSpan对象获取时间间隔
实现效果: 关键知识: TimeSpan对象表是时间间隔或持续时间,两个DateTime对象相减,则会得到一个TimeSpan对象 使用其days ,hours,minutes等属性 实现代码: pr ...
- 如何解决“请考虑使用 app.config 将程序集“XXXXXXXX”从版本XXXX重新映射到版本XXXX”的问题
> 请考虑使用 app.config 将程序集“System.ValueTuple, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51”从版本“\ ...
- hibermate一对一关联
在hibernate.cfg.xml配置<mapping class="oneToOne.IDCard" />,以及实体类的get和set方法省略了. User类 @E ...