高并发情况下,使用 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) |
无论你使用哪一种本地缓存中间件,如果你缓存数据片段时设置了过期时间,都需要考虑缓存失效后的缓存重建(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 ”。
读取缓存遇到 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 }
简单地说,就是:
线程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 异常,虽然此时缓存已经重建完毕。