文章目录
- 背景
- Ehcache2源码解析-如何实现缓存网页
- 读写锁ReentrantReadWriteLock解析
- 读写锁的特性
- 读写锁是如何实现的?
- 如何将Ehcach2-web的源码迁移到Ehcach3中?/ 如何自定义Filter实现高性能网页缓存?
背景
在我们的销售页面,有一个类似日历的控件,用户可以在该控件中查找当前月份中可以买票的活动。当多个用户访问该控件时,看到的内容是一样的,没必要每个用户发起的请求再去数据库查一遍。为了提高用户体验和避免大促期间该页面给服务器带来压力,我们决定对该控件的响应进行缓存。早期使用Ehcache2的SimplePageFragmentCachingFilter实现。
由于Ehcache2不再维护,我们决定将Ehcache2迁移到Ehcache3。在将Ehcache2迁移到Ehcache3的过程中,发现Ehcache2下的ehcache-web在Ehcache3中不再支持,现有项目中仍使用ehcache-web的SimplePageCachingFilter来缓存网页,于是决定copy SimplePageCachingFilter源码并使用 Ehcache3进行改写。在这个过程中发现了此SimplePageCachingFilter的实现利用里Java的读写锁。
Ehcache2源码解析-如何实现缓存网页
- 缓存网页的核心逻辑在于net.sf.ehcache.constructs.web.filter.CachingFilter#buildPageInfo
- blockingCache的get方法如下所示
- 先拿到一个锁,该锁是net.sf.ehcache.concurrent.ReadWriteLockSync的实例
该类的内部有一个Java的ReentrantReadWriteLock - 多线程并发时,拿到锁之后先去获取读锁,拿到之后立刻释放掉。然后去拿写锁,如果第一次拿到写锁则返回的Element对象是null,此时写锁未释放,其他并发的线程拿不到写锁就等待一定时间,此时间可配置。
- 拿到写锁之后则返回到第1步,调用buildPage,最后将该对象blockingCache.put进去,写锁在这里释放
- 总结一下Ehcache2的实现网页缓存的逻辑,通过配置Filter的url-pattern,在浏览器端请求该url时会对response进行缓存。具体的缓存逻辑是由Java的读写锁ReentrantReadWriteLock完成的,使用ReentrantReadWriteLock确实能解决大量并发请求一个网页内容时的性能问题
读写锁ReentrantReadWriteLock解析
读写锁的特性
直接看注释
- 多线程并发时,如果都去获取读锁,则不互斥;如果此时有线程拿到了写锁,再去获取读锁,则需要等待写锁释放
- 获取写锁时,如果此时没有其他线程拿到读锁或写锁,则直接获取;如果其他线程此时拿着读锁或写锁,则需等到锁的释放
读写锁是如何实现的?
在Java中的锁(二)实现自定义锁中提到了,如果想要自定义锁,需要实现Lock接口+重写AQS的方法,并在实现的Lock接口中调用AQS的模板方法,这些方法最终会调用重写的AQS方法。
-
看一下它的主要结构,可以看到它的实现是按照上面我们所说的思路实现
-
以读锁的lock方法为例,调用AQS的模板方法acquireShared,该方法会最终调用ReentrantReadWriteLock内部类Sync中的tryAcquireShared方法
如何将Ehcach2-web的源码迁移到Ehcach3中?/ 如何自定义Filter实现高性能网页缓存?
看了上面的逻辑和读写锁的特性,实现起来就很简单了
- 自定义一个Filter,该Filter在缓存网页时使用Ehcache3。具体做法是将网页的Response放到Ehcache中
- 那怎么提高 高并发下对同一个网页的访问性能呢?使用读写锁
- 每个需要缓存的网页url中应该都有一个标识,此标识作为缓存的key
- 当多个并发请求都请求同一个网页时,拿到key去cache中找,找到则直接返回
- 找不到则开始获取读锁,拿到读锁什么都不做,直接释放;如果拿不到读锁,则等待
- 释放完读锁,立刻去竞争写锁,拿到写锁之后,先判断此时cache中有没有,因为当前线程可能不是第一个拿到写锁的线程,之前的写锁可能已经将内容写入cache中了,有则直接释放写锁;cache中没有则开始将网页的Response写入到cache中,然后释放写锁