过期是一个非常有用的特性,比如我希望登录信息放到redis中,30min之后失效;或者单日的累计信息放在redis中,在每天的凌晨自动清空。
缓存过期
在ICache接口中定义两个方法
 接口
接口
| 12
 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);
 
 | 
 实现
实现
将多久之后过期通过计算转化成什么时候过期的问题。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | @Overridepublic 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就可以执行过期策略。
| 12
 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);
 }
 
 | 
 实现
实现
简单实现
最简单的过期思路就是开启一个定时任务,然后对缓存进行轮询,将过期的信息清空。
| 12
 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。
然后对过期时间排序,轮询的时候就可以快速判断出是否有过期的信息了。
| 12
 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,我们采用定时删除的方案,就有一个问题:可能数据清理的不及时。
那当我们查询时,可能获取到到是脏数据。
于是可以这样想,当我们关心某些数据时,才对数据执行对应的过期策略,这样压力会小很多。
| 12
 3
 4
 
 | public V get(Object key) {this.cacheExpire.refreshExpire(Collections.singletonList((K) key));
 return map.get(key);
 }
 
 | 
刷新实现
实现原理也比较简单,就是一个循环,然后作删除即可。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | @Overridepublic 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());
 }
 }
 }
 
 |