如何建立一个高可用的缓存
最近在写一个项目,有一个列表用来显示信息,原先每页只显示5条10条,后来改了,需要显示50条,现在要显示100条,后面可能需要显示更多记录。
那么问题来了,众所周知,数据库查询5条,50条,100条所用的时间是不一样的,如何高效的返回一个动辄上百条的数据,这是一个问题。
我解决这个问题的大概思路是利用redis建立一个缓存,从缓存中取数据,这样可以解决上面出现的问题,后面又顺便把跟新缓存的操作也写了。
废话不多说,上代码:
DbManager.java
package com.iluwatar.caching;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;
import org.bson.Document;
import com.mongodb.MongoClient;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.UpdateOptions;
/**
* Auth Ilkka Seppälä
* Translate by 林文耀
* 此类模拟与数据库的通信,提供了查询、插入和更新数据的方法。
* 开发人员/测试人员可以选择应用程序是否应使用MongoDB作为
* 其底层数据存储或适应map来作为虚拟数据库
*/
public final class DbManager {
private static MongoClient mongoClient;
private static MongoDatabase db;
private static boolean useMongoDB;
private static Map<String, UserAccount> virtualDB;
private DbManager() {
}
/**
* 创建 DB
*/
public static void createVirtualDb() {
useMongoDB = false;
virtualDB = new HashMap<>();
}
/**
* 连接 DB
*/
public static void connect() throws ParseException {
useMongoDB = true;
mongoClient = new MongoClient();
db = mongoClient.getDatabase("test");
}
/**
* 从 DB中获取用户ID
*/
public static UserAccount readFromDb(String userId) {
if (!useMongoDB) {
if (virtualDB.containsKey(userId)) {
return virtualDB.get(userId);
}
return null;
}
if (db == null) {
try {
connect();
} catch (ParseException e) {
e.printStackTrace();
}
}
FindIterable<Document> iterable =
db.getCollection("user_accounts").find(new Document("userID", userId));
if (iterable == null) {
return null;
}
Document doc = iterable.first();
return new UserAccount(userId, doc.getString("userName"), doc.getString("additionalInfo"));
}
/**
* 将用户写入DB
*/
public static void writeToDb(UserAccount userAccount) {
if (!useMongoDB) {
virtualDB.put(userAccount.getUserId(), userAccount);
return;
}
if (db == null) {
try {
connect();
} catch (ParseException e) {
e.printStackTrace();
}
}
db.getCollection("user_accounts").insertOne(
new Document("userID", userAccount.getUserId()).append("userName",
userAccount.getUserName()).append("additionalInfo", userAccount.getAdditionalInfo()));
}
/**
* 跟新用户信息
*/
public static void updateDb(UserAccount userAccount) {
if (!useMongoDB) {
virtualDB.put(userAccount.getUserId(), userAccount);
return;
}
if (db == null) {
try {
connect();
} catch (ParseException e) {
e.printStackTrace();
}
}
db.getCollection("user_accounts").updateOne(
new Document("userID", userAccount.getUserId()),
new Document("$set", new Document("userName", userAccount.getUserName()).append(
"additionalInfo", userAccount.getAdditionalInfo())));
}
/**
* 新增或修改用户信息(存在则修改,不存在则跟新)
*/
public static void upsertDb(UserAccount userAccount) {
if (!useMongoDB) {
virtualDB.put(userAccount.getUserId(), userAccount);
return;
}
if (db == null) {
try {
connect();
} catch (ParseException e) {
e.printStackTrace();
}
}
db.getCollection("user_accounts").updateOne(
new Document("userID", userAccount.getUserId()),
new Document("$set", new Document("userID", userAccount.getUserId()).append("userName",
userAccount.getUserName()).append("additionalInfo", userAccount.getAdditionalInfo())),
new UpdateOptions().upsert(true));
}
}
UserAccount.java
package com.iluwatar.caching;
/**
* 用户实体类。
*/
public class UserAccount {
private String userId;
private String userName;
private String additionalInfo;
/**
* 构造器
*/
public UserAccount(String userId, String userName, String additionalInfo) {
this.userId = userId;
this.userName = userName;
this.additionalInfo = additionalInfo;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getAdditionalInfo() {
return additionalInfo;
}
public void setAdditionalInfo(String additionalInfo) {
this.additionalInfo = additionalInfo;
}
@Override
public String toString() {
return userId + ", " + userName + ", " + additionalInfo;
}
}
CachingPolicy.java
package com.iluwatar.caching;
/**
* 包含了4中缓存策略的枚举类。
*/
public enum CachingPolicy {
THROUGH("through"), AROUND("around"), BEHIND("behind"), ASIDE("aside");
private String policy;
private CachingPolicy(String policy) {
this.policy = policy;
}
public String getPolicy() {
return policy;
}
}
下面重头戏来了。
LruCache.java
package com.iluwatar.caching;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 数据缓存结构的实现。
*/
public class LruCache {
private static final Logger LOGGER = LoggerFactory.getLogger(LruCache.class);
class Node {
String userId;
UserAccount userAccount;
Node previous;
Node next;
public Node(String userId, UserAccount userAccount) {
this.userId = userId;
this.userAccount = userAccount;
}
}
int capacity;
Map<String, Node> cache = new HashMap<>();
Node head;
Node end;
public LruCache(int capacity) {
this.capacity = capacity;
}
public UserAccount get(String userId) {
if (cache.containsKey(userId)) {
Node node = cache.get(userId);
remove(node);
setHead(node);
return node.userAccount;
}
return null;
}
public void remove(Node node) {
if (node.previous != null) {
node.previous.next = node.next;
} else {
head = node.next;
}
if (node.next != null) {
node.next.previous = node.previous;
} else {
end = node.previous;
}
}
public void setHead(Node node) {
node.next = head;
node.previous = null;
if (head != null) {
head.previous = node;
}
head = node;
if (end == null) {
end = head;
}
}
public void set(String userId, UserAccount userAccount) {
if (cache.containsKey(userId)) {
Node old = cache.get(userId);
old.userAccount = userAccount;
remove(old);
setHead(old);
} else {
Node newNode = new Node(userId, userAccount);
if (cache.size() >= capacity) {
LOGGER.info("# Cache is FULL! Removing {} from cache...", end.userId);
cache.remove(end.userId); // remove LRU data from cache.
remove(end);
setHead(newNode);
} else {
setHead(newNode);
}
cache.put(userId, newNode);
}
}
public boolean contains(String userId) {
return cache.containsKey(userId);
}
public void invalidate(String userId) {
Node toBeRemoved = cache.remove(userId);
if (toBeRemoved != null) {
LOGGER.info("# {} has been updated! Removing older version from cache...", userId);
remove(toBeRemoved);
}
}
public boolean isFull() {
return cache.size() >= capacity;
}
public UserAccount getLruData() {
return end.userAccount;
}
public void clear() {
head = null;
end = null;
cache.clear();
}
public List<UserAccount> getCacheDataInListForm() {
List<UserAccount> listOfCacheData = new ArrayList<>();
Node temp = head;
while (temp != null) {
listOfCacheData.add(temp.userAccount);
temp = temp.next;
}
return listOfCacheData;
}
public void setCapacity(int newCapacity) {
if (capacity > newCapacity) {
clear(); // Behavior can be modified to accommodate for decrease in cache size. For now, we'll
// just clear the cache.
} else {
this.capacity = newCapacity;
}
}
}
LruCache类是用来存储缓存数据的类,有趣的是类似于HashMap的TreeNode一样,他不但维护了一个table表(Node数组)。并且table的所有node自身又组成了一个双端队列,这是怎么做的的呢?我们先来看一下它的内部类Node。
class Node {
String userId;
UserAccount userAccount;
Node previous;
Node next;
public Node(String userId, UserAccount userAccount) {
this.userId = userId;
this.userAccount = userAccount;
}
}
很显然,这个Node包含了上一个Node的引用和下一个Node的引用,这样可以根据第一个Node顺序找到余下的所有Node。
接下来看LruCache类的属性:
int capacity; //缓存的最大值
Map<String, Node> cache = new HashMap<>(); //缓存容器
Node head; //第一个节点(头节点)的引用
Node end; //最后一个节点的引用
remove方法:
- 如果此节点不是头节点,将此节点的上个节点的next引用指向此节点的next
- 如果是头节点,将此节点的下个节点设为头节点
- 如果不是尾节点,将此节点的下个节点的previous引用指向此节点的previous
- 如果是尾节点,将尾节点指向此节点的下一个节点
public void remove(Node node) {
if (node.previous != null) {
node.previous.next = node.next;
} else {
head = node.next;
}
if (node.next != null) {
node.next.previous = node.previous;
} else {
end = node.previous;
}
}
setHead方法:
- 将此节点的下个节点设为头节点
- 将此节点的上一个节点设为null
- 如果头节点不是空的,将头节点的上个节点设为此节点
- 将头节点的指向此节点(注:head只是一个头节点的引用,如果一个节点被设置为头节点,则head需要指向此节点)
- 如果尾节点是空,将尾节点设置为头节点
public void setHead(Node node) {
node.next = head;
node.previous = null;
if (head != null) {
head.previous = node;
}
head = node;
if (end == null) {
end = head;
}
}
get方法:
- 如果缓存中命中了key(userId)则取出这个node
- 移除node节点的前后引用
- 设置此节点为头节点(这样下次取数据时此节点会优先匹配,命中率越多的节点,优先级更高)
public UserAccount get(String userId) {
if (cache.containsKey(userId)) {
Node node = cache.get(userId);
remove(node);
setHead(node);
return node.userAccount;
}
return null;
}
set方法:
- 判断缓存中是否包含了key(userId),如果有,则直接跟新并且位置重置为head
- 如果没有并且队列满了,移除尾部的队列并且新增一条缓存记录,重置为head,并且往容器中加入此节点
public void set(String userId, UserAccount userAccount) {
if (cache.containsKey(userId)) {
Node old = cache.get(userId);
old.userAccount = userAccount;
remove(old);
setHead(old);
} else {
Node newNode = new Node(userId, userAccount);
if (cache.size() >= capacity) {
LOGGER.info("# Cache is FULL! Removing {} from cache...", end.userId);
cache.remove(end.userId); // remove LRU data from cache.
remove(end);
setHead(newNode);
} else {
setHead(newNode);
}
cache.put(userId, newNode);
}
}
返回尾节点的用户信息
public UserAccount getLruData() {
return end.userAccount;
}
CacheStore.java
/**
* The MIT License
* Copyright (c) 2014-2016 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.caching;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 缓存策略类
*/
public class CacheStore {
private static final Logger LOGGER = LoggerFactory.getLogger(CacheStore.class);
static LruCache cache;
private CacheStore() {
}
public static void initCapacity(int capacity) {
if (cache == null) {
cache = new LruCache(capacity);
} else {
cache.setCapacity(capacity);
}
}
public static UserAccount readThrough(String userId) {
if (cache.contains(userId)) {
LOGGER.info("# Cache Hit!");
return cache.get(userId);
}
LOGGER.info("# Cache Miss!");
UserAccount userAccount = DbManager.readFromDb(userId);
cache.set(userId, userAccount);
return userAccount;
}
public static void writeThrough(UserAccount userAccount) {
if (cache.contains(userAccount.getUserId())) {
DbManager.updateDb(userAccount);
} else {
DbManager.writeToDb(userAccount);
}
cache.set(userAccount.getUserId(), userAccount);
}
public static void writeAround(UserAccount userAccount) {
if (cache.contains(userAccount.getUserId())) {
DbManager.updateDb(userAccount);
cache.invalidate(userAccount.getUserId()); // Cache data has been updated -- remove older
// version from cache.
} else {
DbManager.writeToDb(userAccount);
}
}
public static UserAccount readThroughWithWriteBackPolicy(String userId) {
if (cache.contains(userId)) {
LOGGER.info("# Cache Hit!");
return cache.get(userId);
}
LOGGER.info("# Cache Miss!");
UserAccount userAccount = DbManager.readFromDb(userId);
if (cache.isFull()) {
LOGGER.info("# Cache is FULL! Writing LRU data to DB...");
UserAccount toBeWrittenToDb = cache.getLruData();
DbManager.upsertDb(toBeWrittenToDb);
}
cache.set(userId, userAccount);
return userAccount;
}
public static void writeBehind(UserAccount userAccount) {
if (cache.isFull() && !cache.contains(userAccount.getUserId())) {
LOGGER.info("# Cache is FULL! Writing LRU data to DB...");
UserAccount toBeWrittenToDb = cache.getLruData();
DbManager.upsertDb(toBeWrittenToDb);
}
cache.set(userAccount.getUserId(), userAccount);
}
public static void clearCache() {
if (cache != null) {
cache.clear();
}
}
public static void flushCache() {
LOGGER.info("# flushCache...");
if (null == cache) {
return;
}
List<UserAccount> listOfUserAccounts = cache.getCacheDataInListForm();
for (UserAccount userAccount : listOfUserAccounts) {
DbManager.upsertDb(userAccount);
}
}
public static String print() {
List<UserAccount> listOfUserAccounts = cache.getCacheDataInListForm();
StringBuilder sb = new StringBuilder();
sb.append("\n--CACHE CONTENT--\n");
for (UserAccount userAccount : listOfUserAccounts) {
sb.append(userAccount.toString() + "\n");
}
sb.append("----\n");
return sb.toString();
}
public static UserAccount get(String userId) {
return cache.get(userId);
}
public static void set(String userId, UserAccount userAccount) {
cache.set(userId, userAccount);
}
/**
* Delegate to backing cache store
*/
public static void invalidate(String userId) {
cache.invalidate(userId);
}
static class User{
public User(Integer a){
this.a = a;
}
public Integer a ;
}
}
这里需要先了解缓存的4种策略,因为Read-Through和Read-Around代码上没区别,所以放一起讲了。
Read-Through与Read-Around:效率高,但是缓存区满,有可能会造成数据丢失(取决于缓存的写入策略)
- 如果缓存中命中了,直接返回
- 否则从数据库中查询,存入缓存后返回
public static UserAccount readThrough(String userId) {
if (cache.contains(userId)) {
LOGGER.info("# Cache Hit!");
return cache.get(userId);
}
LOGGER.info("# Cache Miss!");
UserAccount userAccount = DbManager.readFromDb(userId);
cache.set(userId, userAccount);
return userAccount;
}
Read-Behind:带有更新数据库的缓存使用
- 如果缓存中命中了,直接返回
- 否则先从数据库中获取数据,如果缓存满了,往数据库中更新一条记录,再返回,如果没满,则设置缓存后返回
public static UserAccount readThroughWithWriteBackPolicy(String userId) {
if (cache.contains(userId)) {
LOGGER.info("# Cache Hit!");
return cache.get(userId);
}
LOGGER.info("# Cache Miss!");
UserAccount userAccount = DbManager.readFromDb(userId);
if (cache.isFull()) {
LOGGER.info("# Cache is FULL! Writing LRU data to DB...");
UserAccount toBeWrittenToDb = cache.getLruData();
DbManager.upsertDb(toBeWrittenToDb);
}
cache.set(userId, userAccount);
return userAccount;
}
Read-Aside:代码似乎跟Read-Thound差不多
private static UserAccount findAside(String userId) {
UserAccount userAccount = CacheStore.get(userId);
if (userAccount != null) {
return userAccount;
}
userAccount = DbManager.readFromDb(userId);
if (userAccount != null) {
CacheStore.set(userId, userAccount);
}
return userAccount;
}
接下来说写缓存的策略,也是从4个方面入手
Write-Through:尽可能确保数据是最新的
- 不管缓存中是否有数据,直接写入数据库,然后再跟新缓存
public static void writeThrough(UserAccount userAccount) {
if (cache.contains(userAccount.getUserId())) {
DbManager.updateDb(userAccount);
} else {
DbManager.writeToDb(userAccount);
}
cache.set(userAccount.getUserId(), userAccount);
}
Write-Around:直接写入数据到数据库,不必写到缓存,缓存的数据应该被立即过期,认为缓存一旦过期就不能使用了
- 如果缓存中命中,更新,并且删除缓存
- 否则直接写入数据库
public static void writeAround(UserAccount userAccount) {
if (cache.contains(userAccount.getUserId())) {
DbManager.updateDb(userAccount);
cache.invalidate(userAccount.getUserId()); // Cache data has been updated -- remove older
// version from cache.
} else {
DbManager.writeToDb(userAccount);
}
}
Write-Behind:类似于牺牲数据时效性,为了改善读写性能,读写操作直接在缓存中进行,统一处理脏数据,这个策略极大的利用到了缓存,适合缓存读写频率高的业务,建议配合Read-Behind使用
- 如果缓存已满并且缓存中不包含此用户信息,更新缓存链表中的尾节点,并移除
- 如果缓存未满或者缓存中包含了此用户信息,直接设置或跟新缓存
public static void writeBehind(UserAccount userAccount) {
if (cache.isFull() && !cache.contains(userAccount.getUserId())) {
LOGGER.info("# Cache is FULL! Writing LRU data to DB...");
UserAccount toBeWrittenToDb = cache.getLruData();
DbManager.upsertDb(toBeWrittenToDb);
}
cache.set(userAccount.getUserId(), userAccount);
}
Write-Around:更新完使缓存过期
private static void saveAside(UserAccount userAccount) {
DbManager.updateDb(userAccount);
CacheStore.invalidate(userAccount.getUserId());
}
AppManager.java
package com.iluwatar.caching;
import java.text.ParseException;
/**
* 对缓存策略的进一步封装,可以理解为调用方的管理类
*/
public final class AppManager {
private static CachingPolicy cachingPolicy;
private AppManager() {
}
public static void initDb(boolean useMongoDb) {
if (useMongoDb) {
try {
DbManager.connect();
} catch (ParseException e) {
e.printStackTrace();
}
} else {
DbManager.createVirtualDb();
}
}
public static void initCachingPolicy(CachingPolicy policy) {
cachingPolicy = policy;
if (cachingPolicy == CachingPolicy.BEHIND) {
Runtime.getRuntime().addShutdownHook(new Thread(CacheStore::flushCache));//lambda 类似() -> CacheStore.flushCache()
}
CacheStore.clearCache();
}
public static void initCacheCapacity(int capacity) {
CacheStore.initCapacity(capacity);
}
public static UserAccount find(String userId) {
if (cachingPolicy == CachingPolicy.THROUGH || cachingPolicy == CachingPolicy.AROUND) {
return CacheStore.readThrough(userId);
} else if (cachingPolicy == CachingPolicy.BEHIND) {
return CacheStore.readThroughWithWriteBackPolicy(userId);
} else if (cachingPolicy == CachingPolicy.ASIDE) {
return findAside(userId);
}
return null;
}
public static void save(UserAccount userAccount) {
if (cachingPolicy == CachingPolicy.THROUGH) {
CacheStore.writeThrough(userAccount);
} else if (cachingPolicy == CachingPolicy.AROUND) {
CacheStore.writeAround(userAccount);
} else if (cachingPolicy == CachingPolicy.BEHIND) {
CacheStore.writeBehind(userAccount);
} else if (cachingPolicy == CachingPolicy.ASIDE) {
saveAside(userAccount);
}
}
public static String printCacheContent() {
return CacheStore.print();
}
private static void saveAside(UserAccount userAccount) {
DbManager.updateDb(userAccount);
CacheStore.invalidate(userAccount.getUserId());
}
private static UserAccount findAside(String userId) {
UserAccount userAccount = CacheStore.get(userId);
if (userAccount != null) {
return userAccount;
}
userAccount = DbManager.readFromDb(userId);
if (userAccount != null) {
CacheStore.set(userId, userAccount);
}
return userAccount;
}
}
App.java
package com.iluwatar.caching;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class App {
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
public static void main(String[] args) {
AppManager.initDb(false); // VirtualDB (instead of MongoDB) was used in running the JUnit tests
// and the App class to avoid Maven compilation errors. Set flag to
// true to run the tests with MongoDB (provided that MongoDB is
// installed and socket connection is open).
AppManager.initCacheCapacity(3);
App app = new App();
app.useReadAndWriteThroughStrategy();
app.useReadThroughAndWriteAroundStrategy();
app.useReadThroughAndWriteBehindStrategy();
app.useCacheAsideStategy();
}
public void useReadAndWriteThroughStrategy() {
LOGGER.info("# CachingPolicy.THROUGH");
AppManager.initCachingPolicy(CachingPolicy.THROUGH);
UserAccount userAccount1 = new UserAccount("001", "John", "He is a boy.");
AppManager.save(userAccount1);
LOGGER.info(AppManager.printCacheContent());
AppManager.find("001");
AppManager.find("001");
}
public void useReadThroughAndWriteAroundStrategy() {
LOGGER.info("# CachingPolicy.AROUND");
AppManager.initCachingPolicy(CachingPolicy.AROUND);
UserAccount userAccount2 = new UserAccount("002", "Jane", "She is a girl.");
AppManager.save(userAccount2);
LOGGER.info(AppManager.printCacheContent());
AppManager.find("002");
LOGGER.info(AppManager.printCacheContent());
userAccount2 = AppManager.find("002");
userAccount2.setUserName("Jane G.");
AppManager.save(userAccount2);
LOGGER.info(AppManager.printCacheContent());
AppManager.find("002");
LOGGER.info(AppManager.printCacheContent());
AppManager.find("002");
}
public void useReadThroughAndWriteBehindStrategy() {
LOGGER.info("# CachingPolicy.BEHIND");
AppManager.initCachingPolicy(CachingPolicy.BEHIND);
UserAccount userAccount3 = new UserAccount("003", "Adam", "He likes food.");
UserAccount userAccount4 = new UserAccount("004", "Rita", "She hates cats.");
UserAccount userAccount5 = new UserAccount("005", "Isaac", "He is allergic to mustard.");
AppManager.save(userAccount3);
AppManager.save(userAccount4);
AppManager.save(userAccount5);
LOGGER.info(AppManager.printCacheContent());
AppManager.find("003");
LOGGER.info(AppManager.printCacheContent());
UserAccount userAccount6 = new UserAccount("006", "Yasha", "She is an only child.");
AppManager.save(userAccount6);
LOGGER.info(AppManager.printCacheContent());
AppManager.find("004");
LOGGER.info(AppManager.printCacheContent());
}
public void useCacheAsideStategy() {
LOGGER.info("# CachingPolicy.ASIDE");
AppManager.initCachingPolicy(CachingPolicy.ASIDE);
LOGGER.info(AppManager.printCacheContent());
UserAccount userAccount3 = new UserAccount("003", "Adam", "He likes food.");
UserAccount userAccount4 = new UserAccount("004", "Rita", "She hates cats.");
UserAccount userAccount5 = new UserAccount("005", "Isaac", "He is allergic to mustard.");
AppManager.save(userAccount3);
AppManager.save(userAccount4);
AppManager.save(userAccount5);
LOGGER.info(AppManager.printCacheContent());
AppManager.find("003");
LOGGER.info(AppManager.printCacheContent());
AppManager.find("004");
LOGGER.info(AppManager.printCacheContent());
}
}
其实缓存策略的定义不止这些,以上只是粗略的使用了一下,如果你对缓存的使用有什么见解,或者对文章有什么意见的话,都欢迎在评论区留言。