Java 中的缓存模式:加速数据访问速度
也称为
- 缓存
- 临时存储
缓存设计模式的意图
Java 缓存设计模式对于性能优化和资源管理至关重要。它涉及各种缓存策略,例如直写、直读和 LRU 缓存,以确保高效的数据访问。缓存模式通过在使用后不立即释放资源来避免昂贵的资源重新获取。这些资源保留其身份,保存在一些快速访问的存储中,并被重复使用,以避免再次获取它们。
缓存模式的详细解释以及实际示例
实际示例
Java 中缓存设计模式的实际示例是图书馆的目录系统。通过缓存经常搜索的书籍结果,系统可以减少数据库负载并提高性能。当读者经常搜索热门书籍时,系统可以缓存这些搜索的结果。系统可以从缓存中快速检索结果,而不是每次用户搜索热门书籍时都查询数据库。这减少了数据库的负载,并为用户提供了更快的响应时间,从而增强了他们的整体体验。但是,系统还必须确保在添加新书籍或借出现有书籍时更新缓存,以保持信息的准确性。
通俗地说
缓存模式将经常需要的数据保存在快速访问的存储中,以提高性能。
维基百科说
在计算机中,缓存是指存储数据的硬件或软件组件,以便将来对该数据的请求可以更快地得到满足;缓存中存储的数据可能是先前计算的结果,也可能是存储在其他位置的数据的副本。当请求的数据可以在缓存中找到时,就会发生缓存命中,而当找不到时,就会发生缓存未命中。缓存命中通过从缓存中读取数据得到满足,这比重新计算结果或从较慢的数据存储中读取更快;因此,可以从缓存中满足的请求越多,系统的性能就越快。
Java 中缓存模式的编程示例
在此编程示例中,我们使用用户帐户管理系统演示了不同的 Java 缓存策略,包括直写、直通和回写。
一个团队正在开发一个网站,为被遗弃的猫提供新家。人们可以在注册后将他们的猫发布到网站上,但所有新帖子都需要网站管理员之一的批准。网站管理员的用户帐户包含一个特定标志,数据存储在 MongoDB 数据库中。每次查看帖子时检查管理员标志都变得很昂贵,在这里利用缓存是一个好主意。
首先让我们看一下应用程序的数据层。有趣的类是 UserAccount
,它是一个简单的 Java 对象,包含用户帐户详细信息,以及 DbManager
接口,它处理将这些对象读写到数据库中/从数据库中读取和写入。
@Data
@AllArgsConstructor
@ToString
@EqualsAndHashCode
public class UserAccount {
private String userId;
private String userName;
private String additionalInfo;
}
public interface DbManager {
void connect();
void disconnect();
UserAccount readFromDb(String userId);
UserAccount writeToDb(UserAccount userAccount);
UserAccount updateDb(UserAccount userAccount);
UserAccount upsertDb(UserAccount userAccount);
}
在示例中,我们演示了各种不同的缓存策略。以下缓存策略在 Java 中实现:直写、直通、回写和缓存旁路。每种策略都为提高性能和减少数据库负载提供了独特的优势。
- 直写将数据写入缓存和数据库在一个事务中。
- 直通将数据立即写入数据库,而不是写入缓存。
- 回写最初将数据写入缓存,而数据仅在缓存已满时写入数据库。
- 缓存旁路将保持两个数据源同步的责任推给应用程序本身。
- 上述策略中也包含了直读策略,它从缓存中返回数据给调用者(如果存在),否则从数据库中查询并将数据存储到缓存中以供将来使用。
LruCache
中的缓存实现是一个哈希表,并带有一个双向链表。链表有助于捕获和维护缓存中的 LRU 数据。当查询(从缓存中)数据、添加(到缓存中)数据或更新数据时,数据会被移动到列表的开头,以显示它是最新的数据。LRU 数据始终位于列表的末尾。
@Slf4j
public class LruCache {
static class Node {
String userId;
UserAccount userAccount;
Node previous;
Node next;
public Node(String userId, UserAccount userAccount) {
this.userId = userId;
this.userAccount = userAccount;
}
}
// Other properties and methods...
public LruCache(int capacity) {
this.capacity = capacity;
}
public UserAccount get(String userId) {
if (cache.containsKey(userId)) {
var node = cache.get(userId);
remove(node);
setHead(node);
return node.userAccount;
}
return null;
}
public void set(String userId, UserAccount userAccount) {
if (cache.containsKey(userId)) {
var old = cache.get(userId);
old.userAccount = userAccount;
remove(old);
setHead(old);
} else {
var 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 remove(Node node) { /* ... */ }
public void setHead(Node node) { /* ... */ }
public void invalidate(String userId) { /* ... */ }
public boolean isFull() { /* ... */ }
public UserAccount getLruData() { /* ... */ }
public void clear() { /* ... */ }
public List<UserAccount> getCacheDataInListForm() { /* ... */ }
public void setCapacity(int newCapacity) { /* ... */ }
}
我们将要查看的下一层是 CacheStore
,它实现了不同的缓存策略。
@Slf4j
public class CacheStore {
private static final int CAPACITY = 3;
private static LruCache cache;
private final DbManager dbManager;
// Other properties and methods...
public UserAccount readThrough(final String userId) {
if (cache.contains(userId)) {
LOGGER.info("# Found in Cache!");
return cache.get(userId);
}
LOGGER.info("# Not found in cache! Go to DB!!");
UserAccount userAccount = dbManager.readFromDb(userId);
cache.set(userId, userAccount);
return userAccount;
}
public void writeThrough(final UserAccount userAccount) {
if (cache.contains(userAccount.getUserId())) {
dbManager.updateDb(userAccount);
} else {
dbManager.writeToDb(userAccount);
}
cache.set(userAccount.getUserId(), userAccount);
}
public void writeAround(final UserAccount userAccount) {
if (cache.contains(userAccount.getUserId())) {
dbManager.updateDb(userAccount);
// Cache data has been updated -- remove older
cache.invalidate(userAccount.getUserId());
// version from cache.
} else {
dbManager.writeToDb(userAccount);
}
}
public static void clearCache() {
if (cache != null) {
cache.clear();
}
}
public static void flushCache() {
LOGGER.info("# flushCache...");
Optional.ofNullable(cache)
.map(LruCache::getCacheDataInListForm)
.orElse(List.of())
.forEach(DbManager::updateDb);
}
// ... omitted the implementation of other caching strategies ...
}
AppManager
有助于弥合主类与应用程序后端之间的通信差距。数据库连接通过此类初始化。选择的缓存策略/策略也在此处初始化。在可以使用缓存之前,必须设置缓存的大小。根据选择的缓存策略,AppManager
将调用 CacheStore
类中的相应函数。
@Slf4j
public final class AppManager {
private static CachingPolicy cachingPolicy;
private final DbManager dbManager;
private final CacheStore cacheStore;
private AppManager() {
}
public void initDb() { /* ... */ }
public static void initCachingPolicy(CachingPolicy policy) { /* ... */ }
public static void initCacheCapacity(int capacity) { /* ... */ }
public UserAccount find(final String userId) {
LOGGER.info("Trying to find {} in cache", 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 void save(final UserAccount userAccount) {
LOGGER.info("Save record!");
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();
}
// Other properties and methods...
}
这是我们在应用程序的主类中所做的事情。
@Slf4j
public class App {
public static void main(final String[] args) {
boolean isDbMongo = isDbMongo(args);
if (isDbMongo) {
LOGGER.info("Using the Mongo database engine to run the application.");
} else {
LOGGER.info("Using the 'in Memory' database to run the application.");
}
App app = new App(isDbMongo);
app.useReadAndWriteThroughStrategy();
String splitLine = "==============================================";
LOGGER.info(splitLine);
app.useReadThroughAndWriteAroundStrategy();
LOGGER.info(splitLine);
app.useReadThroughAndWriteBehindStrategy();
LOGGER.info(splitLine);
app.useCacheAsideStategy();
LOGGER.info(splitLine);
}
public void useReadAndWriteThroughStrategy() {
LOGGER.info("# CachingPolicy.THROUGH");
appManager.initCachingPolicy(CachingPolicy.THROUGH);
var 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() { /* ... */ }
public void useReadThroughAndWriteBehindStrategy() { /* ... */ }
public void useCacheAsideStrategy() { /* ... */ }
}
程序输出
17:00:56.302 [main] INFO com.iluwatar.caching.App -- Using the 'in Memory' database to run the application.
17:00:56.304 [main] INFO com.iluwatar.caching.App -- # CachingPolicy.THROUGH
17:00:56.305 [main] INFO com.iluwatar.caching.AppManager -- Save record!
17:00:56.308 [main] INFO com.iluwatar.caching.App --
--CACHE CONTENT--
UserAccount(userId=001, userName=John, additionalInfo=He is a boy.)
----
17:00:56.308 [main] INFO com.iluwatar.caching.AppManager -- Trying to find 001 in cache
17:00:56.309 [main] INFO com.iluwatar.caching.CacheStore -- # Found in Cache!
17:00:56.309 [main] INFO com.iluwatar.caching.AppManager -- Trying to find 001 in cache
17:00:56.309 [main] INFO com.iluwatar.caching.CacheStore -- # Found in Cache!
17:00:56.309 [main] INFO com.iluwatar.caching.App -- ==============================================
17:00:56.309 [main] INFO com.iluwatar.caching.App -- # CachingPolicy.AROUND
17:00:56.309 [main] INFO com.iluwatar.caching.AppManager -- Save record!
17:00:56.309 [main] INFO com.iluwatar.caching.App --
--CACHE CONTENT--
----
17:00:56.309 [main] INFO com.iluwatar.caching.AppManager -- Trying to find 002 in cache
17:00:56.309 [main] INFO com.iluwatar.caching.CacheStore -- # Not found in cache! Go to DB!!
17:00:56.309 [main] INFO com.iluwatar.caching.App --
--CACHE CONTENT--
UserAccount(userId=002, userName=Jane, additionalInfo=She is a girl.)
----
17:00:56.309 [main] INFO com.iluwatar.caching.AppManager -- Trying to find 002 in cache
17:00:56.309 [main] INFO com.iluwatar.caching.CacheStore -- # Found in Cache!
17:00:56.309 [main] INFO com.iluwatar.caching.AppManager -- Save record!
17:00:56.309 [main] INFO com.iluwatar.caching.LruCache -- # 002 has been updated! Removing older version from cache...
17:00:56.309 [main] INFO com.iluwatar.caching.App --
--CACHE CONTENT--
----
17:00:56.309 [main] INFO com.iluwatar.caching.AppManager -- Trying to find 002 in cache
17:00:56.309 [main] INFO com.iluwatar.caching.CacheStore -- # Not found in cache! Go to DB!!
17:00:56.309 [main] INFO com.iluwatar.caching.App --
--CACHE CONTENT--
UserAccount(userId=002, userName=Jane G., additionalInfo=She is a girl.)
----
17:00:56.309 [main] INFO com.iluwatar.caching.AppManager -- Trying to find 002 in cache
17:00:56.309 [main] INFO com.iluwatar.caching.CacheStore -- # Found in Cache!
17:00:56.309 [main] INFO com.iluwatar.caching.App -- ==============================================
17:00:56.309 [main] INFO com.iluwatar.caching.App -- # CachingPolicy.BEHIND
17:00:56.309 [main] INFO com.iluwatar.caching.AppManager -- Save record!
17:00:56.309 [main] INFO com.iluwatar.caching.AppManager -- Save record!
17:00:56.310 [main] INFO com.iluwatar.caching.AppManager -- Save record!
17:00:56.310 [main] INFO com.iluwatar.caching.App --
--CACHE CONTENT--
UserAccount(userId=005, userName=Isaac, additionalInfo=He is allergic to mustard.)
UserAccount(userId=004, userName=Rita, additionalInfo=She hates cats.)
UserAccount(userId=003, userName=Adam, additionalInfo=He likes food.)
----
17:00:56.310 [main] INFO com.iluwatar.caching.AppManager -- Trying to find 003 in cache
17:00:56.310 [main] INFO com.iluwatar.caching.CacheStore -- # Found in cache!
17:00:56.310 [main] INFO com.iluwatar.caching.App --
--CACHE CONTENT--
UserAccount(userId=003, userName=Adam, additionalInfo=He likes food.)
UserAccount(userId=005, userName=Isaac, additionalInfo=He is allergic to mustard.)
UserAccount(userId=004, userName=Rita, additionalInfo=She hates cats.)
----
17:00:56.310 [main] INFO com.iluwatar.caching.AppManager -- Save record!
17:00:56.310 [main] INFO com.iluwatar.caching.CacheStore -- # Cache is FULL! Writing LRU data to DB...
17:00:56.310 [main] INFO com.iluwatar.caching.LruCache -- # Cache is FULL! Removing 004 from cache...
17:00:56.310 [main] INFO com.iluwatar.caching.App --
--CACHE CONTENT--
UserAccount(userId=006, userName=Yasha, additionalInfo=She is an only child.)
UserAccount(userId=003, userName=Adam, additionalInfo=He likes food.)
UserAccount(userId=005, userName=Isaac, additionalInfo=He is allergic to mustard.)
----
17:00:56.310 [main] INFO com.iluwatar.caching.AppManager -- Trying to find 004 in cache
17:00:56.310 [main] INFO com.iluwatar.caching.CacheStore -- # Not found in Cache!
17:00:56.310 [main] INFO com.iluwatar.caching.CacheStore -- # Cache is FULL! Writing LRU data to DB...
17:00:56.310 [main] INFO com.iluwatar.caching.LruCache -- # Cache is FULL! Removing 005 from cache...
17:00:56.310 [main] INFO com.iluwatar.caching.App --
--CACHE CONTENT--
UserAccount(userId=004, userName=Rita, additionalInfo=She hates cats.)
UserAccount(userId=006, userName=Yasha, additionalInfo=She is an only child.)
UserAccount(userId=003, userName=Adam, additionalInfo=He likes food.)
----
17:00:56.310 [main] INFO com.iluwatar.caching.App -- ==============================================
17:00:56.310 [main] INFO com.iluwatar.caching.App -- # CachingPolicy.ASIDE
17:00:56.310 [main] INFO com.iluwatar.caching.App --
--CACHE CONTENT--
----
17:00:56.310 [main] INFO com.iluwatar.caching.AppManager -- Save record!
17:00:56.310 [main] INFO com.iluwatar.caching.AppManager -- Save record!
17:00:56.310 [main] INFO com.iluwatar.caching.AppManager -- Save record!
17:00:56.310 [main] INFO com.iluwatar.caching.App --
--CACHE CONTENT--
----
17:00:56.310 [main] INFO com.iluwatar.caching.AppManager -- Trying to find 003 in cache
17:00:56.313 [main] INFO com.iluwatar.caching.App --
--CACHE CONTENT--
UserAccount(userId=003, userName=Adam, additionalInfo=He likes food.)
----
17:00:56.313 [main] INFO com.iluwatar.caching.AppManager -- Trying to find 004 in cache
17:00:56.313 [main] INFO com.iluwatar.caching.App --
--CACHE CONTENT--
UserAccount(userId=004, userName=Rita, additionalInfo=She hates cats.)
UserAccount(userId=003, userName=Adam, additionalInfo=He likes food.)
----
17:00:56.313 [main] INFO com.iluwatar.caching.App -- ==============================================
17:00:56.314 [Thread-0] INFO com.iluwatar.caching.CacheStore -- # flushCache...
使用 LRU 缓存和直写缓存等各种策略实现 Java 缓存设计模式可以显著提高应用程序的性能和可扩展性。
何时在 Java 中使用缓存模式
在以下情况下使用缓存模式
- 重复获取、初始化和释放相同资源会导致不必要的性能开销。
- 在重新计算或重新获取数据的成本明显高于将其存储和检索到缓存中的情况下。
- 对于数据相对静态或数据变化不频繁的读密集型应用程序。
Java 中缓存模式的实际应用
- 网页缓存,以减少服务器负载并提高响应时间。
- 数据库查询缓存,以避免重复的昂贵 SQL 查询。
- 缓存 CPU 密集型计算的结果。
- 内容分发网络 (CDN),用于缓存更靠近最终用户的静态资源,例如图像、CSS 和 JavaScript 文件。
缓存模式的优点和权衡
好处
- 提高性能:显著减少数据访问延迟,从而加快应用程序性能。
- 降低负载:降低底层数据源的负载,这可以节省成本并延长资源的使用寿命。
- 可扩展性:通过有效地处理负载增加,而不会成比例地增加资源利用率,来提高应用程序的可扩展性。
权衡
- 复杂性:在缓存失效、一致性和同步方面引入复杂性。
- 资源利用率:需要额外的内存或存储资源来维护缓存。
- 陈旧数据:如果缓存未在底层数据发生变化时被正确失效或更新,则存在提供过时数据的风险。
相关的 Java 设计模式
- 代理:可以使用代理模式实现缓存,其中代理对象拦截请求并在可用时返回缓存数据。
- 观察者:可用于在底层数据发生变化时通知缓存,以便相应地更新或失效缓存。
- 装饰器:可用于将缓存行为添加到现有对象,而无需修改其代码。
- 策略:可以使用策略模式实现不同的缓存策略,允许应用程序在运行时在它们之间切换。