OSCache 缓存重建在 Race Condition 下的 NRE 问题

标签: oscache 缓存 重建 | 发表时间:2012-12-07 23:40 | 作者:旁观者
出处:http://www.cnblogs.com/zhengyun_ustc/
@郑昀汇总
 
一,现象:
高并发情况下,使用 OSCache 作为本地缓存中间件的前端服务,日志文件中会出现大量如下错误信息:
异常堆栈:

java.lang.IllegalStateException:  Cannot complete cache update - current state (2) is not UPDATE_IN_PROGRESS

         at com.opensymphony.oscache.base.EntryUpdateState.completeUpdate(EntryUpdateState.java:105)

         at com.opensymphony.oscache.base.Cache.completeUpdate(Cache.java:762)

         at com.opensymphony.oscache.base.Cache.putInCache(Cache.java:619)

         at com.opensymphony.oscache.base.Cache.putInCache(Cache.java:580)

         at com.opensymphony.oscache.general.GeneralCacheAdministrator.putInCache(GeneralCacheAdministrator.java:249)

         at com.opensymphony.oscache.general.GeneralCacheAdministrator.putInCache(GeneralCacheAdministrator.java:259)

二,NRE 背景:
无论你使用哪一种本地缓存中间件,如果你缓存数据片段时设置了过期时间,都需要考虑缓存失效后的缓存重建(repopulate the cache)场景。
进一步必须考虑 Race Condition (同进程下多线程,或不同进程)下如何重建。
 
也就是说, 某个线程在重建缓存过程中,其他线程发现缓存不存在或已过期,该如何处置
 
从2005年OSCache的版本到现在,它一直这么声称:
   * @throws  NeedsRefreshException Thrown when the object either
     * doesn't exist, or exists but is stale.  When this exception occurs,
     *  the CacheEntry corresponding to the supplied key will be locked
     * and other threads requesting this entry will potentially be blocked
     * until the caller repopulates the cache. If the caller choses not
     * to repopulate the cache, they <em>must</em> instead call
     * {@link #cancelUpdate(String)}.
即,
当 key 不存在,或者存在但数据过期(stale)时,
调用 getFromCache 函数时会抛出 NRE 异常;
当异常发生时,
对应于这个 key 的 CacheEntry 将被锁住,
而请求这个 entry 的其他线程将 可能(注意,仅仅是可能)被阻塞,直到调用者重建缓存。
如果调用者没有选择重建缓存,必须 调用 cancelUpdate 函数来“Cancels any pending update for this cache entry”。
 
三,缓存重建的 OSCache 官方推荐做法:
读取缓存遇到 NRE 异常时,OSCache 官方推荐的做法是:
1 String myKey = "myKey";
2 String myValue;
3 int myRefreshPeriod = 1000;
4 try {
5 // Get from the cache
6 myValue = (String) admin. getFromCache(myKey, myRefreshPeriod);
7 } catch (NeedsRefreshException nre) {
8 try {
9 // Get the value (probably from the database)
10 myValue = "This is the content retrieved.";
11 // Store in the cache
12 admin. putInCache(myKey, myValue);
13 } catch ( Exception ex) {
14 // We have the current content if we want fail-over.
15 myValue = (String) nre.getCacheContent();
16 // It is essential that cancelUpdate is called if the
17 // cached content is not rebuilt
18 admin. cancelUpdate(myKey);
19 }
20 }

即,本线程先试图重建缓存,如果再次发生异常,则本线程(不管三七二十一)直接调用 cancelUpdate 函数。

现在的一些 OSCache Manager 工具类,get 方法也就实现为一旦捕获 NRE 异常就直接 canelUpdate:
1 public OSCache get(String key,int myRefreshPeriod){
2 try{
3 return (OSCache)this.admin. getFromCache(key,myRefreshPeriod);
4 }catch(NeedsRefreshException ex){
5 this.admin. cancelUpdate(key);
6 return null;
7 }
8 }

 

四,OSCache 在 Race Condition 下缓存重建的特殊场景
简单地说,就是:
线程1 正在重建缓存;
线程2 读取缓存时得到 NRE 异常,主动 cancel update;
线程1 重建缓存完毕,却发现状态被改为了 UPDATE_CANCELLED ,与期望不符,于是抛出异常 java.lang.IllegalStateException 。
 
具体过程如下:
(0)缓存过期;
(1)线程 T1 获得 update lock,并开始调用 putInCache 函数 ;
(2) 线程 T2 在 T1 没有结束 update 之前,也开始 getCacheEntry 了;
(3)T2 调用的 getCacheEntry 函数捕获  NeedsRefreshException 异常;
(4)T2 调用 cancelUpdate 函数来“取消所有试图更新本 cache entry 的操作”, 于是 EntryUpdateState 变为 UPDATE_CANCELLED,它是一个正整数 2
(5) T1 其实已经重建了缓存
(6)T1 随后调用 completeUpdate(EntryUpdateState.java,93行) 来通知那些等着本次更新操作的线程; 但 completeUpdate 函数却发现当前 EntryUpdateState 居然不等于 UPDATE_IN_PROGRESS(对应0),而是 UPDATE_CANCELLED(对应2),于是抛出异常。即下面代码抛出的 IllegalStateException 异常,文字通常为:“Cannot complete cache update - current state ( 2) is not UPDATE_IN_PROGRESS”,其中的 2 就是指  UPDATE_CANCELLED:
  1. /** 
  2.  * Updates the state to <code>UPDATE_COMPLETE</code>. This should <em>only</em> 
  3.  * be called by the thread that managed to get the update lock. 
  4.  * @return the counter value after the operation completed 
  5.  */  
  6. public int completeUpdate() {  
  7.     if (state != UPDATE_IN_PROGRESS) {  
  8.         throw new IllegalStateException("Cannot complete cache update - current state (" + state + ") is not UPDATE_IN_PROGRESS");  
  9.     }  
  10.   
  11.     state = UPDATE_COMPLETE;  
  12.     return decrementUsageCounter();  
  13. }  
 
总之,按目前 OSCacheManager 的做法,在高并发环境下,一旦一个 OSCache 缓存失效,而缓存的数据片段很大,那么很有可能让其他线程在 getFromCache 时有机会捕获 NRE 异常,最终导致做缓存重建的线程抛出 IllegalStateException 异常,虽然此时缓存已经重建完毕。

本文链接

相关 [oscache 缓存 重建] 推荐:

OSCache 缓存重建在 Race Condition 下的 NRE 问题

- - 博客园_旁观者
高并发情况下,使用 OSCache 作为本地缓存中间件的前端服务,日志文件中会出现大量如下错误信息:. 无论你使用哪一种本地缓存中间件,如果你缓存数据片段时设置了过期时间,都需要考虑缓存失效后的缓存重建(repopulate the cache)场景. 进一步必须考虑 Race Condition (同进程下多线程,或不同进程)下如何重建.

OSCache缓存监控实现

- - ITeye博客
最近一个项目用到OsCache的页面片段缓存,google了一下居然没有找到OsCache的监控工具(list all keys from cache),于是大略读了一下OSCache-2.4.1的源码,发现Cache.java类的cacheMap定义成了私有变量,如下:. 所以只能用反射机制暴力破解了(按照Sun公司的JVM规范是许可的^_^),主要代码如下:.

oscache之刷新缓存flushEntry的使用

- - ITeye博客
==========================  困扰和痛苦多时的oscache刷新缓存start  =======================. Everyday都不同于2015-9-19 周六15:30 . 【前言】一般而言,oscache缓存常用于在高并发的情形下. 当你初次调用缓存的方法时,如果缓存中还没有响应的key,则会去执行底层的sql语句,并把结果缓存起来.

不可忽略的缓存重建

- gOODiDEA - NoSQLFan
本文的主要内容来源于MongoDB官方博客,由NoSQLFan补充说明,本文对传统的分布式Cache系统进行了分析,指出了其在缓存重建中会对数据库产生巨大压力的问题. 并分析了MongoDB的mmap方案是如何规避这一问题的. 如下图的架构,在数据库前端加上分布式的Cache(比如我们常用的Memcached),让客户端在访问时先查找Cache,Cache不命中再读数据库并将结构缓存在Cache中.

缓存算法

- lostsnow - 小彰
没有人能说清哪种缓存算法由于其他的缓存算法. (以下的几种缓存算法,有的我也理解不好,如果感兴趣,你可以Google一下  ). 大家好,我是 LFU,我会计算为每个缓存对象计算他们被使用的频率. 我是LRU缓存算法,我把最近最少使用的缓存对象给踢走. 我总是需要去了解在什么时候,用了哪个缓存对象.

Hibernate 缓存

- - ITeye博客
1数据缓存:(date caching) 是一种将数据暂时存于内存缓存去中的技术,缓存通常是影响系统性能的关键因素. 2.ORM的数据缓存策略有3中.   1.事务级缓存:  分为 数据库事务和 应用级事务,是基于Session的生命周期的实现,每个session都会在内部维持一个数据缓存, 随session的创建和消亡.

hibernate缓存,一级缓存,二级缓存,查询缓存

- - CSDN博客推荐文章
1、缓存是数据库数据在内存中的临时容器,它包含了库表数据在内存中的临时拷贝,位于数据库和访问层之间. 2、ORM在进行数据读取时,会根据缓存管理策略,首先在缓冲中查询,如果发现,则直接使用,避免数据库调用的开销. 事务级缓存:当前事务范围内的数据缓存. 应用级缓存:某个应用中的数据缓存. 分布式缓存:多个应用,多个JVM之间共享缓存.

缓存相关——缓存穿透、缓存并发、缓存失效、缓存预热、缓存雪崩、缓存算法

- - 编程语言 - ITeye博客
我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据库然后再缓存查询结果返回. 这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,可能DB就挂掉了. 要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞.

Hibernate 二级缓存

- - CSDN博客推荐文章
很多人对二级缓存都不太了解,或者是有错误的认识,我一直想写一篇文章介绍一下hibernate的二级缓存的,今天终于忍不住了. 我的经验主要来自hibernate2.1版本,基本原理和3.0、3.1是一样的,请原谅我的顽固不化. hibernate的session提供了一级缓存,每个session,对同一个id进行两次load,不会发送两条sql给数据库,但是session关闭的时候,一级缓存就失效了.