Skip to content

expire 过期机制

Sivan_Xin edited this page Dec 13, 2023 · 2 revisions

expire 过期淘汰

在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());
}

Clone this wiki locally