过期是一个非常有用的特性,比如我希望登录信息放到redis中,30min之后失效;或者单日的累计信息放在redis中,在每天的凌晨自动清空。
缓存过期
在ICache接口中定义两个方法
接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
ICache<K, V> expire(final K key, final long timeInMills);
ICache<K, V> expireAt(final K key, final long timeInMills);
|
实现
将多久之后过期通过计算转化成什么时候过期的问题。
1 2 3 4 5 6 7 8 9 10
| @Override public ICache<K, V> expire(K key, long timeInMills) { long expireTime = System.currentTimeMillis() + timeInMills; return this.expireAt(key, expireTime); } @Override public ICache<K, V> expireAt(K key, long timeInMills) { this.expire.expire(key, timeInMills); return this; }
|
过期策略
接口
在方法中调用expire
就可以执行过期策略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public interface ICacheExpire<K, V> {
void expire(final K key, final long expireAt);
void refreshExpire(final Collection<K> keyList);
Long expireTime(final K key); }
|
实现
简单实现
最简单的过期思路就是开启一个定时任务,然后对缓存进行轮询,将过期的信息清空。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| public class SingleCacheExpire<K, V> implements ICacheExpire<K, V> {
private final Map<K, Long> expireMap = new HashMap<>();
public static final int LIMIT = 100;
private final ICache<K, V> cache;
private static final ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
public SingleCacheExpire(ICache<K, V> cache) { this.cache = cache; this.init(); }
private void init() { service.scheduleAtFixedRate(new ExpireThread(), 100, 100, TimeUnit.MILLISECONDS); }
@Override public void expire(K key, long expireAt) { expireMap.put(key, expireAt); }
@Override public void refreshExpire(Collection<K> keyList) { if(keyList.isEmpty()) { return; }
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()); } } }
@Override public Long expireTime(K key) { return expireMap.get(key); }
private class ExpireThread implements Runnable {
@Override public void run() { if(expireMap.isEmpty()) return;
int count = 0; for(Map.Entry<K, Long> entry : expireMap.entrySet()) { if(count >= LIMIT) { return; } expireKey(entry); count++; } }
private void expireKey(Map.Entry<K, Long> entry) { final K key = entry.getKey(); final Long expireAt = entry.getValue(); long currentTime = System.currentTimeMillis(); if(currentTime >= expireAt) { expireMap.remove(key); cache.remove(key); } } } }
|
使用有序Map优化
第一种做法通过轮询缓存会有很多弊端,如果过期的应用场景不多,那么经常轮训的意义实际不大。
比如我们的任务 99% 都是在凌晨清空数据,白天无论怎么轮询,纯粹是浪费资源。
那有没有什么方法,可以快速的判断有没有需要处理的过期元素呢?
答案是有的,那就是排序的MAP。
我们换一种思路,让过期的时间做 key,相同时间的需要过期的信息放在一个列表中,作为 value。
然后对过期时间排序,轮询的时候就可以快速判断出是否有过期的信息了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| public class SortedCacheExpire<K, V> implements ICacheExpire<K, V> {
private static final int LIMIT = 100;
private final Map<Long, List<K>> sortMap = new TreeMap<>((o1, o2) -> (int) (o1 - o2));
private final Map<K, Long> expireMap = new HashMap<>();
private final ICache<K, V> cache;
private static final ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
public SortedCacheExpire(ICache<K, V> cache) { this.cache = cache; this.init(); }
private void init() { service.scheduleAtFixedRate(new ExpireThread(), 1, 1, TimeUnit.SECONDS); }
@Override public void expire(K key, long expireAt) { List<K> keys = sortMap.get(expireAt); if(keys == null) { keys = new ArrayList<>(); } keys.add(key);
sortMap.put(expireAt, keys); expireMap.put(key, expireAt); }
@Override public void refreshExpire(Collection<K> keyList) {
}
@Override public Long expireTime(K key) { return null; }
private class ExpireThread implements Runnable { @Override public void run() { if (sortMap.isEmpty()) return;
int count = 0; for(Map.Entry<Long, List<K>> entry : sortMap.entrySet()) { final Long expireAt = entry.getKey(); List<K> expireKeys = entry.getValue();
if(expireKeys.isEmpty()) { sortMap.remove(expireAt); continue; } if(count >= LIMIT) { return; }
long currentTime = System.currentTimeMillis(); if(currentTime >= expireAt) { Iterator<K> iterator = expireKeys.iterator(); while (iterator.hasNext()) { K key = iterator.next(); iterator.remove(); expireMap.remove(key);
cache.remove(key); count++; } } else { return; } } } } }
|
惰性删除
原因
类似于redis,我们采用定时删除的方案,就有一个问题:可能数据清理的不及时。
那当我们查询时,可能获取到到是脏数据。
于是可以这样想,当我们关心某些数据时,才对数据执行对应的过期策略,这样压力会小很多。
1 2 3 4
| public V get(Object key) { this.cacheExpire.refreshExpire(Collections.singletonList((K) key)); return map.get(key); }
|
刷新实现
实现原理也比较简单,就是一个循环,然后作删除即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Override public void refreshExpire(Collection<K> keyList) { if(keyList.isEmpty()) { return; } 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()); } } }
|