zoukankan      html  css  js  c++  java
  • OSCache 缓存重建在 Race Condition 下的 NRE 问题

    @郑昀汇总
     
    一,现象:
    高并发情况下,使用 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 异常,虽然此时缓存已经重建完毕。
     
  • 相关阅读:
    光盘和U盘
    解决时间同步
    僵尸进程 和 孤儿进程
    Centos虚拟机设置网络模式
    常用CDN 和 后台管理模板
    微信小程序wxs如何使用
    kubernetes/client-go--使用 Clientset 获取 Kubernetes 资源对象
    samplecontroller
    volcano
    DNS欺骗
  • 原文地址:https://www.cnblogs.com/zhengyun_ustc/p/oscache_illegalStateException.html
Copyright © 2011-2022 走看看