-
Notifications
You must be signed in to change notification settings - Fork 20
expire 过期机制
Sivan_Xin edited this page Dec 13, 2023
·
2 revisions
在Redis中,我们的key和value存在过期时间,以防内存的持续增长带来的问题。
在SivanCache中,我们同样也实现了过期淘汰的方法。
为了避免缓存中的过期数据占用过多的空间,我们需要使用定期检查并删除他们;
为了避免定时任务对缓存的性能和并发造成影响,我们每次只清除一定数量的过期数据
过期淘汰我们提供两个接口,一个是在指定的时间过期,一个是在什么时间之后过期。
ISivanCache<K, V> expire(final K key, final long timeInMills);
/**
* 在指定的时间过期
* @param key key
* @param timeInMills 时间戳
* @return this
*/
ISivanCache<K, V> expireAt(final K key, final long timeInMills);我们把expire接口转换为expireAt,也就是现在的时间加上过期的时间。
这样就把两个接口全部转换为在指定的时间是否过期问题。
/**
* 设置过期时间
* @param key key
* @param timeInMills 毫秒时间之后过期
* @return this
*/
@Override
@SivanCacheInterceptor
public ISivanCache<K, V> expire(K key, long timeInMills) {
long expireTime = System.currentTimeMillis() + timeInMills;
// 使用代理调用
SivanCache<K,V> cachePoxy = (SivanCache<K, V>) SivanCacheProxy.getProxy(this);
return cachePoxy.expireAt(key, expireTime);
}
/**
* 指定过期信息
* @param key key
* @param timeInMills 时间戳
* @return this
*/
@Override
@SivanCacheInterceptor(aof = true)
public ISivanCache<K, V> expireAt(K key, long timeInMills) {
this.expire.expire(key, timeInMills);
return this;
}/**
* 缓存过期接口
* @author sivan
*/
public interface ISivanCacheExpire<K,V> {
/**
* 指定过期信息
* @param key key
* @param expireAt 什么时候过期
*/
void expire(final K key, final long expireAt);
/**
* 惰性删除中需要处理的 keys
* @param keyList keys
*/
void refreshExpire(final Collection<K> keyList);
/**
* 待过期的 key
* 不存在,则返回 null
* @param key 待过期的 key
* @return 结果
*/
Long expireTime(final K key);
}SivanCache初始化:
在我们的cache类进行初始化时,expire类同时进行初始化,初始化就是将expire放入定时任务。
/**
* 初始化
*
*/
public void init() {
this.expire = new SivanCacheExpire<>(this);
this.load.load(this);
// 初始化持久化
if(this.persist != null) {
new InnerSivanCachePersist<>(this, persist);
}
}这个定时任务我们采用轮询清理的方式,初始延迟100ms开始清理,每隔100ms就清理一次。
public SivanCacheExpire(ISivanCache<K, V> cache) {
this.cache = cache;
this.init();
}
/**
* 初始化任务
*
*/
private void init() {
EXECUTOR_SERVICE.scheduleAtFixedRate(new ExpireThread(), 100, 100, TimeUnit.MILLISECONDS);
}定时任务执行任务如下:
先判断存放过期key的Map是否为空,如果不为空,从Map中获取key进行处理。Map的key存放过期的key,value存放一个long类型的时间,代表过期时间。
如果当前时间大于过期时间,就将这个key删除。
为了每次定时任务的执行效率考虑,每次最多只清除100个过期的key。
/**
* 定时执行任务
*
*/
private class ExpireThread implements Runnable {
@Override
public void run() {
//1.判断是否为空
if(MapUtil.isEmpty(expireMap)) {
return;
}
//2. 获取 key 进行处理
int count = 0;
for(Map.Entry<K, Long> entry : expireMap.entrySet()) {
if(count >= LIMIT) {
return;
}
expireKey(entry.getKey(), entry.getValue());
count++;
}
}
}
/**
* 过期处理 key
* @param key key
* @param expireAt 过期时间
*
*/
private void expireKey(final K key, final Long expireAt) {
if(expireAt == null) {
return;
}
long currentTime = System.currentTimeMillis();
if(currentTime >= expireAt) {
expireMap.remove(key);
// 再移除缓存,后续可以通过惰性删除做补偿
V removeValue = cache.remove(key);
// 执行淘汰监听器
ISivanCacheRemoveListenerContext<K,V> removeListenerContext = SivanCacheRemoveListenerContext.<K,V>newInstance().key(key).value(removeValue).type(SivanCacheRemoveType.EXPIRE.code());
for(ISivanCacheRemoveListener<K,V> listener : cache.removeListeners()) {
listener.listen(removeListenerContext);
}
}
}我们采用轮询的方式来定期删除过期的key,可能会导致数据清理的不及时,进而获取到脏数据。
如果换一种思路,当我们真正关心某一个数据时,我们才来判断它有没有过期即可。这就是一种惰性删除策略。
这种策略的好处是:可以保证我们查找到的数据都是最新的数据,并且可以减少定时任务带来的负担。
当我们使用get获取数据时,我们才刷新缓存。
@Override
@SivanCacheInterceptor(evict = true)
@SuppressWarnings("unchecked")
public V get(Object key) {
//1. 刷新所有过期信息
K genericKey = (K) key;
this.expire.refreshExpire(Collections.singletonList(genericKey));
return map.get(key);
}这里判断一下大小,过期的map比较小,我们就循环过期的map,否则就循环我们正常的map。
@Override
public void refreshExpire(Collection<K> keyList) {
if(CollectionUtil.isEmpty(keyList)) {
return;
}
// 判断大小,小的作为外循环。一般都是过期的 keys 比较小。
if(keyList.size() <= expireMap.size()) {
for(K key : keyList) {
Long expireAt = expireMap.get(key);
expireKey(key, expireAt);
}
} else {
for(Map.Entry<K, Long> entry : expireMap.entrySet()) {
this.expireKey(entry.getKey(), entry.getValue());
}
}
}/**
* 过期测试
*
*/
@Test
public void expireTest() throws InterruptedException {
ISivanCache<String, String> cache = SivanCacheBs.<String,String>newInstance()
.size(3)
.build();
cache.put("1", "1");
cache.put("2", "2");
cache.expire("1", 40);
Assert.assertEquals(2, cache.size());
TimeUnit.MILLISECONDS.sleep(50);
Assert.assertEquals(1, cache.size());
System.out.println(cache.keySet());
}