Mybatis缓存
文章目录
为什么使用缓存
缓存(也成为cache)的作用是为了减去数据库的压力,提高数据库的性能,缓存实现的原理是从数据库中查询处理的对象再使用完后不要销毁,而是储存在内存(缓存)中,当再次需要获取该对象的时,直接从内存(缓存)中直接获取,不在向数据库执行select语句,从而减少了对数据库的查询次数,因此提高了数据库的性能,缓存是使用Map集合缓存数据的
缓存: 将数据临时存储(本地硬盘,内存),这样就减少了对数据库的访问 mybatis 提供一级缓存 二级缓存 一级缓存是Sqlsession级别的 在同一个Sqlsession中可以将第一次查询到的数据缓存到Sqlsession 第二次查询相同数据时,就可以直接从Sqlsession中获取 缓存失效 close clearCache 执行新增,修改,删除操作会清空一级缓存 二级缓存是SqlSessionFactory级别(整个程序只有一个SqlSessionFactory) 以namespace 划分存储的区域
一级缓存
- 一级缓存的作用域是同一个
SqlSession
- 同一个sqlSession中两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。
- 当一个sqlSession结束后该sqlSession中的一级缓存也就不存在了
- Mybatis默认开启一级缓存。
一级缓存的生命周期
- mybatis在开启一个数据库会话时…会创建一个新的SqlSession对象,SqlSession对象中会有一个新的
Executor
对象,Executor对象中持有一个新的PerpetualCache
对象,当会话结束的时候,sqlSqssion对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉 - 如果SqlSession调用close()方法,会释放掉一级缓存
PerpetualCache
对象,一级缓存将不可使用 - 如果SqlSession调用了clearCache(),会清空PerpetualCache对象
中的数据,但是该对象仍可使用
。 - SqlSession中执行了任何一个update操作(
update()
、delete()
、insert()
),都会清空PerpetualCache
对象的数据,但是该对象可以继续使用
二级缓存
-
是多个
SqlSession
共享的,其作用域是mapper
的同一个namespace,
<mapper namespace="com.nie.dao.StudentDao">
-
不同的
sqlSession
两次执行相同namespace
下的sql
语句且向sql
中传递参数也相同即最终执行相同的sql
语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。 -
Mybatis默认没有开启二级缓存需要在setting全局参
数中配置开启二级缓存。
二级缓存
二级缓存详解
二级缓存区域是根据mapper的
namespace
划分的,相同namespace
的mapper查询的数据缓存在同一个区域,如果使用mapper代理方法每个mapper
的namespace
都不同,此时可以理解为二级缓存区域是根据mapper划分。
每次查询会先从缓存区域中查找,如果找不到则从数据库查询,并并将查询到数据写入缓存
Mybatis
内部缓存使用一个hashMap
,Key为hashCode+sqlId+Sql
语句,Value为从从查询出来映射的java对象
sqlSession执行insert、update、delete等操作commit提交后会清空缓存区
域,防止脏读。
配置二级缓存配置
第一步:
启用二级缓存
在SqlMapperConfig.xml中启用二级缓存,如下代码所示,当cacheEnabled设置为true时启用二级缓存,设置为false时禁用二级缓存。
<settingname="cacheEnabled" value="true"/>
第二步:POJO序列化
将所有的POJO类实现序列化接口Java.io.Serializable。
import java.io.Serializable;
public class Student implements Serializable {
.......
}
第三步:配置映射文件
在Mapper映射文件中添加,表示此mapper开启二级缓存
<!--使用namespace使用二级缓存-->
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true">
</cache>
这个更高级的配置创建了一个FIFP缓存,每隔60S刷新一次,存储集合或对象的512个引用,而且返回的对象被认为是只读的,因而在不同线程中的调用者之间修改它们会导致冲突。
cache可以配置的属性如下:
eviction(收回策略)
LRU 最近最少使用的,移除最长时间不被使用的对象,这是默认值
FIFO 先进先出,按对象进入缓存的顺序来移除它们
SOFT 软引用,移除基于垃圾回收器状态和软引用规则的对象
WEAK 弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象
flushInterval(刷新间隔)
可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。 默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新
size(引用数目)
可以被设置为任意的正整数,要记住缓存的对象数目和运行环境的可用内存资源数目,默认1024
readOnly(只读)
属性可以被设置为true后者false。 只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。 可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但很安全,因此默认为false
MyBatisUtils
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class MyBatisUtils {
private static SqlSessionFactory factory = null;
static {
String config = "mybatis.xml";
try {
InputStream resourceAsStream = Resources.getResourceAsStream(config);
factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
} catch (IOException e) {
e.printStackTrace();
}
}
//创建方法,获取sqlSession
public static SqlSession getSqlSession() {
SqlSession SqlSession = null;
if (factory != null) {
SqlSession = factory.openSession();//非自动提交事务
}
return SqlSession;
}
//创建方法,获取SqlSessionFactory
public static SqlSessionFactory getsqlSessionFactory() {
return (SqlSessionFactory) factory;
}
}
相关的动态sql
生命周期和作用域
生命周期和作用域是至关重要的,因为错误的使用会导致非常严重的并发问题
SqlSessionFactoryBuilder
这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。
因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。
SqlSessionFactory
说白了就是数据库连接池。SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。
使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。
SqlSession
连接到连接池的一个请求每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。用完之后需要赶紧关闭,否则资源被占用
缓存穿透,缓存击穿,缓存雪崩
缓存穿透
是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并别出于容错考虑,如果从储存层查不到数据则不写入缓存,则将导致这个不存在的数据每次请求都要到储存层去查询,失去了缓存的意义,在流量大的时,可能数据库就挂掉,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案:
- 有很多种方法可以有效地解决缓存穿透问题,接口层增加校验,如用户鉴权校验,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
- 另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
缓存雪崩
是指我们设置缓存时采用了相同的过期时间,导致缓存在某一时间同时失效,请求全部转发到的DB,DB瞬间压力过于重 雪崩
解决方案
缓存失效时的雪崩效应对底层系统的冲击非常可怕。
- 考虑加锁或者从队列的方式保证缓存的单线程写,从而避免了失效时大量的并发请求落在到底层数储存系统上.
- 一个简单方案就时讲缓存失效时间分散开,比如可以在原有的失效时间上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
- 设置热点数据永远不过期。
缓存击穿
缓存击穿是指缓存中没有但是数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没有读到的数据,又同时去数据库取数据,引起数据库压力瞬间增大,造成过大压力
缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
解决方案
- 设置热点数据永远不过期。
- 加互斥锁,互斥锁参考代码如下
1)缓存中有数据,直接走上述代码13行后就返回结果了
2)缓存中没有数据,第1个进入的线程,获取锁并从数据库去取数据,没释放锁之前,其他并行进入的线程会等待100ms,再重新去缓存取数据。这样就防止都去数据库重复取数据,重复往缓存中更新数据情况出现。
3)当然这是简化处理,理论上如果能根据key值加锁就更好了,就是线程A从数据库取key1的数据并不妨碍线程B取key2的数据,上面代码明显做不到这点。