通常为了减轻数据库的压力,我们会引入缓存。在Dao查询数据库之前,先去缓存中找是否有要找的数据,如果有则用缓存中的数据即可,就不用查询数据库了。如果没有才去数据库中查找。这样就能分担一下数据库的压力。另外,为了让缓存中的数据与数据库同步,我们应该在该数据发生变化的地方加入更新缓存的逻辑代码。这样无形之中增加了工作量,同时也是一种对原有代码的入侵。这对于有着代码洁癖的程序员来说,无疑是一种伤害。
MyBatis框架早就考虑到了这些问题,因此MyBatis提供了自定义的二级缓存概念,方便引入我们自己的缓存机制,而不用更改原有的业务逻辑。下面就让我们了解一下MyBatis的缓存机制。
正如大多数持久层框架一样,MyBatis 同样提供了一级缓存和二级缓存的支持;
一级缓存基于 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(装饰器缓存):
一般缓存框架的数据结构基本上都是 Key-Value 方式存储,MyBatis 对于其 Key 的生成采取规则为:
[hashcode : checksum : mappedStementId : offset : limit : executeSql : queryParams]。
对于并发 Read/Write 时缓存数据的同步问题,MyBatis 默认基于 JDK/concurrent中的ReadWriteLock,使用 ReentrantReadWriteLock 的实现,从而通过 Lock 机制防止在并发 Write Cache 过程中线程安全问题。
接下来将结合 MyBatis 序列图进行源码分析。在分析其Cache前,先看看其整个处理过程。
Executor:执行器接口。也是最终执行数据获取及更新的实例。其结构如下:
BaseExecutor:基础执行器抽象类。实现一些通用方法,如createCacheKey之类的。并采用模板模式将具体的数据库操作逻辑交由子类实现。另外,可以看到变量localCache:PerpetualCache,在该类采用perpetualCache实现基于map存储的一级缓存,其query方法如下:
BatchExcutor、ReuseExcutor、SimpleExcutor:这三个就是简单的继承了BaseExcutor,实现了doQuery、doUpdate等发放,同样都是采用了JDBC对数据库进行操作;三者的区别在于,批量执行、重用Statement执行、普通方法执行。具体应用及长江在mybatis文档上都有详细的说明。
CachingExecutor:二级缓存执行器。其中使用了静态代理模式,当二级缓存中没有数据的时候,就使用BaseExecutor做代理,进行下一步执行。具体代码如下:
像之前所说,Cache是一个缓存接口,运行时用到的其实是在解析mapper文件的时候根据配置文件生成的对应Cahce实现类。另外这个实现类的构造过程使用了建造者(Builder)模式。在build的过程中,将所有设计到的cache放入基础缓存中,并使用装饰器模式将cache进行装饰。具体设计如下:
1. 从配置文件中获取节点,将配置信息提取出来初始化生成Cache2. useNewCache方法中使用了建造者(Builder)模式,将从配置文件中读取出来的各个元素组装起来。其中最主要的是build方法。
总体上看,我们可以把MyBatis关于缓存的这一部分分为三个部分:
添加实现Cache接口的实现类。重写方法会在查询数据库前后调用,查询、更新、删除、创建缓存需要在这几个方法中实现。值得注意的是,getObject方法,当返回的是null时,就会接着查询。如果不为null,则返回,不再查询了。
/*** 使用redis做mybatis二级缓存* @Description* @file_name MyBatisRedisCache.java* @time 2016-07-26 下午4:49:13* @author muxiaocao*/public class MyBatisRedisCache implements Cache{@Value("#{config['redis.ip']}")protected String redisIp;@Value("#{config['redis.port']}")protected Integer redisPort;private static Log logger = LogFactory.getLog(MyBatisRedisCache.class);private Jedis redisClient = createClient();private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();private String id;public MyBatisRedisCache(final String id) { if (id == null) { throw new IllegalArgumentException("缓存没有初始化id"); } logger.debug("==================MyBatisRedisCache:id=" + id); this.id = id;}@Overridepublic String getId() { return this.id;}@Overridepublic int getSize() { return Integer.valueOf(redisClient.dbSize().toString());}@Overridepublic void putObject(Object key, Object value) { logger.debug("==================pubObject:" + key + "=" + value); redisClient.set(SerializeUtil.serialize(key),SerializeUtil.serialize(value));}@Overridepublic Object getObject(Object key) { Object value = SerializeUtil.unserialize(redisClient.get(SerializeUtil.serialize(key.toString()))); logger.debug("==================getObjec:" + key + "=" + value); return value;}@Overridepublic Object removeObject(Object key) { return redisClient.expire(SerializeUtil.serialize(key.toString()), 0);}@Overridepublic void clear() { redisClient.flushDB();}@Overridepublic ReadWriteLock getReadWriteLock() { return readWriteLock;}private Jedis createClient() { try { RedisUtil.initRedis(redisIp); return RedisUtil.getRedis(); } catch (Exception e) { e.printStackTrace(); } throw new RuntimeException("初始化redis错误");}}
MyBatis的整体思路其实很简单,但是跟着源码发现,一个好的框架需要考虑的问题很多,从可扩展性、功能维护等角度考虑,如果没有一个好的设计思路会把代码设计的很乱很乱。MyBatis充分利用了动态代理、建造模式、装饰器模式,使他们结合在一起,让整个框架变得简单易用,其实是很难得的。
这就好比读一本书,需要先读厚再度薄一样,框架的设计最开始需要考虑到各种各样的问题,然后把一个简单的思路变得复杂。然后通过合理的设计,将复杂的问题简单的设计出来,使得代码很整洁,易于维护和读,这才是一个好的框架应该有的样子。
真的很感谢能有这么一个机会研究一下mybatis,并从中学到了许多。希望有朝一日,也能写出像mybatis一样的框架。
在这里很非常感谢http://denger.iteye.com/blog/1126423/。
注意:转载请标明,转自itboy-木小草。
尊重原创,尊重技术。
联系客服