Java缓存
1 概念
1.1 什么是缓存
在计算中,缓存是一个高速数据存储层,其中存储了数据子集,且通常是短暂性存储。缓存的访问速度比主存的访问速度快,可以高效地重用之前检索或者计算的数据。
也就是把数据从访问慢的介质挪到访问快的介质。
1.2 为什么要用缓存
- 提升应用程序性能
- 降低数据库成本
- 减少后端负载
- 可预测性能
- 消除数据库热点
- 提高读取吞吐量(IOPS)
2 Java内存缓存
2.1 场景
在Java应用中,对于访问频率高,更新少的数据,通常的方案是将这些数据加入到缓存中。相对于从数据库中读取,使用缓存在效率上有很大的提升。
在集群环境下,通常的分布式缓存有Redis、Memcached等。但在某些业务场景中,可能不需要搭建一套复杂的分布式缓存系统,在单机环境下,通常是会希望使用内部缓存(LocalCache)。
2.2 ConcurrentHashMap实现缓存
MapCache
public class MapCache {
/**
* 定时5秒一次清理过期缓存
*/
private static final int CLEAN_UP_PERIOD_IN_SEC = 5;
/**
* 软引用保证在跑出OutOfMemory之前,如果缺少内存,gc会清理引用的对象
*/
private final ConcurrentHashMap<String, SoftReference<CacheObject>> cache = new ConcurrentHashMap<>();
public MapCache() {
/**
* 守护线程,定期去除过期数据
*/
Thread cleanerThread = new Thread(() -> {
String key;
SoftReference<CacheObject> softRef;
while (!Thread.currentThread().isInterrupted()){
try {
Thread.sleep(CLEAN_UP_PERIOD_IN_SEC * 1000);
for (Map.Entry<String, SoftReference<CacheObject>> entry : cache.entrySet()){
key = entry.getKey();
softRef = entry.getValue();
if (softRef.get() == null || softRef.get().isExpired()){
cache.remove(key);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
cleanerThread.setDaemon(true);
cleanerThread.start();
}
/**
* 添加键值对
*
* @param key
* @param value
* @param periodInMillis
*/
public void add(String key, Object value, long periodInMillis) {
if (key == null) {
return;
}
if (value == null) {
cache.remove(key);
} else {
long expiryTime = System.currentTimeMillis() + periodInMillis;
cache.put(key, new SoftReference<>(new CacheObject(value, expiryTime)));
}
}
/**
* 删除键值对
*
* @param key
*/
public void remove(String key) {
cache.remove(key);
}
/**
* 获取值
*
* @param key
* @return
*/
public Object get(String key) {
if (cache.containsKey(key)) {
return cache.get(key).get().getValue();
}
return null;
}
/**
* 清空缓存
*/
public void clear() {
cache.clear();
}
/**
* 获取大小
*
* @return
*/
public long size() {
return cache.size();
}
/**
* 缓存对象value
*/
private static class CacheObject {
private Object value;
private long expiryTime;
private CacheObject(Object value, long expiryTime) {
this.value = value;
this.expiryTime = expiryTime;
}
/**
* 是否过期
* @return
*/
boolean isExpired() {
return System.currentTimeMillis() > expiryTime;
}
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
}
pojo
public class User implements Serializable {
private String userName;
private String userId;
public User(String userName, String userId) {
this.userName = userName;
this.userId = userId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
@Override
public String toString() {
return userId + " --- " + userName;
}
}
test
@Test
public void test() throws InterruptedException {
MapCache cache = new MapCache();
cache.add("uid_10001", new User("张三","uid_10001"), 5 * 1000);
cache.add("uid_10002", new User("李四","uid_10002"), 5 * 1000);
cache.add("uid_10003", new User("王五","uid_10003"), 5 * 1000);
System.out.println("从缓存中取出值:" + cache.get("uid_10001").toString());
System.out.println("从缓存中取出值:" + cache.get("uid_10002").toString());
System.out.println("从缓存中取出值:" + cache.get("uid_10003").toString());
Thread.sleep(6000L);
System.out.println("6秒钟过后");
System.out.println("从缓存中取出值:" + cache.get("uid_10001").toString());
System.out.println("从缓存中取出值:" + cache.get("uid_10002").toString());
System.out.println("从缓存中取出值:" + cache.get("uid_10003").toString());
}
3 Guava
3.1 介绍
Guava Cache是google guava中的一个内存缓存模块,用于讲数据缓存到JVM内存中。实际项目开发中经常将一些公共后者常用的数据缓存起来方便快速访问。
3.2 适用场景
- 愿意消耗一些内存空间来提升速度
- 预料到某些建会被查询一次以上
- 缓存中存放的数据总量不会超出内存容量
Guava Cache是单个应用运行时的本地缓存,它不会把数据存放到文件或者外部服务器。其他场景可使用Redis、Memcached这类工具。
3.3 简单使用
public class GuavaCacheDemo {
public static void main(String[] args) throws ExecutionException {
LoadingCache<String, User> cache = CacheBuilder.newBuilder()
// 设置并发级别为8,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(8)
// 设置缓存容器的初始容量为10
.initialCapacity(10)
// 设置缓存的最大容量为100,超过100之后按照LRU最近最少使用算法来移除缓存
.maximumSize(100)
// 是否需要统计缓存情况,该操作消耗一定的性能,生产环境应该去除
.recordStats()
// 设置写缓存后n秒钟过期
.expireAfterWrite(17, TimeUnit.SECONDS)
// 设置读写缓存后n秒钟过期,实际很少用到,类似于expireAfterWrite
//.expireAfterAccess(17, TimeUnit.SECONDS)
//只阻塞当前数据加载线程,其他线程返回旧值
//.refreshAfterWrite(13, TimeUnit.SECONDS)
//设置缓存的移除通知
.removalListener(removalNotification -> {
System.out.println(removalNotification.getKey() + " " + removalNotification.getValue() + " 被移除,原因:" + removalNotification.getCause());
})
//build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存
.build(new MyCacheLoader());
// 第一次查询
for (int i = 0; i < 10; i++) {
System.out.println(cache.get("uid_1000" + i).toString());
}
// 第二次查询
for (int i = 0; i < 10; i++) {
System.out.println(cache.get("uid_1000" + i).toString());
}
// 打印统计信息
System.out.println(cache.stats().toString());
}
/**
* 实际使用时应实现业务的缓存加载逻辑,例如从数据库中获取数据
*/
public static class MyCacheLoader extends CacheLoader<String, User> {
@Override
public User load(String s) throws Exception {
System.out.println(Thread.currentThread().getName() + " 加载数据开始");
// 假设从数据库加载数据
TimeUnit.SECONDS.sleep(1);
Random random = new Random();
User user = new User(s, "zzy" + random.nextInt(1000));
System.out.println(Thread.currentThread().getName() + " 加载数据结束");
return user;
}
}
}
输出
main 加载数据开始
main 加载数据结束
uid_10000 --- zzy841
main 加载数据开始
main 加载数据结束
uid_10001 --- zzy211
main 加载数据开始
main 加载数据结束
uid_10002 --- zzy346
main 加载数据开始
main 加载数据结束
uid_10003 --- zzy768
main 加载数据开始
main 加载数据结束
uid_10004 --- zzy955
main 加载数据开始
main 加载数据结束
uid_10005 --- zzy585
main 加载数据开始
main 加载数据结束
uid_10006 --- zzy132
main 加载数据开始
main 加载数据结束
uid_10007 --- zzy664
main 加载数据开始
main 加载数据结束
uid_10008 --- zzy212
main 加载数据开始
main 加载数据结束
uid_10009 --- zzy234
uid_10000 --- zzy841
uid_10001 --- zzy211
uid_10002 --- zzy346
uid_10003 --- zzy768
uid_10004 --- zzy955
uid_10005 --- zzy585
uid_10006 --- zzy132
uid_10007 --- zzy664
uid_10008 --- zzy212
uid_10009 --- zzy234
CacheStats{hitCount=10, missCount=10, loadSuccessCount=10, loadExceptionCount=0, totalLoadTime=10005123400, evictionCount=0}