OSCache 应用与源码分析

缓存配置oscache.properties
cache.memory是否使用内存缓存; true 或 false。默认为true; 如设置为false,那cache只能缓存到数据库或硬盘中。
cache.capacity缓存的最大数量。默认是不限制,cache不会移走任何缓存内容。负数也被视不限制。
cache.algorithm运算规则。为了使用规则,cache.capacity必须是指定的。有下面三种规则:
com.opensymphony.oscache.base.algorithm.LRUCache: last in first out(最后插入的最先调用)。
com.opensymphony.oscache.base.algorithm.FIFOCache: first int first out(最先插入的最先调用)。
com.opensymphony.oscache.base.algorithm.UnlimitedCache: cache中的内容将永远不会被丢弃。 默认选项
如果不指定cache.capacity,它将被设为默认选项即不限制。
cache.blocking是否同步。true 或者 false。一般设为true,避免读取脏数据。
cache.unlimited.disk指定硬盘缓存是否要作限制。默认false,disk cache capacity 和cache.capacity的值相同。
cache.persistence.class指定类是被持久化缓存的类。class必须实现PersistenceListener接口。
作为硬盘持久,可以实现HashDiskPersistenceListener接口。
注意:HashDiskPersistenceListener 和 DiskPersistenceListener 需要设定硬盘路径:cache.path。
cache.path硬盘缓存的路径。目录如果不存在将被建立。注意oscache应该要有权限写文件系统。
cache.persistence.overflow.only是否只有在内存不足的情况下才使用硬盘缓存。默认值false。
如果内存cache被允许的话(cache.memore=true),推荐设置为true。
cache.event.listeners用逗号隔开的class名列表,默认是不配置任何class的。
每个class必须实现以下接口中的一个或者几个CacheEntryEventListener:接收cache add/update/flush/remove事件
CacheMapAccessEventListener :接收cache访问事件。这个可以让你跟踪cache怎么工作。
com.opensymphony.oscache.plugins.clustersupport.BroadcastingCacheEventListener : 分布式的监听器。可以广播到局域网内的其他cache实例。
com.opensymphony.oscache.extra.CacheEntryEventListenerImpl :一个简单的监听器。在cache的生命周期中记录所有entry的事件。
com.opensymphony.oscache.extra.CacheMapAccessEventListenerImpl : 记录count of cache map events(cache hits,misses and state hits)
cache.key 在application 和 session的作用域时用于标识cache 对象的。默认值:"__oscache_cache"
如果代码中需要用到默认值时可以通使用com.opensymphony.oscache.base.Const.DEFAULT_CACHE_KEY 来取得
cache.use.host.domain.in.key当配置多个服务器时,想通过服备器名称自动生成cache key时,可将此属性设为true. 默认值为false
核心类和概念
角色作用
cache factory
缓存管理
AbstractCacheAdministrator
GeneralCacheAdministrator
ServletCacheAdministrator
生产Cache,同时管理用户配置的config,配置监听器列表和Cache的绑定。
负责获得 Cache Proxy ,兼有一些对 Cache Proxy 的管理功能。
子类ServletCacheAdministrator关联了一个ServletContext,以实现在Web容器中的存储和传值
cache proxy
缓存接口
Cache
ServletCache
OSCache缓存管理的核心,也是cache map的存放场所。
Cache Map 的代理,它主要负责从 Cache Map 中取得 存储指定的缓存对象,
如果缓存对象过期,那么就将缓存刷新,并向指定的监听者发送存 取事件。
子类ServletCache引入了一个scope的概念,用以管理不同的可见性缓存,存在application session级别。
cache map
缓存
AbstractConcurrentReadCache
FIFOCache LRUCacheUnlimitedCache
缓存存储map。存储了所有的缓存实体,是一个 OSCache 专有的 Map 实现,
根据指定的算法清除缓存,以及将缓存持久化到磁盘中。
cache entry
缓存条目
CacheEntry
map中存储的每一项。其内部包含了缓存条目的创建、修改时间,存储的key、value,group等属性
缓存对象的包装实体,它包装了缓存对象和刷新策略。
cache map 对 cache entry的管理
EntryUpdateState是cache entry当前所处状态的表示,OSCache尽量避免了使用synchronize,引入了许多状态参量。
 

 

 
 
典型应用场景
典型的“ 缓存对象 ”场景是:
应用调用Cache Factory获得Cache Proxy,然后应用将要缓存的对象以及刷新策略通过Cache Proxy存储到Cache Map中,并通知各个Listener。
 
典型的“取得缓存对象”的场景是:
应用调用 Cache Factory 获得 Cache Proxy ,然后给 Cache Proxy 的相应方法传入要获得的缓存对象的 key ,
Cache Proxy 会根据指定的刷新策略判断缓存是否过期,如果缓存没有过期,则返回缓存对象,如果缓存过期,则刷新缓存,
并向应用层抛出需要刷新的异常NRE,应用如果收到此异常,将重新计算内容并将内容缓存。
 
在缓存条目过期或者不存在的时候都会抛出NeedsRefreshException。
当这个异常出现,相应的cache map的key会被锁住,并且要访问它的所有其他线程都被block住了,
所以,这个时候一定要调用putInCache或者cancelUpdate,千万不能遗漏,否则就是一个死锁。
 
缓存刷新处理
在catch(NeedRefreshException)中进行 putInCache, 又在putInCache的catch(Exception)中做cancelUpdate, 因为我们不希望看到一个线程在 getFromCache的位置一直阻塞下去。
GeneralCacheAdministrator admin = new GeneralCacheAdministrator();
admin.putInCache("myKey", "HelloWorld"); ①
 
String myKey = "myKey";
String myValue;
int myRefreshPeriod = 1000;
try {
    // Get from the cache
    myValue = (String) admin.getFromCache(myKey, myRefreshPeriod); ②
} catch (NeedsRefreshException nre) {
    try {
        // Get the value (probably from the database)
        myValue = "This is the content retrieved." ;
        // Store in the cache
        admin.putInCache(myKey, myValue); ③
    } catch (Exception ex) {
        // We have the current content if we want fail-over.
        myValue = (String) nre.getCacheContent();
        // It is essential that cancelUpdate is called if the
        // cached content is not rebuilt
        admin.cancelUpdate(myKey);
    }
}
从对象缓存map中取出myKey对应的对象,两种情况可能发生: 
1 如果对象存在(先前putInCache)且没有过期,getFromCache正常返回。
2 如果对应的对象不存在或者过期,需要刷新缓存:      
   1) 请求的线程第一个探测到对象过期,抛给client一个NRE, 
         提示client需要对数据进行刷新putInCache。    
   2) 如果请求的线程并非第一个探测到对象不存在
          A.之前探测到的线程没有进行刷新处理,直接阻塞。
          B.之前探测到的线程进行了刷新处理,抛出NRE。
   3) 如果请求的线程并非第一个探测到对象过期
          A.之前探测到的线程没有进行刷新处理,直接阻塞。
          B.之前探测到的线程进行了刷新处理:
               I.如果blocking=false 抛出NRE,采取补救措施取可能过期内容 
              II.如果blocking=true,那么该线程会在此阻塞,
                  直到putInCache/ cancelUpdate 在另一个线程中被调用。
 
源码分析
Cache管理类
// 私有化Cache实例,不直接操作Cache实例,而是通过管理类对缓存管理:put,get,flush。当然也可以通过getCache获取Cache实例对Cache操作
private Cache applicationCache = null
 
public GeneralCacheAdministrator(Properties p) {
    super (p); //调用父类AbstractCacheAdministrator的构造函数
    createCache();
}
 
protected AbstractCacheAdministrator(Properties p) {
    loadProps(p); //加载配置文件,封装到对象 Config里(通过 Config获取配置信息)
    initCacheParameters(); //初始化参数,将 Config获取到的配置信息赋值给几个重要的属性,目的是在类或子类中可以直接使用已经赋值好的变量
}
 
private void createCache () {
    applicationCache = new Cache(isMemoryCaching(), isUnlimitedDiskCache(), isOverflowPersistence(), isBlocking(), algorithmClass , cacheCapacity );    
    //创建完Cache实例后,将配置文件配置的监听器应用到Cache上
    configureStandardListeners( applicationCache);
}
 
getFromCache 
读取缓存数据,指的是从CacheMap[缓存]中获取对应的CacheEntry[缓存条目],再获取CacheEntry的内容[缓存对象]
 
updateStates:map对象。它提供了cache中的所有被同步访问的cacheEntry对象的状态集合, 用来协调并发访问(修改/读取)同一个缓存条目。
key就是cache中该cacheEntry对象对应的key。value是updatestate 对象。
 

OSCache采用引用计数状态量机制,解决了多线程并发访问缓存的问题,同时,没有任何语句锁住整个cache map,在高并发的情况下不会有太大的性能损失。

当一个过期的缓存条目被请求,或者观察到缓存未命中,cache会检查这个map
当一条缓存条目正在被另一个线程更新,那么有两种策略,根据配置项cache.blocking的配置,
要么等待更新完成(阻塞策略),要么返回已经过时的缓存内容(非阻塞策略)。

为了避免数据争用(并发读取),map里面的值在某线程操作的过程中不能消失,
因此updateStates实际的作用是显式引用计数(每一个updateState里面都有一个计数器),
在所有线程都对指定的key完成存取和更新以后,map的这条entry才能被清除掉。

updateStates存储的是所有 处于更新状态的(UPDATE_IN_PROGRESS) 缓存条目,   
也就是说如果某一个缓存条目没有被更新(NOT_YET_UPDATING),则不会存放在这个map里。      
或者当前缓存条目更新完毕(UPDATE_COMPLETE),则被从这个map里踢除掉,因为不再处于正在更新的状态!
 
根据key读取CacheMap中的缓存对象
[1] 缓存中不存在key对应的缓存条目,都没有怎么获取?
[2] key对应的updateState已经存在,说明获取该key对象的线程又多了一个
[3] updateState为等待更新或被取消更新,设置状态为正在更新,并将线程引用计数+1,抛出NRE异常,提示客户端刷新缓存
[4] 缓存过期,如果缓存内容为空,访问类型为未命中,不为空,访问类型为命中过期数据,都会抛出NRE异常
[5] 缓存数据存在,如果没有过期,直接返回。如果过期,当其他线程正在更新,如果配置blocking=true则等待更新完成并获取最新数据,否则直接返回过期数据。
[6] 其他线程正在更新缓存,但是又取消了更新,当前线程设置状态为正在更新,并将线程引用计数+1,抛出NRE异常,提示客户端刷新缓存
[7] 同步块结束后,将[2]读取缓存的线程计数-1,如果没有线程引用updateState,则释放updateState对象
[8] 客户端捕获到NRE异常,刷新缓存,可以调用putInCache()或cancleUpdate(),将引用计数-1,释放updateState对象
 
读取流程
判断缓存是否过期,如果过期,那么会从更新状态缓存中查找更新状态,如果没查到那么创建一个,如果已经存在了,将引用计数+1。 ->getUpdateState(key)
如果缓存还没有进行更新,那么将更新状态设置为UPDATE_IN_PROGRESS,并且再次将引用计数+1,此时: 
(1) 在设定完更新状态后,读取缓存的线程会将引用计数-1,如果没有线程此时再引用了那么在更新状态缓存中移除此项。 -> finally里
(2) 如果其他线程在更新缓存的时候会将引用计数-1,如果没有线程此时再引用了那么在更新状态缓存中移除此项。 
这样,通过上边的两个过程,就将引用计数变为0了。
 
public  Object getFromCache(String key,  int  refreshPeriod, String cronExpiry)  throws  NeedsRefreshException {
      // 先尝试在缓存CacheMap中查找。和putInCache一样,如果存在则返回缓存条目,
    // 不存在则新建一个缓存条目并返回.因为缓存CacheMap中不存在这个缓存条目,缓存对象也为空[1]
    CacheEntry cacheEntry =  this .getCacheEntry(key,  null ,  null );
 
    Object content = cacheEntry.getContent();  // 缓存内容,实际缓存的对象[5]
    CacheMapAccessEventType accessEventType = CacheMapAccessEventType.  HIT ;  // 缓存访问事件类型,默认是"缓存命中"
 
     boolean  reload =  false ;  //是否重新加载,如果另外的线程正在更新,等待更新完毕后,reload=true,重加获取缓存里的对象
 
     // Check if this entry has expired or has not yet been added to the cache.If so, 缓存条目过期/还没加入到缓存中[1]
     // we need to decide whether to block, serve stale content or throw a NeedsRefreshException 阻塞,获取过期数据,抛出NRE
     if  (  this .isStale(cacheEntry, refreshPeriod, cronExpiry)) {
         // Get access to the EntryUpdateState instance and increment the usage count during the potential sleep
         // 检测updateStates中是否存在该key,如果没有,则为该key新建一个EntryUpdateState对象并将其put到updateStates中;
         // 如果存在该key,则需要为该对象的同时被访问的线程计数+1,表示读取该key对象的线程又多了一个[2:+1]
        EntryUpdateState updateState = getUpdateState(key);
         try  {
             //  住updateState,即对updateState进行同步
             synchronized  (updateState) { 
                // A.若状态为"等待更新",则该对象尚未被其他线程访问
                 // A.若状态为"被取消",则表明已有其他的线程对该CacheEntry对象进行访问,但当前非"正在被更新"的状态
                 if  (updateState.isAwaitingUpdate() || updateState.isCancelled()) {
                     // No one else is currently updating this entry - grab ownership
                    //  将更新状态设置为正在更新UPDATE_IN_PROGRESS, 再次将线程引用计数+1 [3:+1] 
                     // 只是更新状态并没有真正更新,再说了这里只是读取操作所以肯定不会更新 
                    // 先了解下此时状态:等待更新,实际是等待被更新(被动的,读取操作显然不会主动更新,而是客户端主动更新)
                    // 这里引用计数+1,只是提示客户端更新缓存条目,当客户端更新完缓存条目,会将引用计数-1,这样就完成了一次存取操作
                    updateState.startUpdate(); 
                    
                    // 新建缓存条目,还没放入缓存中 [1] ,缓存的对象内容为空, 因此将此次访问记录为未命中(缓存中都没有数据,显然没有命中)
                     if  (cacheEntry.isNew()) {
                        accessEventType = CacheMapAccessEventType.  MISS ;
                    } 
                     // 若对象内容不为空,则表明该次访问的数据已经过期,记录为命中过期数据(缓存中有数据但是过期了)
                    // 进入同步块时判断缓存条目是否过期 当数据有效时,执行到这里,既然不是新建的缓存条目,就一定是过期的数据
                     else  {
                        accessEventType = CacheMapAccessEventType.  STALE_HIT ;
                    }
                    // 这两种类型,在后面的处理中[4],都会抛出NRE异常,提示客户端刷新缓存。未命中和命中过期数据都不是我们想要的。
                }
                // B.若状态为" 正在更新 ",表明有其他线程正在对该对象进行更新
                 else  if  (updateState.isUpdating()) {
                     // Another thread is already updating the cache. We block if this is a new entry, or blocking mode is enabled.
                     // Either putInCache() or cancelUpdate() can cause this thread to resume.
                      // 在配置为阻塞策略的情况下,或这是一个新建的缓存条目 [1] ,都会阻塞, 等待更新完成后notify
 
                    // 1:其他线程正在对该对象进行更新,是否需要等待其他线程更新完毕读取最新数据,或者直接返回过期数据 [5]
                     // 2:其他线程取消对该对象更新,此时需要抛出NRE异常;
                     // 3:其他线程对该对象的更新完毕,此时可以返回最新数据。
                     if  (cacheEntry.isNew() ||  blocking ) {
                         do  {
                             try  {
                                // 如果对象调用了wait方法就会使持有该对象的线程把该对象的控制权交出去(交给正在更新该缓存条目的线程),然后处于等待状态
                                // 当更新缓存条目的线程更新完毕,即在putInCache()的completeUpdate(key)中调用updateState.notifyAll()
                                 // notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行,
                                // 即当前线程继续判断updateState的状态,两种状态:更新被取消或更新完成
                                updateState.wait();
                            }  catch  (InterruptedException e) {
                            }
                        }  while  (updateState.isUpdating());
 
                         // 如果其他正在更新的线程取消了更新(虽然你说你正在更新但你又取消这个操作),那么缓存仍然是过期的,执行和前面分支相同的操作
                         if  (updateState.isCancelled()) {
                             // The updating thread cancelled the update, let this one have a go. 将更新操作让给当前的线程来更新
                             // This increments the usage count for this EntryUpdateState instance 引用计数+1 [6:+1]
                            updateState.startUpdate();
                           
                             if  (cacheEntry.isNew()) {
                                accessEventType = CacheMapAccessEventType.  MISS ;
                            }  else  {
                                accessEventType = CacheMapAccessEventType. STALE_HIT  ;
                            }
                            //  抛出NRE异常 [4]
                        } 
                        // 如果没取消更新的话,那么这时updateState的状态应该是complete,即其他 线程更新完毕, 此时可以返回最新数据
                        // 其他线程在更新完毕时,会将同步访问计数-1,表明一个线程对该对象的访问结束。
                         else  if  (updateState.isComplete()) {
                             reload =  true ;
                        }  else  {
                             log .error( "Invalid update state for cache entry "  + key);
                        }
                    }
                     // 如果缓存中有数据(此时是过期的了),且blocking=false,下面的逻辑除了finally外都不执行,返回过期数据content [5]
                } 
                // C.若状态为" 完成更新 ",则表明有其他线程调用了completeUpdate(key)将该对象的状态改为"完成更新" 
                // 数据是过期的,在进入同步块后发现另外的线程已将数据更新完毕,当前线程不需要做任何操作,只需要从缓存中获取最新的数据
                else  {
                     reload =  true ;
                }
            }
        }  finally  {
             // Make sure we release the usage count for this EntryUpdateState since we don't use it anymore.
             // If the current thread started the update, then the counter was increased by one in startUpdate()
             // 在设定完更新状态后,读取缓存的线程会将引用计数-1[7:-1]对应 [2:+1]
            // 如果没有线程此时再引用了,updateStates就会移除此项updateState,释放updateState对象
            releaseUpdateState(updateState, key);
        }
    }
 
     // If reload is true then another thread must have successfully rebuilt the cache entry 
    // 其他线程已经重建缓存条目了,可以重新载入缓存中的数据了
     if  (reload) {
        cacheEntry = (CacheEntry)  cacheMap .get(key);
 
         if  (cacheEntry !=  null ) {
            content = cacheEntry.getContent();
        }  else  {
             log .error( "Could not reload cache entry after waiting for it to be rebuilt" );
        }
    }
 
    dispatchCacheMapAccessEvent(accessEventType, cacheEntry,  null );  // 发送缓存事件
 
     // If we didn't end up getting a hit then we need to throw a NRE 如果没有命中数据(未命中或命中过期),抛给客户端需要刷新数据的异常
    // 上面updateState.startUpdate()提示客户端刷新缓存,都会执行到这里 [4]
     if  (accessEventType != CacheMapAccessEventType.  HIT ) { 
         throw  new  NeedsRefreshException(content);
    }
    // 当抛出异常时,没有将同步访问计数减一,因此在设置状态为“正在更新”时增加的计数不会被减去,该 updateState对象将被保留在 updateStates
     // 在捕获NRE异常的地方一定要调用completeUpdate或cancleUpdate方法,将计数-1[8:-1]对应 [3:+1]或 [6:+1] ,这样就会释放   updateState 对象
 
     return  content;
}
 
putInCache写入缓存
public  void  putInCache(String key, Object content, String[] groups, EntryRefreshPolicy policy, String origin) {
     // 首先查找这个key对应的缓存条目在缓存中是否已经存在,没有就新建CacheEntry,注意此时缓存对象的内容还没有设置
    CacheEntry cacheEntry =  this .getCacheEntry(key, policy, origin);
   
     // 判断是否是新创建的缓存
     boolean  isNewEntry = cacheEntry.isNew();
     // [CACHE-118] If we have an existing entry, create a new CacheEntry so we can still access the old one later
     // 如果不是新的缓存也会新建一个CacheEntry,因为老的缓存值在后边也能访问到
     if  (!isNewEntry) {
        cacheEntry =  new  CacheEntry(key, policy);
    }
 
    cacheEntry.setContent(content);
    cacheEntry.setGroups(groups);
     cacheMap .put(key, cacheEntry);  // 放入缓存:将content放入CacheEntry,将CacheEntry放入CacheMap
 
     // Signal to any threads waiting on this update that it's now ready for them in the cache!
     // 通知其他在等待值的线程结束等待,现在该key对应的缓存条目已经准备好了,其他线程可以获取最新的缓存内容了
    completeUpdate(key);
 
     // 针对缓存事件的listener发送事件
     if  (  listenerList .getListenerCount() > 0) {
        CacheEntryEvent event =  new  CacheEntryEvent( this , cacheEntry, origin);
 
         if  (isNewEntry) {
            dispatchCacheEntryEvent(CacheEntryEventType.  ENTRY_ADDED , event);  // 新添加缓存
        }  else  {
            dispatchCacheEntryEvent(CacheEntryEventType.  ENTRY_UPDATED , event);  // 更新缓存
        }
    }
}
 
 

Web应用集成
缓存对象:直接调用API的接口即可
缓存部分页面
复制oscache.tld到WEB-INF下,在web.xml添加
< taglib >
    < taglib-uri > oscache </ taglib-uri >
     < taglib-location > /WEB-INF/oscache.tld </ taglib-location >
  </ taglib >
在jsp页面使用标签<%@ taglib uri="oscache" prefix="cache"%>
使用Oscache的标签<oscache></oscache>来进行页面的局部缓存

缓存整个页面:用CashFilter实现页面级缓存,可缓存单个文件、缓存URL pattern和自己设定缓存属性的缓存。
主要用于对web应用中的某些动态页面进行缓存,尤其是那些需要生成PDF格式文件/报表、图片文件等的页面,
不仅减少了数据库的交互、减少数据库服务器的压力,而且对于减少web服务器的性能消耗有很显著的效果。

在web.xml中进行配置来决定缓存哪一个或者一组页面,而且还可以设置缓存的相关属性。
[注] 只有客户访问时返回http头信息中代码为200(也就是访问已经成功)的页面信息才能够被缓存。
<filter>
    <filter-name>CacheFilter</filter-name>
    <filter-class>com.opensymphony.oscache.web.filter.CacheFilter</filter-class>
    <init-param>
        <param-name>time</param-name>
        <param-value>600</param-value>
    </init-param>
    <init-param>
        <param-name>scope</param-name>
        <param-value>session</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CacheFilter</filter-name>
    <url-pattern>*.jsp</url-pattern> <!-- 对所有jsp页面内容进行缓存-->
</filter-mapping>
 
 
cache标签(缓存部分页面)
第一次请求到达时,标签中的内容被处理并且缓存起来,当下一个请求到达时,缓存系统会检查这部分内容的缓存是否已经失效,
如果符合下面四项中的任何一项,被缓存的内容视为已经失效,这时被缓存的内容将被重新处理并且返回处理过后的信息,
如果被缓存的内容没有失效,那么返回给用户的将是缓存中的信息。 主要是以下几项:
(1)缓存时间超过了cache标签设置的time或者duration属性规定的超时时间 
(2)cron属性规定的时间比缓存信息的开始时间更晚 
(3)标签中缓存的内容在缓存后又被重新刷新过 
(4)其他缓存超期设定 
key
标识缓存内容的关键词。在指定的作用范围内必须是唯一的。默认的key是被访问页面的URI和后面的请求字符串。
scope 缓存发生作用的范围,application(默认)或者session
time
缓存内容的时间段,单位是秒,默认是3600秒,如果设定一个负值,那么这部分被缓存的内容将永远不过期。
duration
指定缓存内容失效的时间,是相对time的另一个选择,可以使用简单日期格式或者符合USO-8601的日期格式。如:duration='PT5M' duration='5s'
cron指定缓存内容失效表达式
refresh
false/true。如果refresh属性设置为true,不管其他的属性是否符合条件,这部分被缓存的内容都将被更新,这给编程者一种选择,决定什么时候必须刷新。
mode
如果不希望被缓存的内容增加到给用户的响应中,可以设置mode属性为"silent"
此时被缓存的部分不在页面上显示,而其它任意的mode属性值都会将缓存的部分显示到页面上。
groups
指定当前cache标签所属的组,可使用“,”分割组名。这样就可以对缓存项进行分组了。
如果缓存项依赖于应用的其它部分或其它数据,分组就有了用武之地——当这种依赖改变时(刷新相关的组),这个组的所有缓存项都将过期。
language
使用ISO-639定义的语言码来发布不同的缓存内容(under an otherwise identical key)。
refreshpolicyclass
指定自定义的刷新策略类的全限定类名。这个类继承自com.opensymphony.oscache.web.WebEntryRefreshPolicy
refreshpolicyparam
指定任意需要传给refreshpolicyclass的参数。如果没有指定refreshpolicyclass,则这个值不起作用。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值