WHCSRL 技术网

Myabtis源码分析四-缓存模块分析 ,装饰模式的使用

一、Mybatis缓存模块分析

mybatis缓存模块具备以下特点:

  1. MyBatis 缓存的实现是基于 Map 的,从缓存里面读写数据是缓存模块的核心基础功能;
  2. 除核心功能之外,有很多额外的附加功能,如:防止缓存击穿,添加缓存清空策略(fifo、 lru)、序列化功能、日志能力、定时清空能力等;
  3. 附加功能可以以任意的组合附加到核心基础功能之上;

那么我们应该如何优雅的为核心功能添加附加能力呢?

有些同学可能了解使用继承的办法扩展附加功能,继承的方式是静态的,用户不能控制增加行为的方式和时机。另外,新功能的存在多种组合,使用继承可能导致大量子类存在;

基于 Map 核心缓存能力,将阻塞、清空策略、序列化、日志等等能力以任意组合的方式优 雅的增强是 Mybatis 缓存模块实现最大的难题,用动态代理或者继承的方式扩展多种附加能力的传统方式存在以下问题:这些方式是静态的,用户不能控制增加行为的方式和时机;另 外,新功能的存在多种组合,使用继承可能导致大量子类存在。综上,MyBtis 缓存模块采用 了装饰器模式实现了缓存模块; 

二、装饰器模式 

1、装饰器模式介绍

装饰器模式是一种用于代替继承的技术,无需通过继承增加子类就能扩展对象的新功能。使 用对象的关联关系代替继承关系,更加灵活,同时避免类型体系的快速膨胀。装饰器 UML 类图如下: 
 

 组件含义如下:

  • 组件(Component):组件接口定义了全部组件类和装饰器实现的行为; 
  • 组件实现类(ConcreteComponent):实现 Component 接口,组件实现类就是被装饰器 装饰的原始对象,新功能或者附加功能都是通过装饰器添加到该类的对象上的
  • 装饰器抽象类(Decorator):实现 Component 接口的抽象类,在其中封装了一个 Component 对象,也就是被装饰的对象;
  • 具体装饰器类(ConcreteDecorator):该实现类要向被装饰的对象添加某些功能; 

 我们很多人都玩过游戏,以DNF里的职业剑魂为例,装饰器模式图示如下: 

2、装饰器模式优点

装饰器相对于继承,装饰器模式灵活性更强,扩展性更强:  

  • 灵活性:装饰器模式将功能切分成一个个独立的装饰器,在运行期可以根据需要动态的 添加功能,甚至对添加的新功能进行自由的组合;
  • 扩展性:当有新功能要添加的时候,只需要添加新的装饰器实现类,然后通过组合方式 添加这个新装饰器,无需修改已有代码,符合开闭原则;

3、装饰器模式使用举例

  1. IO 中输入流和输出流的设计 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("c://a.txt"))); 
  2. 对网络爬虫的自定义增强,可增强的功能包括:多线程能力、缓存、自动生成报表、黑 白名单、random 触发等 

三、装饰器在缓存模块的使用 

MyBatis 缓存模块是一个经典的使用装饰器实现的模块,类图如下:

  • Cache:Cache 接口是缓存模块的核 心接口,定义了缓存的基本操作; 
  • PerpetualCache:在缓存模块中扮演 ConcreteComponent 角色,使用 HashMap 来实现 cache 的相关操作; 
  • BlockingCache:阻塞版本的缓存装 饰器,保证只有一个线程到数据库去查 找指定的 key 对应的数据;BlockingCache 是阻塞版本的缓存装饰器,这个装饰器通过 ConcurrentHashMap 对锁的粒度 进行了控制,提高加锁后系统代码运行的效率(注:缓存雪崩的问题可以使用细粒度锁的方 式提升锁性能) 

源码分析:

  1. /**
  2. * Simple blocking decorator
  3. *
  4. * Simple and inefficient version of EhCache's BlockingCache decorator.
  5. * It sets a lock over a cache key when the element is not found in cache.
  6. * This way, other threads will wait until this element is filled instead of hitting the database.
  7. *
  8. * 阻塞版本的缓存装饰器,保证只有一个线程到数据库去查找指定的key对应的数据
  9. *
  10. *
  11. */
  12. public class BlockingCache implements Cache {
  13. //阻塞的超时时长
  14. private long timeout;
  15. //被装饰的底层对象,一般是PerpetualCache
  16. private final Cache delegate;
  17. //锁对象集,粒度到key值
  18. private final ConcurrentHashMap<Object, ReentrantLock> locks;
  19. public BlockingCache(Cache delegate) {
  20. this.delegate = delegate;
  21. this.locks = new ConcurrentHashMap<>();
  22. }
  23. @Override
  24. public String getId() {
  25. return delegate.getId();
  26. }
  27. @Override
  28. public int getSize() {
  29. return delegate.getSize();
  30. }
  31. @Override
  32. public void putObject(Object key, Object value) {
  33. try {
  34. delegate.putObject(key, value);
  35. } finally {
  36. releaseLock(key);
  37. }
  38. }
  39. @Override
  40. public Object getObject(Object key) {
  41. acquireLock(key);//根据key获得锁对象,获取锁成功加锁,获取锁失败阻塞一段时间重试
  42. Object value = delegate.getObject(key);
  43. if (value != null) {//获取数据成功的,要释放锁
  44. releaseLock(key);
  45. }
  46. return value;
  47. }
  48. @Override
  49. public Object removeObject(Object key) {
  50. // despite of its name, this method is called only to release locks
  51. releaseLock(key);
  52. return null;
  53. }
  54. @Override
  55. public void clear() {
  56. delegate.clear();
  57. }
  58. @Override
  59. public ReadWriteLock getReadWriteLock() {
  60. return null;
  61. }
  62. private ReentrantLock getLockForKey(Object key) {
  63. ReentrantLock lock = new ReentrantLock();//创建锁
  64. ReentrantLock previous = locks.putIfAbsent(key, lock);//把新锁添加到locks集合中,如果添加成功使用新锁,如果添加失败则使用locks集合中的锁
  65. return previous == null ? lock : previous;
  66. }
  67. //根据key获得锁对象,获取锁成功加锁,获取锁失败阻塞一段时间重试
  68. private void acquireLock(Object key) {
  69. //获得锁对象
  70. Lock lock = getLockForKey(key);
  71. if (timeout > 0) {//使用带超时时间的锁
  72. try {
  73. boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
  74. if (!acquired) {//如果超时抛出异常
  75. throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
  76. }
  77. } catch (InterruptedException e) {
  78. throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
  79. }
  80. } else {//使用不带超时时间的锁
  81. lock.lock();
  82. }
  83. }
  84. private void releaseLock(Object key) {
  85. ReentrantLock lock = locks.get(key);
  86. if (lock.isHeldByCurrentThread()) {
  87. lock.unlock();
  88. }
  89. }
  90. public long getTimeout() {
  91. return timeout;
  92. }
  93. public void setTimeout(long timeout) {
  94. this.timeout = timeout;
  95. }
  96. }

除了 BlockingCache 之外,缓存模块还有其他的装饰器如: 

  1. LoggingCache:日志能力的缓存; 
  2. ScheduledCache:定时清空的缓存; 
  3. BlockingCache:阻塞式缓存; 
  4. SerializedCache:序列化能力的缓存; 
  5. SynchronizedCache:进行同步控制的缓存; 

 
那么问题来了,我们知道HashMap是线程不安全的,那么Mybatis 的缓存功能使用 HashMap 实现会不会出现并发安全的问题呢? 

MyBatis 的缓存分为一级缓存、二级缓存。二级缓存是多个会话共享的缓存,确实会出 现并发安全的问题,因此 MyBatis 在初始化二级缓存时,会给二级缓存默认加上 SynchronizedCache 装饰器的增强,在对共享数据 HashMap 操作时进行同步控制,所以二级 缓存不会出现并发安全问题;而一级缓存是会话独享的,不会出现多个线程同时操作缓存数 据的场景,因此一级缓存也不会出现并发安全的问题; 

四、缓存的唯一标识 CacheKey 

MyBatis 中涉及到动态 SQL 的原因,缓存项的 key 不能仅仅通过一个 String 来表示,所以通 过 CacheKey 来封装缓存的 Key 值,CacheKey 可以封装多个影响缓存项的因素;
判断两个 CacheKey是否相同关键是比较两个对象的hash值是否一致;构成CacheKey对象的要素包括:

  1. mappedStatment 的 id 
  2. 指定查询结果集的范围(分页信息) 
  3. 查询所使用的 SQL 语句 
  4. 用户传递给 SQL 语句的实际参数值

CacheKey 中 update 方法和 equals 方法是进行比较时非常重要的两个方法:

  • update 方法:用于添加构成 CacheKey 对象的要素,每添加一个元素会对 hashcode、checksum、count 以及 updateList 进行更新;
  • equals 方法:用于比较两个元素是否相等。首先比较 hashcode、checksum、count 是否 相等,如果这三个值相等,会循环比较 updateList 中每个元素的 hashCode 是否一致;

 按照这种方式判断两个对象是否相等,一方面能很严格的判断是否一致避免出现误判, 另外一方面能提高比较的效率; 

推荐阅读