每日一课【后端缓存系统】
后端缓存可以分为分布式缓存、本地缓存两种。
两者区别:

-
分布式缓存需要远程调用,多了一次网络开销。
-
同样情况下,分布式缓存要慢于本地缓存。
-
本地缓存不能跨区域共享。
适用场景:

缓存与数据库同步问题:
第一种:Cache Aside

优点:简单易实现。
缺点:需要自己维护数据更新后的逻辑,对业务代码有侵入。
第二种:Read/Write Through
本质上和第一种是一样的,不过使用了缓存服务代理,不需要自己维护,对业务代码无侵入。
不过需要封装缓存和数据库的同步机制,实现更复杂。
第三种:Write Back
在更新数据时只更新缓存,然后异步更新数据库。
优点:性能高,因为读写的是内存。
缺点:数据不是强一致性,如果缓存宕机,数据可能丢失。
适合对数据一致性要求不高的场景。

总结:
-
Cache Aside:适用于轻量级应用
-
Read/Write Through:适合需要频繁使用缓存的应用
-
Write Back:适合数据一致性不高的场景
缓存命中率:

命中率越高,缓存的效率越高,到数据库的请求就越少。
变更频率高的数据不适合缓存。因为数据经常变更会导致缓存失效。
缓存过期时间:缓存空间需设置上限,为数据设置过期时间,以维护存储空间的可用性。
常见的如LRU算法。
实现一个简单的本地缓存:
public class LocalCache<K,V> implements Cacheable<K,V>{
// 缓存数据的集合
private Map<K, Data> map = new ConcurrentHashMap<>();
// 每60s请理过期数据,延迟1s执行
public LocalCache(){
Timer t = new Timer();
t.schedule(new ExpiringChecker(), 1000, 60 * 1000);
}
@Override
public V get(K key) {
Data data = map.get(key);
if (isExpired(data)) {
delete(key);
return null;
}
return data.getVal();
}
private boolean isExpired(Data data) {
if (data == null) {
return true;
}
return data.getExpire() > 0 && Instant.now().toEpochMilli() > data.getExpire();
}
/**
* 添加数据
* @param key
* @param val
* @param expire 过期时间,单位s
*/
@Override
public void set(K key, V val, long expire) {
map.put(key, new Data(val, expire));
}
@Override
public void delete(K key) {
map.remove(key);
}
@Override
public boolean exist(K key) {
return map.containsKey(key);
}
// 数据包装类,保存val的时间戳
private class Data{
V val;
long expire;
Data(V val, long expire) {
this.val = val;
this.expire = expire > 0 ? Instant.now().toEpochMilli() + expire * 1000 : expire;
}
public V getVal() {
return val;
}
public void setVal(V val) {
this.val = val;
}
public long getExpire() {
return expire;
}
public void setExpire(long expire) {
this.expire = expire;
}
}
/**
* 过期数据检测器
*/
private class ExpiringChecker extends TimerTask{
@Override
public void run() {
map.forEach((k, v) -> {
if (isExpired(v)) {
delete(k);
}
});
}
}
}