文章目录
1、缓存概述
- 缓存的概念:
缓存是应用程序和物理数据源之间数据存储区,其作用是为了降低应用程序对物理数据源访问的频次,从而提高应用程序的运行性能;
缓存内的数据是对物理数据源的复制,应用程序在运行时从缓存中读取数据,在特定的时刻或事件中会同步缓存和物理数据源的数据(保持数据的同步);
- 计算机关于数据读取的一些硬件:
cpu【一级缓存,二级缓存】
内存【缓存】
硬盘【永久数据】
- 缓存的应用场景:
比如说,游戏中的榜单,对整个玩家数据进行统计。
优化方案:将榜单统计数据储存在内存中,玩家请求榜单排行操作,程序直接从内存中获取,可以设置为五分钟重新读取一次,此时的数据也许并不准确;
短信验证码:注册账号,或者找回密码操作中都会收到一个验证码。验证码储存在缓存中,并在一定的时间之后验证码失效;
新闻信息:第一个人看大的新闻和第1000万看到的是一样的,除非新闻修改了,因此新闻也可以存储在缓存中,动态页面静态化,生成的是html;
MyBatis
是常见的Java数据库访问层框架。在日常工作中,开发人员多数情况下是使用MyBatis
的默认缓存配置,但是MyBatis
缓存机制有一些不足之处,在使用中容易引起脏数据,形成一些潜在的隐患;- 【缓存的意义】
操作数据库的速率快、提升系统性能、减少数据库压力;
常用的缓存框架有:ehcache(hibernate推荐)
、memcached(键值对)
、redis(支持数据类型,数据的运算);
2、memcached使用详解
Memcached
是国外社区网站 LiveJournal 的开发团队开发的一套高性能的分布式内存对象缓存服务器。- 它将所有的数据统统保存在内存中,在内存中会维护一个巨大的 hash表,支持任意存储类型的数据,很多网站通过
Memcached
提高网站的访问速度,尤其是对于大型的需要频繁访问的网站,减少查询效率,提高查询速度; - 缓存的应用系统:
计算机体系存储系统模型扩展到应用也是一样,应用需要数据,数据哪里来?缓存(更快的存储)———>DB(较慢的存储),它们的工作流程大致如下图所示:
Memcache架构
服务端:Memcached 服务端,通过C语言编写而成;
客户端:Memcached API客户端,可以通过任何语言编写,如php、py等;
- 特点:
1、为了提高性能,
memcached
中保存的数据都存储在memcached
内置的内存存储空间中。由于数据仅存在于内存中,因此重启memcached
、重启操作系统会导致全部数据消失;
2、基于
libevent
的事件处理:libevent是个程序库,它将Linux的epoll、BSD类操作系统的kqueue等事件处理功能封装成统一的接口。即使对服务器的连接数增加,也能发挥I/O
的性能。memcached使用这个libevent库,因此能在Linux、BSD、Solaris等操作系统上发挥其高性能;
3、简单
key/value
存储:服务器不关心数据本身的意义及结构,只要是可序列化数据即可。存储项由"键、过期时间、可选的标志及数据"四个部分组成;
4、功能的实现一半依赖于客户端,一半基于服务器端:客户负责发送存储项至服务器端、从服务端获取数据以及无法连接至服务器时采用相应的动作;服务端负责接收、存储数据,并负责数据项的超时过期;
- 运行架构:
Memcached缓存存储策略
使用内存缓存策略:Slab Allocation机制;
【Slab Allocation机制的基本原理】:按照预先规定的大小,将分配的内存分割成特定长度的块(chunk),并把尺寸相同的块分成组,以完全解决内存碎片问题。但由于分配的是特定长度的内存,因此无法有效利用分配的内存。比如将100字节的数据缓存到128字节的chunk
中,剩余的28字节就浪费了;按照预先规定的大小,将分配的内存分割成特定长度的内存块(chunk),再把尺寸相同的内存块分层组(chunk集合)
,这些内存不会释放,可以反复利用;
- Slab Allocation 机制角色:
1.Chunk为固定大小的内存空间,默认为96Byte。
2.page对应实际的物理空间,1个page为1M。
3.同样大小的chunk又称为slab。
- 客户端选择slab机制:
下面说明memcached
如何针对客户端发送的数据选择slab并缓存到chunk
中。memcached
根据收到的数据的大小,选择最适合数据大小的slab。 memcached
中保存着slab内空闲chunk
的列表,根据该列表选择chunk
, 然后将数据缓存于其中。如下图;
内存释放机制
Laxzy Expiration:
Memcached每个被存取的对象都有唯一的标识符key,存取操作均通过key进行,例如可以把后端数据库中的select操作提取出来,然后对相应的SQL进行hash计算得出key,然后以这个key在memcached中查找数据,如果数据不存在,说明其尚未被写入缓存中,并设置一个失效时间(比如1小时),在失效时间内的数据都是从缓存中提取,这样就有效地减少了数据库的压力;
Least Recently Used(LRU):
删除“最近最少使用”的记录的机制。当memcached的内存空间不足时,从最近未被使用的记录中搜索,并将其空间分配给新的记录。-M 参数禁止LRU功能,内存用尽时,memcached会返回错误,不建议使用memcached -M -m 1024;
Memcached优缺点
【优点】
1.读写性能优异,特别是高并发时和文件缓存比有明显优势;
2.memcached组建支持集群,并且是自动管理负载均衡;
3.开源,占用资源小,协议简单的软件,实现了数据库和web之间的数据缓存功能,减少数据库的检索次数,减少数据库的I/O,解决了架构数据库端的压力;
4.存储方式:内置于内存存储方式,存取的效率高,执行的速度快;
【缺点】
1.缓存空间有限:据说一台电脑的mem缓存开到2g以上会出现不稳定,数据无故丢失的现象;
2.掉电丢失数据:由于是把数据放在内存里的,所有一旦机器掉电,数据也就全部丢失了。
一般建议:而mem则适合放一些频繁更改的数据,比如可以把session数据放进mem;
安装
- 首先下载Win下的Memcached,解压到指定目录。
- memcached.exe -d install 安装memcached服务
- 然后通过Memcached start memcached就启动了。
- 常用命令:
-p 监听的端口
-l 连接的IP地址, 默认是本机
-d start 启动memcached服务
-d restart 重起memcached服务
-d stop|shutdown 关闭正在运行的memcached服务
-d install 安装memcached服务
-d uninstall 卸载memcached服务
-u 以的身份运行 (仅在以root运行的时候有效)
-m 最大内存使用,单位MB。默认64MB
-M 内存耗尽时返回错误,而不是删除项
-c 最大同时连接数,默认是1024
-f 块大小增长因子,默认是1.25
-n 最小分配空间,key+value+flags默认是48
-h 显示帮助
-
官网的地址为:
http://memcached.org/
; -
Java中
memcached
客户端程序有很多,这里我们使用xmemcached
;
——首先导入项目依赖:
<!-- https://mvnrepository.com/artifact/com.googlecode.xmemcached/xmemcached -->
<dependency>
<groupId>com.googlecode.xmemcached</groupId>
<artifactId>xmemcached</artifactId>
<version>2.4.6</version>
</dependency>
——编写Java代码:
import net.rubyeye.xmemcached.MemcachedClient;
import net.rubyeye.xmemcached.MemcachedClientBuilder;
import net.rubyeye.xmemcached.XMemcachedClientBuilder;
import net.rubyeye.xmemcached.utils.AddrUtil;
public class memcacheTest {
public static void main(String[] args) {
MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses("127.0.0.1:11211"));
MemcachedClient memcachedClient;
try {
memcachedClient = builder.build();
//第二个参数为存活时间,0 是不过期,单位为 秒 //
//memcachedClient.set("hello3", 100, "Hello,xmemcached");
String value = memcachedClient.get("hello3");
System.out.println("hello=" + value);
// memcachedClient.delete("hello");
// value = memcachedClient.get("hello");
// System.out.println("hello=" + value);
memcachedClient.shutdown();
} catch (Exception e) {
System.err.println("MemcachedClient operation fail");
e.printStackTrace();
}
}
}
- 客户端工具treeNMS,是BS架构的,用于查看缓存中的数据:
完成上面的一系列操作,需要同时开启memcached的客户端与服务端,并在pom.xml中导入xmemcached的jar包,最后在浏览器中输入客户端网址,进行查看此时缓存中的内容;
- 注意:
1、打开memcached的方式有两种,第一种是直接双击打开,第二种是使用命令的方式:telnet 127.0.0.1 11211
,11211
是memcached的端口;
2、服务端是一次性的,如果关闭服务端,缓存在里面的数据就都消失了;
3、一级缓存
- 参考原文链接:
https://tech.meituan.com/2018/01/19/mybatis-cache.html
- 在应用程序过程中,我们有可能再一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能;具体执行过程如下图所示:
- 每个SqlSession中持有了Excutor,每个Excutor中有一个LocalCache;当用户发起查询时,MyBatis根据当前执行的语句生成MapperStatement,在LocalCache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户;具体实现类关系图如下图所示:
- 一级缓存配置:
开发者需要在MyBatis的配置文件中添加语句:
<setting name="localCacheScope" value="SESSION"/>
就可以使用一级缓存,共有两个选项,SESSION或者STATEMENT,默认是SESSION级别,即就是在一个Mybatis会话中执行的所有语句,都会共享这一个缓存;一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个STATEMENT有效,这里一定要注意大小写;
一级缓存是SqlSession级别的缓存。在操作数据库时需要构造 sqlSession对象,在对象中有一个(内存区域)数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。
一级缓存的作用域是同一个SqlSession,在同一个sqlSession中两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。当一个sqlSession结束后该sqlSession中的一级缓存也就不存在了,Mybatis默认开启一级缓存。
- 一级缓存实验:
1、mapper执行多次相同的条件查询,结果是缓存的
@Test
public void select1(){
AccountMapper mapper = sqlSession.getMapper(AccountMapper.class);
System.out.println(JSON.toJSONString(mapper.selectByPrimaryKey(2), true));
System.out.println("----------------------------------------");
System.out.println(JSON.toJSONString(mapper.selectByPrimaryKey(2), true));
}
两次查询采用的是一个SqlSession和一个Mapper文件,只进行了一次查询;
2、不同的SqlSession,查询同一个条件
@Test
public void select2() {
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
AccountMapper mapper1 = sqlSession1.getMapper(AccountMapper.class);
AccountMapper mapper2 = sqlSession2.getMapper(AccountMapper.class);
System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));
System.out.println("----------------------------------------");
System.out.println(JSON.toJSONString(mapper2.selectByPrimaryKey(2), true));
}
最终结果是进行了两次查询;
3、同一个SqlSession,获取多次mapper,mapper执行多次相同的条件查询
@Test
public void select3() {
SqlSession sqlSession1 = sqlSessionFactory.openSession();
AccountMapper mapper1 = sqlSession1.getMapper(AccountMapper.class);
AccountMapper mapper2 = sqlSession1.getMapper(AccountMapper.class);
System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));
System.out.println("----------------------------------------");
System.out.println(JSON.toJSONString(mapper2.selectByPrimaryKey(2), true));
}
这里查询了一次;
4、不同查询条件,相同SqlSession
@Test
public void select3() {
SqlSession sqlSession1 = sqlSessionFactory.openSession();
AccountMapper mapper1 = sqlSession1.getMapper(AccountMapper.class);
System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));
System.out.println("----------------------------------------");
System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(3), true));
}
这里很明显查询条件都不同,查询了两次;
5、当调用SqlSession的增删改查方法,会清空缓存数据,这样做是为了防止数据的脏读
@Test
public void select4() {
SqlSession sqlSession = sqlSessionFactory.openSession();
AccountMapper mapper = sqlSession.getMapper(AccountMapper.class);
System.out.println(JSON.toJSONString(mapper.selectByPrimaryKey(2), true));
System.out.println("----------------------------------------");
Account account = new Account();
account.setaNikename("mybatis缓存");
account.setAname("Java");
account.setApass("mybatis");
mapper.insert(account);
System.out.println("插入数据完成!");
sqlSession.commit();
System.out.println("----------------------------------------");
System.out.println(JSON.toJSONString(mapper.selectByPrimaryKey(2), true));
}
6、不同的SqlSession,a在查询,b修改数据,a再次查询,数据就会不一致(a读取缓存)
@Test
public void select5() {
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
AccountMapper mapper1 = sqlSession1.getMapper(AccountMapper.class);
AccountMapper mapper2 = sqlSession2.getMapper(AccountMapper.class);
//a查询
System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));
System.out.println("----------------------------------------");
//b修改
Account account = new Account();
account.setaNikename("mybatis缓存");
account.setAname("Java");
account.setApass("mybatis");
account.setAid(2);
mapper2.updateByPrimaryKey(account);
System.out.println("修改数据完成!");
sqlSession2.commit();
System.out.println("----------------------------------------");
System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));
}
7、可以手动清除一级缓存
@Test
public void select6() {
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
AccountMapper mapper1 = sqlSession1.getMapper(AccountMapper.class);
AccountMapper mapper2 = sqlSession2.getMapper(AccountMapper.class);
//a查询
System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));
System.out.println("----------------------------------------");
//b修改
Account account = new Account();
account.setaNikename("mybatis缓存");
account.setAname("Java");
account.setApass("mybatis");
account.setAid(2);
mapper2.updateByPrimaryKey(account);
System.out.println("修改数据完成!");
sqlSession2.commit();
System.out.println("----------------------------------------");
//手动清除缓存
sqlSession1.clearCache();
System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));
}
清除缓存之后,进行了第二次的查询;
8、flushCache=true
清空缓存,每一次都是物理查询
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap" flushCache="true">
select
<include refid="Base_Column_List" />
from account account
where account.aid = #{aid,jdbcType=INTEGER}
</select>
- 小结一级缓存:
1、一级缓存确实存在,同一个sqlsession获取一个代理接口对象,不论mapper接口的查询方法执行几次,都会缓存查询结果的;当执行了sqlsession的增删改方法或者调用sqlsession.clearCache()方法或者<select>标签
上的flushCache="true"
都会清空缓存;
2、一级缓存有什么意义?
在同一个sqlsession中执行相同的多次查询条件,查询的结果会从缓存直接返回,降低物理查询的频次,提升性能;
3、sqlsession的作用域在方法块中,mapper接口的代理方法中,都持有一个sqlsession;
4、缓存有缓存命中率之说,缓存的命中率越高,缓存越有意义;
4、一级缓存源码分析
- 一级缓存执行的时序图,如下图所示:
SqlSession:对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节;默认实现类是DefaultSqlSession
;
Excutor:SqlSession
向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给Excutor;
如下图所示,执行器Excutor有若干实现类,为Excutor赋予了不同的能力:
在一级缓存的源码分析中,主要学习BaeExecutor
的内部实现;
BaseExcutor:BaseExcutor
是一个实现了Excutor接口的抽象类,定义若干抽象方法,在执行的时候,把具体的操作委托给子类进行执行;
protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException;
在一级缓存的介绍中提到了对Local Cache
的查询和写入是在Excutor
内部完成的,其实Local Cache
是BaseExcutor
内部的一个成员变量:
public abstract class BaseExecutor implements Executor {
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
protected PerpetualCache localCache;
CaChe:MyBatis中的Cache接口,提供了和缓存相关的最基本的操作,如下图所示:
有若干实现类,使用装饰者模式互相组装,提供丰富的操控缓存的能力,部分实现类如下图所示:
BaseExcutor
成员变量之一的PerpetualCache
,是对Cache接口最基本的实现,其实现非常简单,内部持有HashMap,对一级缓存的操作实则是对HashMap的操作,如下面的代码所示:
public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
在阅读相关类代码之后,从源代码层面对一级缓存工作中涉及到相关代码:
为执行和数据库的交互,首先需要初始化SqlSession,通过DefaultSqlSessionFactory
开启SqlSession
;
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
............
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
}
在初始化SqlSesion
时,会使用Configuration
类创建一个全新的Executor
,作为DefaultSqlSession
构造函数的参数,创建Executor代码如下所示:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
// 尤其可以注意这里,如果二级缓存开关开启的话,是使用CahingExecutor装饰BaseExecutor的子类
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
SqlSession
创建完毕后,根据Statment
的不同类型,会进入SqlSession
的不同方法中,如果是Select
语句的话,最后会执行到SqlSession
的selectList
,代码如下所示:
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}
SqlSession把具体的查询职责委托给了Executor。如果只开启了一级缓存的话,首先会进入BaseExecutor的query方法。代码如下所示:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
在上述代码中,会先根据传入的参数生成CacheKey,进入该方法查看CacheKey是如何生成的,代码如下所示:
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
//后面是update了sql中带的参数
cacheKey.update(value);
在上述的代码中,将MappedStatement的 Id、SQL的offset、SQL的limit、SQL本身以及SQL中的参数
传入了CacheKey这个类,最终构成CacheKey。以下是这个类的内部结构:
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
private int multiplier;
private int hashcode;
private long checksum;
private int count;
private List<Object> updateList;
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<Object>();
}
首先是成员变量和构造函数,有一个初始的hachcode和乘数,同时维护了一个内部的updatelist。在CacheKey的update方法中,会进行一个hashcode和checksum的计算,同时把传入的参数添加进updatelist中。如下代码所示:
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
同时重写了CacheKey的equals方法,代码如下所示:
@Override
public boolean equals(Object object) {
.............
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}
除去hashcode、checksum和count的比较外,只要updatelist中的元素一一对应相等,那么就可以认为是CacheKey相等。只要两条SQL的下列五个值相同,即可以认为是相同的SQL。
Statement Id + Offset + Limmit + Sql + Params
BaseExecutor的query方法继续往下走,代码如下所示:
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 这个主要是处理存储过程用的。
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。
在query方法执行的最后,会判断一级缓存级别是否是STATEMENT级别,如果是的话,就清空缓存,这也就是STATEMENT级别的一级缓存无法共享localCache的原因。代码如下所示:
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
在源码分析的最后,我们确认一下,如果是insert/delete/update方法,缓存就会刷新的原因。
SqlSession的insert方法和delete方法,都会统一走update的流程,代码如下所示:
@Override
public int insert(String statement, Object parameter) {
return update(statement, parameter);
}
@Override
public int delete(String statement) {
return update(statement, null);
}
update方法也是委托给了Executor执行。BaseExecutor的执行方法如下所示:
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
每次执行update前都会清空localCache。
至此,一级缓存的工作流程讲解以及源码分析完毕。
5、二级缓存
- 在上文提到的一级缓存中,最大的共享范围就是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存;开启二级缓存之后,会使用CachingExcutor装饰Excutor,进入一级缓存的查询流程前,先CachingExecutor进行二级缓存的查询,具体的工作流程为:
-
二级缓存开启之后,同一个namespace下的所有操作语句,都影响着一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量;
-
当开启缓存之后,数据的查询的执行流程就是
二级缓存——>一级缓存——>数据库
; -
二级缓存配置:
要正确的使用二级缓存,需要完成以下的配置:
1、
<setting name="cacheEnabled" value="true"/>
2、
在MyBatis
的映射文件XML中配置cache
或者cache-ref
;
cache标签用于声明这个namespace使用二级缓存,并且可以自定义配置;
<cache/>
type:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。
eviction:定义回收的策略,常见的有FIFO,LRU。
flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
size:最多缓存对象的个数。
readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
blocking:若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
- cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。
<cache-ref namespace="mapper.StudentMapper"/>
- 二级缓存实验:
1、不同的SqlSession,没有提交的时候不贡献缓存
@Test
public void select1() throws InterruptedException {
AccountMapper mapper1 = sqlSessionFactory.openSession().getMapper(AccountMapper.class);
AccountMapper mapper2 = sqlSessionFactory.openSession().getMapper(AccountMapper.class);
System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));
Thread.sleep(1000);
System.out.println("----------------------------------------");
System.out.println(JSON.toJSONString(mapper2.selectByPrimaryKey(2), true));
}
这里没有使用到缓存,是因为事务没有提交;提交之后:
2、二级缓存是基于namespace的,不同的namespace就算是执行相同的SQL,相同的参数也不会共享数据
@Test
public void select2() throws InterruptedException {
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
AccountMapper mapper1 = sqlSession1.getMapper(AccountMapper.class);
BooksDao mapper2 = sqlSession2.getMapper(BooksDao.class);
System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));
sqlSession1.commit();
System.out.println("----------------------------------------");
System.out.println(JSON.toJSONString(mapper2.selectByPrimaryKey(2), true));
}
这里二级缓存是不起作用的;
3、不同的SqlSession,操作同一个namespace,进行增删改查的时候,清空namespace
@Test
public void select3() throws InterruptedException {
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
AccountMapper mapper1 = sqlSession1.getMapper(AccountMapper.class);
AccountMapper mapper2 = sqlSession1.getMapper(AccountMapper.class);
System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));
sqlSession1.commit();
System.out.println("----------------------------------------");
mapper2.deleteByPrimaryKey(25);
sqlSession2.commit();
System.out.println("----------------------------------------");
System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));
}
memcached缓存数据
- 我们现在的数据存储在二级缓存默认的实现hashmap中,但是数据应该存储在缓存服务器当中,理论上来说,可以指定无数种二级缓存,只要程序实现mybatis中的Cache接口即可;
- memcached——纯内存,redis——功能更强大,ehcache——hibernate推荐的缓存框架;
- 如何在mybatis中使用memcached缓存数据;
步骤:
1、导入memcached for mybatis 缓存的jar包:
<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-memcached -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-memcached</artifactId>
<version>1.0.0</version>
</dependency>
2、编写memcached.properties
的配置文件,"约定大于配置"思想:
#any string identifier
org.mybatis.caches.memcached.keyprefix=_biz-cache-wk_
#space separated list of ${host}:${port}
org.mybatis.caches.memcached.servers=127.0.0.1:11211
#org.mybatis.caches.memcached.servers=192.168.0.44:12000
#Any class that implementsnet.spy.memcached.ConnectionFactory
org.mybatis.caches.memcached.connectionfactory=net.spy.memcached.DefaultConnectionFactory
#the number of seconds in 30 days the expiration time (in seconds)
org.mybatis.caches.memcached.expiration=6000
#flag to enable/disable the async get
org.mybatis.caches.memcached.asyncget=false
#the timeout when using async get
org.mybatis.caches.memcached.timeout=5
#the timeout unit when using async get
org.mybatis.caches.memcached.timeoutunit=java.util.concurrent.TimeUnit.SECONDS
#if true, objects will be GZIP compressed before putting them to
org.mybatis.caches.memcached.compression=false
#refuse time when connection refused
org.mybatis.caches.memcached.refuseperiod=1000
3、在mapper.xml中指定二级缓存的实现类:
<cache type="org.mybatis.caches.memcached.MemcachedCache"/>
4、进行测试:
开启memcached缓存的服务端,以及客户端软件TreeSoft,编写测试案例:
@Test
public void select1() throws InterruptedException {
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
BooksDao mapper1 = sqlSession1.getMapper(BooksDao.class);
BooksDao mapper2 = sqlSession2.getMapper(BooksDao.class);
System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(3), true));
sqlSession1.commit();
System.out.println("----------------------------------------");
System.out.println(JSON.toJSONString(mapper2.selectByPrimaryKey(3), true));
}
二级缓存起到了作用,并出现了
Cache Hit Ratio [com.oracle.mapper.BooksDao]: 0.5
;
- 可以发现,只有二级缓存才有日志信息的输出,一级缓存没有;
二级缓存中数据的脏读:
1、单表操作的时候,先查询数据,再修改数据,再查询数据,数据是没有问题的:因为二级缓存的作用域是整个namespace
,出了这个范围,缓存不起作用;并且更新、修改、删除操作也会清空缓存;
@Test
public void select3() throws InterruptedException {
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
BooksDao mapper1 = sqlSession1.getMapper(BooksDao.class);
BooksDao mapper2 = sqlSession1.getMapper(BooksDao.class);
System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(3), true));
sqlSession1.commit();
System.out.println("----------------------------------------");
Books books = new Books();
books.setBookid(3);
books.setBookname("C#入门");
books.setBprice(200d);
books.setAccountid(4);
mapper2.updateByPrimaryKey(books);
sqlSession2.commit();
System.out.println("----------------------------------------");
System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(3), true));
}
2、多表联合查询的时候,读取数据是会存在问题的,因为二级缓存是基于namespace的,多表不属于一个namespace;
为了解决实验上述问题,可以使用
Cache ref
,让一个Mapper引用另外一个Mapper的命名空间,这样两个映射文件对应的SQL操作都使用的是同一块缓存了。不过这样做的后果是,缓存的粒度变粗了,多个Mapper namespace下的所有操作都会对缓存使用造成影响。
6、二级缓存源码分析
MyBatis二级缓存的工作流程和前文提到的一级缓存类似,只是在一级缓存处理前,用CachingExcutor装饰了BaseExcutor的子类,在委托具体职责给delegate之前,实现了二级缓存的查询和写入功能;
CachingExecutor
的query方法,首先会从MappedStatement
中获得在配置初始化时赋予的Cache。
Cache cache = ms.getCache();
本质上是装饰者模式的使用,具体的装饰链是:
SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache
以下是具体这些Cache实现类的介绍,他们的组合为Cache赋予了不同的能力。
- SynchronizedCache:同步Cache,实现比较简单,直接使用synchronized修饰方法。
- LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。
- SerializedCache:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。
- LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value。
- PerpetualCache: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。
然后是判断是否需要刷新缓存,代码如下所示:
flushCacheIfRequired(ms);
在默认的设置中SELECT语句不会刷新缓存,insert/update/delte会刷新缓存。进入该方法。代码如下所示:
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
MyBatis的CachingExecutor
持有了TransactionalCacheManager
,即上述代码中的tcm;TransactionalCacheManager中持有了一个Map,代码如下所示:
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
这个Map保存了Cache
和用TransactionalCache
包装后的Cache的映射关系。TransactionalCache
实现了Cache接口,CachingExecutor
会默认使用他包装初始生成的Cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。
在TransactionalCache的clear,有以下两句。清空了需要在提交时加入缓存的列表,同时设定提交时清空缓存,代码如下所示:
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
CachingExecutor继续往下走,ensureNoOutParams主要是用来处理存储过程的,暂时不用考虑。
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
之后会尝试从tcm中获取缓存的列表。
List<E> list = (List<E>) tcm.getObject(cache, key);
在getObject方法中,会把获取值的职责一路传递,最终到PerpetualCache。如果没有查到,会把key加入Miss集合,这个主要是为了统计命中率。
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
CachingExecutor继续往下走,如果查询到数据,则调用tcm.putObject方法,往缓存中放入值。
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
tcm的put方法也不是直接操作缓存,只是在把这次的数据和key放入待提交的Map中。
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
从以上的代码分析中,我们可以明白,如果不调用commit方法的话,由于TranscationalCache的作用,并不会对二级缓存造成直接的影响。因此我们看看Sqlsession的commit方法中做了什么。代码如下所示:
@Override
public void commit(boolean force) {
try {
executor.commit(isCommitOrRollbackRequired(force));
因为我们使用了CachingExecutor,首先会进入CachingExecutor实现的commit方法。
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
会把具体commit的职责委托给包装的Executor。主要是看下tcm.commit()
,tcm最终又会调用到TrancationalCache。
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
看到这里的clearOnCommit就想起刚才TrancationalCache的clear方法设置的标志位,真正的清理Cache是放到这里来进行的。具体清理的职责委托给了包装的Cache类。之后进入flushPendingEntries方法。代码如下所示:
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
................
}
在flushPendingEntries中,将待提交的Map进行循环处理,委托给包装的Cache类,进行putObject的操作。
后续的查询操作会重复执行这套流程。如果是insert|update|delete
的话,会统一进入CachingExecutor的update方法,其中调用了这个函数,代码如下所示:
private void flushCacheIfRequired(MappedStatement ms)
在二级缓存执行流程后就会进入一级缓存的执行流程,因此不再赘述。