1. 引言
应用服务端开发干的事情基本都是围绕数据的读和写展开的,基本上多数使用的是DB + Redis方案。这样做无可厚非,对于多数ToB场景和量不大的ToC场景也基本够用。但是在实际的工作中,由于各种问题,例如:无脑生吃Redis(什么东西都往Redis中放,甚至同一个数据由于不同的开发导致不同key的冗余缓存),Redis使用不当(所有的都是Get/Set,从不用HSet等方式去减少Key的数量),导致Redis中数据太多,从而导致性能低下(大家可以测试下10亿个Key之后Redis Get的性能)、服务器成本太高(内存资源贵啊!)。这些也认了,但是如果Redis发生故障,缓存雪崩后把DB打死,引发生产环境故障,那么就不好玩了。人是最不可靠的东西,多数的中小公司都是快速铺业务为主,什么代码Review,技术方案评审,监控告警等都是后置的,因此要把redis用好,涉及到的方方面面也不少。不过这里最核心的是提出以下问题:
1、Redis服务器成本太高
2、Redis故障导致的缓存雪崩
3、用好Redis涉及到方方面面也不少
通常的解决方案为:在Redis之上增加一个更稳定和高效的本地缓存,并且具备合理的缓存淘汰策略使得在有限内存资源的环境下获得较高的缓存命中率。 不过我个人认为,多数情况下使用Redis的必要性并不是那么大,除非你有强分布式缓存的使用场景,例如:实现一个二维码扫码登录。优先DB + LocalCache 的方案不但能节省服务器资源、而且还能少依赖一个中间件,这样使得整个系统更加轻量,那么,在私有化部署、国产化适配、跨平台等方面就具备天生优势。
本文先对常见缓存淘汰策略进行简介,然后再对常用的LRU和LFU代码实现及使用给出实例。
2. 常见缓存淘汰策略简介
常见的缓存淘汰策略一般来说主要为以下几种:
- FIFO(First In First Out)
FIFO先进先出比较符合常规思维,先来的先服务,在操作系统的设计中也常见该思想,但在缓存中使用时会出现将先到的高频数据被淘汰而保留了后到的低频数据的问题,导致缓存命中率低。除非极特殊的场景下,一般的业务应用场景中都不会采取该种缓存淘汰策略。 - LRU(Least Recently Used)
LRU最近最少使用,如果一个数据在最近一段时间内没有被访问那么它在接下来被访问的可能性也很小,当空间已满的时候优先将最近最少被访问的数据淘汰,这种缓存淘汰策略复合多数业务应用场景,例如,一个群的信息最近被访问了,那么一般来说将来其被访问的几率较大,例如:活跃的群。反之如果一个群的信息很久不被访问,则将来其被访问的几率也较小,例如:“死群”。虽然LRU复合多数业务应用场景,但是有一个明显的问题:如果短时间内大量的冷数据被访问则将导致热数据被淘汰。 - LFU(Least Frequently Used)
LFU最近最少频率使用,如果一个数据在最近一段时间内使用次数很少那么它在接下来被使用的概率也很小。与LRU的区别是,LRU基于访问时间,LFU基于访问频率。LFU利用额外的空间记录每个数据的使用频率,然后选出频率最低进行淘汰,解决了LRU不能处理时间段的问题。LFU可以处理LRU因为冷数据突增带来的缓存污染问题,但存在的问题是数据访问模式如果改变,这种算法命中率会下降,未解决该问题Caffeine提出了一种更高效的近似LFU准入策略的缓存结构TinyLFU及其变种W-TinyLFU,W-TinyLFU记录了近期访问记录的频率信息,不满足的记录不会进入到缓存,并使用Count-Min Sketch算法记录访问记录的频率信息,依靠衰减操作,来尽可能的支持数据访问模式的变化。
这三种算法的命中率一个比一个好,但实现成本也一个比一个高,实际实现中一般选择中间的LRU或者LFU。
3. 常用本地缓存代码实现及使用实例
3.1 LRU(LruHashMap)
上面提到LRU最大的问题在于冷数据突增带来的缓存污染问题,其实真实情况往往不会那么糟糕,多数时候这种情况的发生源于外部的暴力穷举,例如,对外暴露了某个数字类型的ID,然后被人基于某个较大范围的暴力穷举攻击。在真实的业务场景中,少数情况会涉及冷数据的大量涌入,例如:给所有的群发消息,给所有的人发消息,这类场景一般和广播相关。关于暴力穷举,这是一个基本的安全问题,抛开缓存淘汰策略问题,这种安全问题加固也应当被处理。之前写了一个篇Blog:高效数据加解密(混淆)方法介绍及示例代码实现,里面的给出解决该问题的实例。因此,虽然都知LRU并不完美,但如果能用更轻量的方式去实现并且满足要求,这就应该被倡导,下面给出一个LRU的简易实现:LruHashMap,其底层基于LinkedHashMap,因此把它当成LinkedHashMap去用可以可以,只是增加一个缓存淘汰监听处理,当然你也可以不使用(之前用Java写了一个分布式文件服务的半成品,里面使用LruHashMap去缓存打开的文件句柄,当热数据过期之后,通过该监听处理去关闭文件句柄,这样可以避免频繁的开关文件句柄,参考:StorageHandler,一个用Java开发的分布式高性能文件服务)。
最后补充说明一下,LruHashMap的实现虽然相对简陋,但是经历过真实商用,因此一般情况下大家可以直接收过去放心使用。
测试类:LruHashMapTest
package cache.lru;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* LruHashMapTest
* 直接把LruHashMap当成一个LinkedHashMap使用即可;
*
* @author chenx
*/
public class LruHashMapTest {
private LruHashMap<String, String> cache = null;
private static final int LOCK_SIZE = 8;
private static final ReadWriteLock[] CACHE_LOCKS = new ReadWriteLock[LOCK_SIZE];
public LruHashMapTest() {
/**
* maxSize: 容量
* Func: 淘汰监听处理
* duration: 过期时间(毫秒)
*/
this.cache = new LruHashMap<>(1000, (key, value) -> this.onDataEvicted(key, value), 5 * 1000L);
// init ReadWriteLocks
for (int i = 0; i < LOCK_SIZE; i++) {
CACHE_LOCKS[i] = new ReentrantReadWriteLock();
}
}
/**
* getCacheSize
*
* @return
*/
public int getCacheSize() {
int size = this.cache.size();
System.out.println("the cache size is: " + size);
return size;
}
/**
* setCache
* 由于底层用LinkedHashMap实现,因此如果要线程安全,则需要自己加锁;
*
* @param key
* @param value
*/
public void setCache(String key, String value) {
Lock wLock = getLock(key).writeLock();
wLock.lock();
try {
this.cache.put(key, value);
} finally {
wLock.unlock();
}
}
/**
* getData
* 由于底层用LinkedHashMap实现,因此如果要线程安全,则需要自己加锁;
*
* @param key
* @return
*/
public String getData(String key) {
Lock wLock = getLock(key).writeLock();
wLock.lock();
try {
if (this.cache.containsKey(key)) {
String value = this.cache.get(key);
System.out.println("hit the cache, key: " + key + ", value: " + value);
return value;
}
String value = this.mockGetDataFromDb(key);
this.cache.put(key, value);
return value;
} finally {
wLock.unlock();
}
}
/**
* invalidate(清除指定缓存)
*
* @param key
*/
public void invalidate(String key) {
this.cache.remove(key);
System.out.println("remove the cache done: " + key);
}
/**
* getLock
*
* @param key
* @return
*/
private static ReadWriteLock getLock(String key) {
int lockId = Math.abs(key.hashCode() % LOCK_SIZE);
return CACHE_LOCKS[lockId];
}
/**
* mockedGetDataFromDb(模拟从数据库获取数据)
*
* @param key
* @return
*/
private String mockGetDataFromDb(String key) {
String value = key + "-value";
System.out.println("get the data from db and put into cache, key: " + key + ", value: " + value);
return value;
}
/**
* onDataEvicted
*
* @param key
* @param value
*/
public void onDataEvicted(String key, String value) {
System.out.println("evicted a cache, key: " + key + "value: " + value);
}
public static void main(String[] args) throws InterruptedException {
LruHashMapTest lruHashMapTest = new LruHashMapTest();
System.out.println("-------常规测试-------");
lruHashMapTest.setCache("key1", "key1-value");
lruHashMapTest.getData("key1");
lruHashMapTest.getData("key2");
lruHashMapTest.getData("key3");
lruHashMapTest.getData("key4");
lruHashMapTest.getData("key5");
lruHashMapTest.getData("key6");
System.out.println("------超时淘汰测试--------");
Thread.sleep(10000L);
System.out.println("------容量淘汰测试--------");
for (int i = 0; i < 20; i++) {
lruHashMapTest.getData("key" + i);
Thread.sleep(500L);
}
lruHashMapTest.getCacheSize();
System.out.println("--------清除指定缓存测试-------");
lruHashMapTest.getData("key1");
lruHashMapTest.invalidate("key1");
lruHashMapTest.getData("key1");
System.exit(0);
}
}
实现类:LruHashMap
package cache.lru;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.collections.CollectionUtils;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
/**
* LruHashMap
*
* @author chenx
*/
public class LruHashMap<K, V> {
private static ScheduledExecutorService expireExecutor = new ScheduledThreadPoolExecutor(1,
new ThreadFactoryBuilder().setNameFormat("lru-schedule-pool-%d")
.setDaemon(true)
.build()
);
private AtomicBoolean isCleanerRunning = new AtomicBoolean(false);
private long duration = -1;
private LruContainerMap<K, TimestampEntryValue<V>> container;
private Runnable expireRunnable = new Runnable() {
@Override
public void run() {
long nextInterval = 1000;
LruHashMap.this.container.getLock().lock();
try {
boolean shouldStopCleaner = true;
if (LruHashMap.this.container.size() > 0) {
long now = System.currentTimeMillis();
List<K> toBeRemoved = new ArrayList<>();
for (Map.Entry<K, TimestampEntryValue<V>> e : LruHashMap.this.container.entrySet()) {
K key = e.getKey();
TimestampEntryValue<V> tValue = e.getValue();
long timeLapsed = now - tValue.timestamp;
if (timeLapsed >= LruHashMap.this.duration) {
toBeRemoved.add(key);
} else {
long delta = LruHashMap.this.duration - timeLapsed;
if (delta > 1000L) {
nextInterval = delta;
}
break;
}
}
if (!CollectionUtils.isEmpty(toBeRemoved)) {
for (K key : toBeRemoved) {
LruHashMap.this.container.remove(key);
}
}
if (LruHashMap.this.container.size() > 0) {
shouldStopCleaner = false;
}
}
if (shouldStopCleaner) {
LruHashMap.this.isCleanerRunning.compareAndSet(true, false);
} else {
expireExecutor.schedule(this, nextInterval, TimeUnit.MILLISECONDS);
}
} finally {
LruHashMap.this.container.getLock().unlock();
}
}
};
public LruHashMap(int maxSize, Func.Action2<K, V> onEvict) {
this(maxSize, onEvict, -1L);
}
public LruHashMap(int maxSize, Func.Action2<K, V> onEvict, long duration) {
Func.Action2<K, TimestampEntryValue<V>> doOnEvict = null;
if (onEvict != null) {
doOnEvict = (key, value) -> {
if (value != null) {
onEvict.invoke(key, value.value);
}
};
}
this.duration = duration;
this.container = new LruContainerMap<>(maxSize, doOnEvict);
}
/**
* size
*
* @return
*/
public int size() {
return this.container.size();
}
/**
* getKeys
*
* @return
*/
public Set<K> getKeys() {
return this.container.keySet();
}
/**
* put
*
* @param key
* @param value
* @return
*/
public V put(K key, V value) {
TimestampEntryValue<V> v = new TimestampEntryValue<>();
v.timestamp = System.currentTimeMillis();
v.value = value;
TimestampEntryValue<V> old = this.container.put(key, v);
if (this.duration > 0 && this.isCleanerRunning.compareAndSet(false, true)) {
expireExecutor.schedule(this.expireRunnable, this.duration, TimeUnit.MILLISECONDS);
}
return old == null ? null : old.value;
}
/**
* putIfAbsent
*
* @param key
* @param value
* @return
*/
public V putIfAbsent(K key, V value) {
TimestampEntryValue<V> v = new TimestampEntryValue<>();
v.timestamp = System.currentTimeMillis();
v.value = value;
TimestampEntryValue<V> old = this.container.putIfAbsent(key, v);
boolean isSchedule = (old == null)
&& this.duration > 0
&& this.isCleanerRunning.compareAndSet(false, true);
if (isSchedule) {
expireExecutor.schedule(this.expireRunnable, this.duration, TimeUnit.MILLISECONDS);
}
return old == null ? null : old.value;
}
/**
* containsKey
*
* @param key
* @return
*/
public boolean containsKey(Object key) {
return this.container.containsKey(key);
}
/**
* get
*
* @param key
* @return
*/
public V get(Object key) {
TimestampEntryValue<V> got = this.container.get(key);
V ret = null;
if (got != null) {
got.timestamp = System.currentTimeMillis();
ret = got.value;
}
return ret;
}
/**
* remove
*
* @param key
* @param doEvict
* @return
*/
public V remove(Object key, boolean doEvict) {
TimestampEntryValue<V> removed;
if (doEvict) {
removed = this.container.remove(key);
} else {
removed = this.container.removeUnEvict(key);
}
V ret = null;
if (removed != null) {
ret = removed.value;
}
return ret;
}
/**
* remove
*
* @param key
* @return
*/
public V remove(Object key) {
return this.remove(key, true);
}
/**
* TimestampEntryValue
*
* @param <V>
*/
private static class TimestampEntryValue<V> {
@Getter
@Setter
private V value;
@Getter
@Setter
private long timestamp;
}
/**
* LruContainerMap
*
* @param <K>
* @param <V>
*/
private static class LruContainerMap<K, V extends TimestampEntryValue<?>> extends LinkedHashMap<K, V> {
private static ExecutorService pool = ThreadPoolUtil.getThreadPool("lru-action");
@Getter
private ReentrantLock lock = new ReentrantLock();
@Getter
@Setter
private int maxSize;
private transient Func.Action2<K, V> onEvict;
public LruContainerMap(int maxSize, Func.Action2<K, V> onEvict) {
super(16, 0.75f, true);
this.maxSize = maxSize;
this.onEvict = onEvict;
}
@Override
public V put(K key, V value) {
this.lock.lock();
try {
return super.put(key, value);
} finally {
this.lock.unlock();
}
}
@Override
public V putIfAbsent(K key, V value) {
this.lock.lock();
try {
V result = super.get(key);
if (result != null) {
return result;
} else {
super.put(key, value);
return null;
}
} finally {
this.lock.unlock();
}
}
@Override
public V get(Object key) {
this.lock.lock();
try {
return super.get(key);
} finally {
this.lock.unlock();
}
}
@Override
public V remove(Object key) {
this.lock.lock();
try {
V ret = super.remove(key);
if (this.onEvict != null) {
pool.execute(() -> this.onEvict.invoke((K) key, ret));
}
return ret;
} finally {
this.lock.unlock();
}
}
/**
* removeUnEvict
*
* @param key
* @return
*/
public V removeUnEvict(Object key) {
this.lock.lock();
try {
return super.remove(key);
} finally {
this.lock.unlock();
}
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
boolean ret = this.size() > this.maxSize;
if (this.onEvict != null && ret) {
pool.execute(() -> this.onEvict.invoke(eldest.getKey(), eldest.getValue()));
}
return ret;
}
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
@Override
public int hashCode() {
return super.hashCode();
}
}
}
事件方法抽象:Func
package cache.lru;
/**
* F
*
* @author chenx
*/
public class Func {
private Func() {
}
public static interface Action0 {
/**
* invoke
*/
public void invoke();
}
public static interface Action1<T> {
/**
* invoke
*
* @param arg
*/
public void invoke(T arg);
}
public static interface Action2<T1, T2> {
/**
* invoke
*
* @param arg1
* @param arg2
*/
public void invoke(T1 arg1, T2 arg2);
}
public static interface Action3<T1, T2, T3> {
/**
* invoke
*
* @param arg1
* @param arg2
* @param arg3
*/
public void invoke(T1 arg1, T2 arg2, T3 arg3);
}
public static interface Action4<T1, T2, T3, T4> {
/**
* invoke
*
* @param arg1
* @param arg2
* @param arg3
* @param arg4
*/
public void invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
}
public static interface Function0<R> {
/**
* invoke
*
* @return
*/
public R invoke();
}
public static interface Function1<T, R> {
/**
* invoke
*
* @param arg
* @return
*/
public R invoke(T arg);
}
public static interface Promise<V> extends Action1<V> {
/**
* invokeWithThrowable
*
* @param e
*/
public void invokeWithThrowable(Throwable e);
}
}
工具类:ThreadPoolUtil
package cache.lru;
import org.apache.commons.lang.StringUtils;
import java.util.concurrent.*;
/**
* ThreadPoolUtil
*
* @author chenx
*/
public class ThreadPoolUtil {
private static final ConcurrentHashMap<String, ExecutorService> THREAD_MAP = new ConcurrentHashMap<>();
public static final int AVAILABLE_PROCESSORS;
private static final String THREAD_COMMON = "common";
private static final String THREAD_NAME_PLACEHOLDER = "%d";
static {
AVAILABLE_PROCESSORS = Runtime.getRuntime().availableProcessors();
}
private ThreadPoolUtil() {
}
/**
* getCommonThreadPool
*/
public static ExecutorService getCommonThreadPool() {
return getThreadPool(THREAD_COMMON, Runtime.getRuntime().availableProcessors() * 2);
}
/**
* getSingleThreadExecutor
*
* @param name
* @return
*/
public static ExecutorService getSingleThreadExecutor(String name) {
return getThreadPool(name, name, 1, 1, 0, new ThreadPoolExecutor.AbortPolicy());
}
/**
* ExecutorService
*
* @param name
* @param workerQueueSize
* @return
*/
public static ExecutorService getSingleThreadExecutor(String name, int workerQueueSize) {
return getThreadPool(name, name, 1, 1, workerQueueSize, new ThreadPoolExecutor.AbortPolicy());
}
/**
* getThreadPool
*
* @param name
* @return
*/
public static ExecutorService getThreadPool(String name) {
return getThreadPool(name, ThreadPoolUtil.AVAILABLE_PROCESSORS);
}
/**
* getThreadPool
*
* @param name
* @param size
* @return
*/
public static ExecutorService getThreadPool(String name, int size) {
return getThreadPool(name, name, size);
}
/**
* getThreadPool
*
* @param name
* @param threadNamePrefix
* @param coreSize
* @return
*/
public static ExecutorService getThreadPool(String name,
String threadNamePrefix,
int coreSize) {
return getThreadPool(name, threadNamePrefix, coreSize, 0);
}
/**
* getThreadPool
*
* @param name
* @param threadNamePrefix
* @param coreSize
* @param workerQueueSize
* @return
*/
public static ExecutorService getThreadPool(String name,
String threadNamePrefix,
int coreSize,
int workerQueueSize) {
return getThreadPool(name, threadNamePrefix, coreSize, coreSize * 2, workerQueueSize, new ThreadPoolExecutor.AbortPolicy());
}
/**
* getThreadPool
*
* @param name 线程池名称,
* @param threadNamePrefix 线程名称前缀.
* @param coreSize 线程数量. 必须> 1
* @param maxThreadSize 最大数量. 必须> 1 ,并且大于 coreSize ,否则使用coreSize
* @param workerQueueSize 线程队列数量, 当 workerQueueSize <=0 workerQueueSize:使用默认值 Integer.MAX_VALUE
* @param rejectedHandler 拒绝策略, 如果为空 使用 ThreadPoolExecutor.AbortPolicy()
*/
public static ExecutorService getThreadPool(String name,
String threadNamePrefix,
int coreSize,
int maxThreadSize,
int workerQueueSize,
RejectedExecutionHandler rejectedHandler) {
if (!THREAD_MAP.containsKey(name)) {
ExecutorService pool = new ThreadPoolExecutor(
getCoreSize(name, coreSize),
maxThreadSize > coreSize ? maxThreadSize : coreSize, 0L,
TimeUnit.MILLISECONDS,
getWorkerBlockingQueue(workerQueueSize),
getThreadFactory(name, threadNamePrefix),
rejectedHandler == null ? new ThreadPoolExecutor.AbortPolicy() : rejectedHandler);
ExecutorService existedPool = THREAD_MAP.putIfAbsent(name, pool);
if (existedPool != null) {
pool.shutdown();
}
}
return THREAD_MAP.get(name);
}
/**
* getThreadFactory
*
* @param name
* @param threadNamePrefix
* @return
*/
public static ThreadFactory getThreadFactory(String name, String threadNamePrefix) {
if (StringUtils.isBlank(threadNamePrefix)) {
threadNamePrefix = name;
}
if (!threadNamePrefix.contains(THREAD_NAME_PLACEHOLDER)) {
threadNamePrefix += "_" + THREAD_NAME_PLACEHOLDER;
}
return new ThreadFactoryBuilder().setNameFormat(threadNamePrefix).build();
}
/**
* BlockingQueue
*
* @param workerQueueSize
* @return
*/
private static BlockingQueue getWorkerBlockingQueue(int workerQueueSize) {
int queueMaxSize = workerQueueSize > 0 ? workerQueueSize : Integer.MAX_VALUE;
return new LinkedBlockingQueue(queueMaxSize);
}
/**
* getCoreSize:未将来有配置优先走配置留统一处理口子
*
* @param name
* @param coreSize
* @return
*/
private static int getCoreSize(String name, int coreSize) {
// 有配置优先走配置
return coreSize;
}
}
工具类:ThreadFactoryBuilder
package cache.lru;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicLong;
/**
* ThreadFactoryBuilder
*
* @author chenx
*/
public final class ThreadFactoryBuilder {
private String nameFormat = null;
private Boolean daemon = null;
private Integer priority = null;
private UncaughtExceptionHandler uncaughtExceptionHandler = null;
private ThreadFactory backingThreadFactory = null;
public ThreadFactoryBuilder() {
// just an empty constructor
}
/**
* setNameFormat
*
* @param nameFormat
* @return
*/
public ThreadFactoryBuilder setNameFormat(String nameFormat) {
this.nameFormat = nameFormat;
return this;
}
/**
* setDaemon
*
* @param daemon
* @return
*/
public ThreadFactoryBuilder setDaemon(boolean daemon) {
this.daemon = daemon;
return this;
}
/**
* setPriority
*
* @param priority
* @return
*/
public ThreadFactoryBuilder setPriority(int priority) {
this.checkArgument(priority >= Thread.MIN_PRIORITY,
"Thread priority (%s) must be >= %s", priority, Thread.MIN_PRIORITY);
this.checkArgument(priority <= Thread.MAX_PRIORITY,
"Thread priority (%s) must be <= %s", priority, Thread.MAX_PRIORITY);
this.priority = priority;
return this;
}
/**
* checkNotNull
*
* @param reference
* @param <T>
* @return
*/
public static <T> T checkNotNull(T reference) {
if (reference == null) {
throw new NullPointerException();
}
return reference;
}
/**
* checkNotNull
*
* @param reference
* @param errorMessage
* @param <T>
* @return
*/
public static <T> T checkNotNull(T reference, String errorMessage) {
if (reference == null) {
throw new NullPointerException(String.valueOf(errorMessage));
}
return reference;
}
/**
* Sets the {@link UncaughtExceptionHandler} for new threads created with this ThreadFactory.
*
* @param uncaughtExceptionHandler the uncaught exception handler for new Threads created with this ThreadFactory
* @return this for the builder pattern
*/
public ThreadFactoryBuilder setUncaughtExceptionHandler(
UncaughtExceptionHandler uncaughtExceptionHandler) {
this.uncaughtExceptionHandler = checkNotNull(uncaughtExceptionHandler);
return this;
}
public ThreadFactoryBuilder setThreadFactory(
ThreadFactory backingThreadFactory) {
this.backingThreadFactory = checkNotNull(backingThreadFactory);
return this;
}
/**
* Returns a new thread factory using the options supplied during the building process. After building, it is still
* possible to change the options used to build the ThreadFactory and/or build again. State is not shared amongst
* built instances.
*
* @return the fully constructed {@link ThreadFactory}
*/
public ThreadFactory build() {
return build(this);
}
/**
* build
*
* @param builder
* @return
*/
private static ThreadFactory build(ThreadFactoryBuilder builder) {
final String nameFormat = builder.nameFormat;
final Boolean daemon = builder.daemon;
final Integer priority = builder.priority;
final UncaughtExceptionHandler uncaughtExceptionHandler =
builder.uncaughtExceptionHandler;
final ThreadFactory backingThreadFactory =
(builder.backingThreadFactory != null)
? builder.backingThreadFactory
: Executors.defaultThreadFactory();
final AtomicLong count = (nameFormat != null) ? new AtomicLong(0) : null;
return new ThreadFactory() {
@Override
public Thread newThread(Runnable runnable) {
Thread thread = backingThreadFactory.newThread(runnable);
if (nameFormat != null) {
thread.setName(String.format(nameFormat, count.getAndIncrement()));
}
if (daemon != null) {
thread.setDaemon(daemon);
}
if (priority != null) {
thread.setPriority(priority);
}
if (uncaughtExceptionHandler != null) {
thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
}
return thread;
}
};
}
/**
* checkArgument
*
* @param expression
* @param errMsgTemplate
* @param errorMessageArgs
*/
private void checkArgument(boolean expression, String errMsgTemplate, Object... errorMessageArgs) {
if (!expression) {
throw new IllegalArgumentException(String.format(errMsgTemplate, errorMessageArgs));
}
}
}
3.2 LFU(Caffeine)
对于LFU有现成的组件,直接使用Caffeine即可。Caffeine的作者是Ben manes,在设计上参考了google的Guava cach组件,可以通过他的github:github.com/ben-manes来查看组件的源码 ,并且他还编写了ConcurrentLinkedHashMap工具类,也被用于缓存的底层数据结构,比如:这个类就是Guava cache的基础。关于Caffeine的使用,一搜一大把,这里我仅给出基本的使用示例。
package cache.caffeine;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* CaffeineTest
*
* @author chenx
*/
public class CaffeineTest {
private Cache<String, String> cache = null;
public CaffeineTest() {
this.cache = Caffeine.newBuilder()
// 缓存初始容量:合适的初始容量可以免缓存不断地进行扩容以获得更好的性能
.initialCapacity(2)
// 缓存最大容量:如果缓存中的数据量超过这个数值,Caffeine 会有一个异步线程来专门负责清除缓存,按照指定的清除策略来清除掉多余的缓存。
.maximumSize(3)
// 缓存过期时间:指定的时间段内没有被读或写就会被回收
.expireAfterAccess(5L, TimeUnit.SECONDS)
// 缓存淘汰监听处理
.removalListener((String key, String value, RemovalCause cause) -> this.onDataEvicted(key, value, cause))
.build();
}
/**
* getSize
*
* @return
*/
public long getSize() {
long size = this.cache.estimatedSize();
System.out.println("the cache size is: " + size);
return size;
}
/**
* setCache
*
* @param key
* @param value
*/
public void setCache(String key, String value) {
this.cache.put(key, value);
}
/**
* getData
*
* @param key
* @return
*/
public String getData(String key) {
// 如果缓存不存在则返回空
String data = this.cache.getIfPresent(key);
if (Objects.nonNull(data)) {
System.out.println("hit the cache, key: " + key + ", value: " + data);
return data;
}
/**
* 1、get()方法线程安全(底层使用ConcurrentHashMap进行节点存储)
* 2、如果缓存不存在则执行后面而方法获取数据同时加入缓存
*/
return this.cache.get(key, this::mockGetDataFromDb);
}
/**
* invalidate(清除指定缓存)
*
* @param key
*/
public void invalidate(String key) {
this.cache.invalidate(key);
System.out.println("remove the cache done: " + key);
}
/**
* invalidateAll(清除所有缓存)
*/
public void invalidateAll() {
this.cache.invalidateAll();
System.out.println("clear all the cache done.");
}
/**
* onDataEvicted
*
* @param key
* @param value
* @param cause
*/
public void onDataEvicted(String key, String value, RemovalCause cause) {
System.out.println("evicted a cache, key: " + key + ", value: " + value + ", cause:" + cause);
}
/**
* mockedGetDataFromDb(模拟从数据库获取数据)
*
* @param key
* @return
*/
private String mockGetDataFromDb(String key) {
String value = key + "-value";
System.out.println("get the data from db and put into cache, key: " + key + ", value: " + value);
return value;
}
public static void main(String[] args) throws InterruptedException {
CaffeineTest caffeineTest = new CaffeineTest();
System.out.println("-------常规测试-------");
caffeineTest.setCache("key1", "key1-value");
caffeineTest.getData("key1");
caffeineTest.getData("key2");
caffeineTest.getData("key3");
caffeineTest.getData("key4");
caffeineTest.getData("key5");
caffeineTest.getData("key6");
System.out.println("------容量淘汰测试--------");
caffeineTest.getSize();
Thread.sleep(1000L);
caffeineTest.getSize();
System.out.println("------超时淘汰测试--------");
Thread.sleep(10000L);
caffeineTest.getData("key1");
caffeineTest.getData("key2");
caffeineTest.getData("key3");
caffeineTest.getData("key4");
caffeineTest.getData("key5");
caffeineTest.getData("key6");
System.out.println("--------清除指定缓存测试-------");
caffeineTest.getData("key1");
caffeineTest.invalidate("key1");
caffeineTest.getData("key1");
System.out.println("-------清除所有缓存测试--------");
caffeineTest.invalidateAll();
caffeineTest.getData("key1");
caffeineTest.getData("key2");
caffeineTest.getData("key3");
caffeineTest.getData("key4");
caffeineTest.getData("key5");
caffeineTest.getData("key6");
}
}
4. 总结
对于缓存淘汰策略,本文只是拿出几个典型的方法给予简介,一些其他的缓存淘汰算法也极为优秀且被广泛应用。例如,很多DB所标配的LIRS (Low Inter-reference Recency Set),LIRS是针对LRU做优化的算法,在很多文章中被给予很高的评价,并且已经被应用在mysql 5.1之后的版本中。LIRS算法会设置一个栈和一个FIFO队列,栈负责热数据(LIR块)淘汰,队列中负责冷数据(HIR块)淘汰。对于其java实现,我只是简单找一下,发现apached的Jackrabbit中有(Jackrabbit是Apache基金会的顶级项目之一,其致力于大规模高性能的内容仓库以便满足当代世界级站点需求。),不过并未实际使用和测试过,欢迎大家通过评论或者其他方式进行补充说明,有兴趣的可以参考:https://jackrabbit.apache.org/oak/docs/apidocs/org/apache/jackrabbit/oak/cache/CacheLIRS.html