在应用程序和数据库都是单节点的情况下,合理使用缓存能够减少数据库IO,能显著提升系统性能。
但是现在大多是分布式环境,如果使用不当,则可能带来数据一致性问题。
MyBatis提供了一级缓存和二级缓存,其中一级缓存基于SqlSession实现,二级缓存基于Mapper实现。
一、MyBatis缓存的使用
MyBatis的缓存分为一级缓存和二级缓存,一级缓存默认是开启的,而且不能关闭,用户只能控制缓存的级别。
MyBatis核心开发人员解释:MyBatis的一些关键特性例如通过<association>
和<collection>
建立级联映射、避免循环引用circular references、加速重复嵌套查询等都是基于MyBatis一级缓存实现的,而且MyBatis结果集映射相关代码重度依赖CacheKey,所以目前MyBatis不支持关闭一级缓存。
MyBatis通过参数localCacheScope控制一级缓存的级别:
localCacheScope=SESSION时,缓存对整个SqlSession有效,只有执行DML语句更新语句时,缓存才会被清除。
localCacheScope=STATEMENT时,缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。
MyBatis二级缓存的使用步骤:
1.在MyBatis主配置文件中指定cacheEnabled属性值为true。
2.在MyBatis Mapper配置文件中,配置缓存策略、缓存刷新频率、缓存的容量等属性,
3.在配置Mapper时,通过useCache属性指定Mapper执行时是否使用缓存。另外,还可以通过flushCache属性指定Mapper执行后是否刷新缓存,
通过上面的配置,MyBatis的二级缓存就可以生效了。执行查询操作时,查询结果会缓存到二级缓存中,执行更新操作后,二级缓存会被清空。
二、MyBatis缓存实现类
MyBatis的缓存基于JVM堆内存实现,即所有的缓存数据都存放在Java对象中。
MyBatis通过Cache接口定义缓存对象的行为:
getId():获取缓存的Id,通常情况下缓存的Id为Mapper的命名空间名称。
putObject():用于将一个Java对象添加到缓存中,方法有两个参数,第一个参数为缓存的Key,即CacheKey的实例;第二个参数为需要缓存的对象。
getObject():获取缓存Key对应的缓存对象。
removeObject():将一个对象从缓存中移除。
clear():清空缓存。
MyBatis中的缓存类采用装饰器模式设计:
Cache接口有一个基本的实现类,即PerpetualCache类,除此提供了很多缓存的装饰器类来增强PerpetualCache类的功能,
MyBatis提供了一个CacheBuilder类,通过生成器模式创建缓存对象。
PerpetualCache类的实现比较简单,通过一个HashMap实例存放缓存对象。它重写了Object类的equals()方法和hashCode()方法,
以缓存对象的Id作为因子生成hashCode,所以当两个缓存对象的Id相同时,即认为缓存对象相同。
缓存装饰器类:
BlockingCache:阻塞版本的缓存装饰器,能够保证同一时间只有一个线程到缓存中查找指定的Key对应的数据。
FifoCache:先入先出缓存装饰器,FifoCache内部有一个维护具有长度限制的Key键值链表LinkedList实例和一个被装饰的缓存对象,Key值链表主要是维护Key的FIFO顺序,而缓存存储和获取则交给被装饰的缓存对象来完成。
LoggingCache:为缓存增加日志输出功能,记录缓存的请求次数和命中次数,通过日志输出缓存命中率。
LruCache:最近最少使用的缓存装饰器,当缓存容量满了之后,使用LRU算法淘汰最近最少使用的Key和Value。LruCache中通过重写LinkedHashMap类的removeEldestEntry()方法获取最近最少使用的Key值,将Key值保存在LruCache类的eldestKey属性中,然后在缓存中添加对象时,淘汰eldestKey对应的Value值。具体实现细节读者可参考LruCache类的源码。
ScheduledCache:自动刷新缓存装饰器,当操作缓存对象时,如果当前时间与上次清空缓存的时间间隔大于指定的时间间隔,则清空缓存。清空缓存的动作由getObject()、putObject()、removeObject()等方法触发。
SerializedCache:序列化缓存装饰器,向缓存中添加对象时,对添加的对象进行序列化处理,从缓存中取出对象时,进行反序列化处理。
SoftCache:软引用缓存装饰器,SoftCache内部维护了一个缓存对象的强引用队列和软引用队列,缓存以软引用的方式添加到缓存中,并将软引用添加到队列中,获取缓存对象时,如果对象已经被回收,则移除Key,如果未被回收,则将对象添加到强引用队列中,避免被回收,如果强引用队列已经满了,则移除最早入队列的对象的引用。
SynchronizedCache:线程安全缓存装饰器,SynchronizedCache的实现比较简单,为了保证线程安全,对操作缓存的方法使用synchronized关键字修饰。
TransactionalCache:事务缓存装饰器,该缓存与其他缓存的不同之处在于,TransactionalCache增加了两个方法,即commit()和rollback()。当写入缓存时,只有调用commit()方法后,缓存对象才会真正添加到TransactionalCache对象中,如果调用了rollback()方法,写入操作将被回滚。
WeakCache:弱引用缓存装饰器,功能和SoftCache类似,只是使用不同的引用类型。
三、MyBatis一级缓存实现原理
MyBatis的一级缓存是SqlSession级别的缓存:
SqlSession只是提供了面向用户的API,真正执行SQL操作的是Executor组件,
所以一级缓存相关的逻辑都在BaseExecutor类中,使用PerpetualCache实例实现。
在BaseExecutor类中维护了两个PerpetualCache属性(在BaseExecutor构造方法中进行初始化):
localCache属性用于缓存MyBatis查询结果,
localOutputParameterCache属性用于缓存存储过程调用结果。
MyBatis通过CacheKey对象来描述缓存的Key值,CacheKey对象通过BaseExecutor类的createCacheKey()方法创建,与下列因素有关:
1.Mapper的Id,即Mapper命名空间与<select|update|insert|delete>标签的Id组成的全局限定名。
2.查询结果的偏移量及查询的条数。
3.具体的SQL语句及SQL语句中需要传递的所有参数。
4.MyBatis主配置文件中,通过<environment>
标签配置的环境信息对应的Id属性值。
执行两次查询时,只有上面的信息完全相同时,才会认为两次查询执行的是相同的SQL语句,缓存才会生效。
BaseExecutor的query()方法执行逻辑:
首先根据缓存Key从localCache属性中查找是否有缓存对象,如果查找不到,调用queryFromDatabase()方法从数据库中获取数据,然后将数据写入localCache对象中。
如果localCache中缓存了本次查询的结果,则直接从缓存中获取。
注意:
localCacheScope=STATEMENT时,缓存仅对当前执行的语句有效,都会调用clearLocalCache()方法清空缓存。
localCacheScope=SESSION时,缓存对整个SqlSession有效,只有执行DML语句更新语句时,缓存才会被清除。
在分布式环境下,一定要将MyBatis的localCacheScope属性设置为STATEMENT,
避免其他应用节点执行SQL更新语句后,本节点缓存得不到刷新而导致的数据一致性问题。
下面看下BaseExecutor类的update()方法:
1.首先会调用clearLocalCache()方法清空缓存
2.调用doUpdate()方法完成更新操作
四、 MyBatis二级缓存实现原理
MyBatis二级缓存在默认关闭,通过设置cacheEnabled参数值为true来开启。
SqlSession将执行Mapper的逻辑委托给Executor组件完成,而Executor接口有几种不同的实现,分别为SimpleExecutor、BatchExecutor、ReuseExecutor。
另外,还有一个比较特殊的CachingExecutor,用到了装饰器模式,在其他几种Executor的基础上增加了二级缓存功能。
Configuration类提供了一个工厂方法newExecutor(),方法返回一个Executor对象,方法大概逻辑:
根据defaultExecutorType参数指定的Executor类型创建对应的Executor实例,
如果cacheEnabled属性值为true开启了二级缓存,则使用CachingExecutor对普通的Executor对象进行装饰。
CachingExecutor类:维护了一个TransactionalCacheManager实例,用于管理所有的二级缓存对象。
TransactionalCacheManager类:通过一个HashMap对象维护所有二级缓存实例对应的TransactionalCache对象。
getObject()方法和putObject()方法中都会调用getTransactionalCache()方法获取二级缓存对象对应的TransactionalCache对象,然后对TransactionalCache对象进行操作。
getTransactionalCache()方法中:
1.首先从HashMap对象中获取二级缓存对象对应的TransactionalCache对象,
2.如果获取不到,则创建新的TransactionalCache对象添加到HashMap对象中。
介绍二级缓存的工作机制:
以查询操作为例,即CachingExecutor的query()方法:
1.首先调用createCacheKey()方法创建缓存Key对象,
2.然后调用MappedStatement对象的getCache()方法获取MappedStatement对象中维护的二级缓存对象。
3.然后尝试从二级缓存对象中获取结果,如果获取不到,则调用目标Executor对象的query()方法从数据库获取数据,再将数据添加到二级缓存中。
以更新操作为例,即CachingExecutor的update()方法:
方法中会调用flushCacheIfRequired()方法确定是否需要刷新缓存:同一命名空间下的二级缓存将会被清空。
会根据<select|update|delete|insert>标签的flushCache属性来决定是否清空缓存,
标签的flushCache属性值默认为false,而<update|delete|insert>标签的flushCache属性值默认为true。
介绍MappedStatement对象创建过程中二级缓存实例的创建:
XMLMapperBuilder在解析Mapper配置时会调用cacheElement()方法解析<cache>
标签,cacheElement()方法的逻辑:
1.获取<cache>
标签的所有属性信息
2.调用MapperBuilderAssistant对象的userNewCache()方法创建二级缓存实例
3.然后通过MapperBuilderAssistant的currentCache属性保存二级缓存对象的引用。
在调用MapperBuilderAssistant对象的addMappedStatement()方法创建MappedStatement对象时会将当前命名空间对应的二级缓存对象的引用添加到MappedStatement对象中。
即:
五、MyBatis使用Redis缓存
MyBatis除了提供内置的一级缓存和二级缓存外,还支持使用第三方缓存例如Redis、Ehcache作为二级缓存。
下面学习下MyBatis中使用Redis作为二级缓存,步骤:
1.引入mybatis-redis模块的依赖,如果项目通过Maven构建,只需要向pom.xml文件中添加如下内容:
2.在Mapper的XML配置文件中添加缓存配置:
3.在classpath下新增redis.properties文件,配置Redis的连接信息。
简单了解下mybatis-redis模块的实现:模块提供了一个比较核心的缓存实现类,即RedisCache类,该类实现了Cache接口,使用Jedis客户端操作Redis,在RedisCache构造方法中建立与Redis的连接:
在RedisCache构造方法中:
1.首先获取RedisConfigurationBuilder对象,将redis.properties文件中的配置信息转换为RedisConfig对象,RedisConfig类是描述Redis配置信息的Java Bean。
2.接着创建JedisPool对象,通过JedisPool对象与Redis服务器建立连接。
RedisCache使用Redis的Hash数据结构存放缓存数据,看下其核心方法。
putObject()方法:对Java对象进行序列化,然后将序列化后的信息存放在Redis中。
mybatis-redis模块提供了两种序列化策略,即JDK内置的序列化机制和第三方序列化框架Kryo,可以在redis.properties文件中配置使用哪种序列化方式。
getObject()方法:根据Key获取序列化的对象信息,再进行反序列化操作。
注意:
使用Redis作为二级缓存,需要通过<cache>
标签的type属性指定缓存实现类为org.mybatis.caches.redis.RedisCache。
MyBatis启动时会解析Mapper配置信息,为每个命名空间创建对应的RedisCache实例,由于JedisPool实例是RedisCache类的静态属性,因此JedisPool实例是所有RedisCache对象共享的。
除了Redis外,MyBatis还提供了整合其他缓存的适配器。例如,ehcache-cache项目用于整合EhCache缓存,oscache-cache项目用于整合OSCache缓存,memcached-cache项目用于整合Memcached缓存。