@[TOC] 缓存模块—》 装饰器模式
#MyBatis 缓存模块需满足如下需求:
- MyBatis 缓存的实现是基于 Map 的,从缓存里面读写数据是缓存模块的核心基础功能;
- 除核心功能之外,有很多额外的附加功能,如:防止缓存击穿,添加缓存清空策略(fifo、lru)、序列化功能、日志能力、定时清空能力等;
- 附加功能可以以任意的组合附加到核心基础功能之上;基于 Map 核心缓存能力,将阻塞、清空策略、序列化、日志等等能力以任意组合的方式优雅的增强是 Mybatis 缓存模块实现最大的难题,用动态代理或者继承的方式扩展多种附加能力的传统方式存在以下问题:这些方式是静态的,用户不能控制增加行为的方式和时机;另外,新功能的存在多种组合,使用继承可能导致大量子类存在。综上,MyBtis 缓存模块采用了装饰器模式实现了缓存模块。
part1:装饰器模式原理
装饰器模式是一种用于代替继承的技术,无需通过继承增加子类就能扩展对象的新功能。使用对象的关联关系代替继承关系,更加灵活,同时避免类型体系的快速膨胀。装饰器 UML类图如下:
组件(Component):组件接口定义了全部组件类和装饰器实现的行为;
组件实现类(ConcreteComponent):实现 Component 接口,组件实现类就是被装饰器装饰的原始对象,新功能或者附加功能都是通过装饰器添加到该类的对象上的;
装饰器抽象类(Decorator):实现 Component 接口的抽象类,在其中封装了一个
Component 对象,也就是被装饰的对象;
具体装饰器类(ConcreteDecorator):该实现类要向被装饰的对象添加某些功能;
装饰器相对于继承,装饰器模式灵活性更强,扩展性更强:
灵活性:装饰器模式将功能切分成一个个独立的装饰器,在运行期可以根据需要动态的
添加功能,甚至对添加的新功能进行自由的组合;
扩展性:当有新功能要添加的时候,只需要添加新的装饰器实现类,然后通过组合方式
添加这个新装饰器,无需修改已有代码,符合开闭原则;
装饰器模式使用举例:
- IO 中输入流和输出流的设计
- 对网络爬虫的自定义增强,可增强的功能包括:多线程能力、缓存、自动生成报表、黑
白名单、random 触发等
part2:缓存模块(装饰器模式)
MyBatis 缓存模块是一个经典的使用装饰器实现的模块,类图如下:
Cache:Cache 接口是缓存模块的核心接口,定义了缓存的基本操作;
PerpetualCache:在缓存模块中扮演ConcreteComponent 角色,使用 HashMap来实现 cache 的相关操作;
BlockingCache:阻塞版本的缓存装饰器,保证只有一个线程到数据库去查找指定的 key 对应的数据(当然还有许多其他的装饰器),这里展示一下阻塞装饰器;
阻塞装饰器内部维护了一个ConcurrentHashMap<Object, ReentrantLock> locks,保证只有一个线程到数据库去查找指定的key对应的数据
BlockingCache 是阻塞版本的缓存装饰器,这个装饰器通过 ConcurrentHashMap 对锁的粒度进行了控制,提高加锁后系统代码运行的效率(注:缓存雪崩的问题可以使用细粒度锁的方式提升锁性能);
除了 BlockingCache 之外,缓存模块还有其他的装饰器如:
- LoggingCache:日志能力的缓存;
- ScheduledCache:定时清空的缓存;
- BlockingCache:阻塞式缓存;
- SerializedCache:序列化能力的缓存;
- SynchronizedCache:进行同步控制的缓存;
【问题】:从上面的源码我们可以了解到mybatis的缓存中使用的是HashMap,会不会出现并发安全的问题?
MyBatis 的缓存分为一级缓存、二级缓存。二级缓存是多个会话共享的缓存,确实会出现并发安全的问题,因此 MyBatis 在初始化二级缓存时,会给二级缓存默认加上
SynchronizedCache 装饰器的增强,在对共享数据 HashMap 操作时进行同步控制,所以二级缓存不会出现并发安全问题;而一级缓存是会话独享的,不会出现多个线程同时操作缓存数据的场景,因此一级缓存也不会出现并发安全的问题;
缓存的唯一标识 CacheKey:
MyBatis 中涉及到动态 SQL 的原因,缓存项的 key 不能仅仅通过一个 String 来表示,所以通过 CacheKey 来封装缓存的 Key 值,CacheKey 可以封装多个影响缓存项的因素;判断两个CacheKey是否相同关键是比较两个对象的hash值是否一致;构成CacheKey对象的要素包括: - mappedStatment 的 id
- 指定查询结果集的范围(分页信息)
- 查询所使用的 SQL 语句
- 用户传递给 SQL 语句的实际参数值
CacheKey 中 update 方法和 equals 方法是进行比较时非常重要的两个方法:
update 方法:用于添加构成 CacheKey 对象的要素,每添加一个元素会对 hashcode、
checksum、count 以及 updateList 进行更新;
equals 方法:用于比较两个元素是否相等。首先比较 hashcode、checksum、count 是否相等,如果这三个值相等,会循环比较 updateList 中每个元素的 hashCode 是否一致;
按照这种方式判断两个对象是否相等,一方面能很严格的判断是否一致避免出现误判,另外一方面能提高比较的效率;
【持续更新中 敬请期待 下一篇文章将介绍mybatis配置加载的过程】