您的位置:首页 > 其它

2019.9.22笔记——mybatis源码解析之缓存实现原理

2019-09-22 20:19 344 查看
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://blog.csdn.net/qq_35262405/article/details/101170856

mybatis多参数报错误问题分析及解决方法

问题分析

在mybatis以前的版本在mapper类的方法中传递多参数时如果不用@Param传递参数名就会报错


如果从源码上分析我们可以观察到mybaits的底层是通过jdk反射获取参数名,然后通过参数名将

#{}
${}
中同名的参数替换成真正的数值的,当然
#{}
${}
的底层实现也是不一样的,前者是预编译,后者是单纯的在发送sql之前替换数值的。

之所以会出现这个问题是因为jdk8以前反射得到的方法参数名默认都是arg0、arg1、、、、、、

如果@Param注解又可以解决这个问题


其实在springMVC中也有需要拿到方法参数的地方,但是spirngMVC不会出现这个问题,因为spring直接是通过字节码操作的,没用使用jdk的反射获取。

解决方法

当然在jdk1.8之后就可以解决这个问题了,当然还需要在编译器上做一些处理,这里主要介绍了Idea和eclipse的解决方法。

  • Idea

在j编译环境是dk8以后可以通过加上这个参数

-parameters
,如果项目很大不会每次自动编译可以使用maven主动用complie去编译一次项目

  • eclipse

在eclipse中可以通过勾选如下选项拿到参数名(同样需要jdk8以上)

mybatis缓存的实现及分析

mybatis的缓存的实现不管是一级缓存还是二级缓存底层都是通过一个map结构实现的

一级、二级缓存的特点

  • 一级缓存

1、一级缓存不能关闭,默认开启。

2、默认作用域是单个sqlsession。

3、可以改变作用域,总共两个作用域,分别是单个sqlsession或者一个statement,如果是statement相当于没有缓存(因为发送一个sql就是一个statement)。

4、每个namespace的一级缓存是相互隔离的。

  • 二级缓存

1、全局默认开启,但是需要手动加在需要缓存的mapper上,通过@CacheNamespace开启某个命名空间的二级缓存。

2、二级缓存的作用域是所有sqlsession。

3、每个namespace的二级缓存也是相互隔离的。

需要注意所有缓存都是有单位的,都是namespace(命名空间),都是不能跨命名空间的,不同namespace的缓存存储在不同map中,namespace就是mapper类对象的全名(含包名)。

缓存的查询顺序

先查询二级缓存,再查询一级缓存,最后查询数据库

一级缓存的的缓存对象

一级缓存的的缓存对象里主要有着一个map和缓存的id(namespace),被封装在了一个PerpetualCache对象中。

在查询前会先从一级缓存中检查缓存中是否有对应的数据

一级缓存在查询出来结果后将key和数据放到map中去

二级缓存对象

二级缓存的缓存对象是将一级缓存的对象层层封装,属于装饰者模式,下面都是二级缓存对象,由上到下代表封装的顺序,最上面是最外层的对象。

每一个缓存对象都有各自的作用,最里层的PerpetualCache对象就是一级缓存对象,作用也同一级缓存对象一致。

  • SynchronizedCache:同步Cache,实现比较简单,直接使用synchronized修饰方法。
  • LoggingCache:日志功能,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。
  • SerializedCache:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。
  • LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value。
  • PerpetualCache:作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。

这上面有一个个特殊的缓存对象LruCache,这个缓存对象默认是不开启的,只有在配置了缓存刷新间隔之后才能起作用,而且还可以配置它的回收规则

  • LRU(最近最少使用的):移除最长时间不被使用的对象,这是默认值
  • FIFO(先进先出): 按对象进入缓存的顺序来移除它们
  • SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象
  • WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象


必须配置flushInterval属性,而且缓存只有在执行增删改查的时候才会主动刷新缓存

下面就是二级缓存在无事务的状态下的基本调用

事务管理器

如果有事务存在,二级缓存会交给一个事务管理器对象去管理,这个对象里会有一个map集合,里面存放着子事务管理器。

这个子事务管理器中有一个map对象储存着在事务开启之后和事务提交之前的所有缓存,同时也存在一个真正的二级缓存对象,也就是交给事务管理器管理的缓存对象。还存在一个set集合,这个集合存放着未命中的缓存。

如果事务提交失败就不会将map中的数据更新到真正的二级缓存对象中,如果成功会将map中的数据更新到真正的二级缓存对象中去。

那个set集合是为了解决缓存穿透的,在事务提交的时候会将这个set中的缓存把真正缓存中同样的数据清空。这样也能解决二级缓存所带来的脏读幻读

CacheKey的生成

我们知道每个缓存数据都会存在一个map中,那么自然会存在一个key来拿到缓存的数据。那么CacheKey是如何生成的呢?如何才能保证生成的key一定能拿到对应的数据?怎么才能知道两条sql语句是查询同一数据的?

这样看来CacheKey的生成规则就是关键,那么它是根据什么计算出来的呢?

CacheKey主要是生成其中的hashcode,生成的依据如下:

  • sql的id(namespace加上mapper的id)
  • 如果开启分页,起始
  • sql语句
  • 传递的参数

我们知道缓存放在一个hashmap中,所以key是否相等是通过

equals
方法比较的,所以同时还重写了过了CacheKey的
equals
方法。

public boolean equals(Object object) {
if (this == object) {
return true;
} else if (!(object instanceof CacheKey)) {
return false;
} else {
CacheKey cacheKey = (CacheKey)object;
if (this.hashcode != cacheKey.hashcode) {
return false;
} else if (this.checksum != cacheKey.checksum) {
return false;
} else if (this.count != cacheKey.count) {
return false;
} else {
for(int i = 0; i < this.updateList.size(); ++i) {
Object thisObject = this.updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (thisObject == null) {
if (thatObject != null) {
return false;
}
} else if (!thisObject.equals(thatObject)) {
return false;
}
}

return true;
}
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: