【Java设计模式】Caching模式:加速数据访问速度

文章目录

  • 【Java设计模式】Caching模式:加速数据访问速度
    • 一、概述
    • 二、Caching设计模式的别名
    • 三、Caching设计模式的意图
    • 四、Caching模式的详细解释及实际示例
    • 五、Java中Caching模式的编程示例
    • 六、何时在Java中使用Caching模式
    • 七、Caching模式在Java中的实际应用
    • 八、Caching模式的优点和权衡
    • 九、源码下载

【Java设计模式】Caching模式:加速数据访问速度

一、概述

在Java开发中,Caching模式是一种用于优化性能和资源管理的重要设计模式。本文将详细介绍Caching模式的意图、解释、编程示例、适用场景、实际应用、优点和权衡。同时,还将提供示例代码的下载链接,方便读者进行学习和实践。

二、Caching设计模式的别名

  • Cache(缓存)
  • Temporary Storage(临时存储)

三、Caching设计模式的意图

Java中的Caching设计模式对于性能优化和资源管理至关重要。它涉及各种缓存策略,如写通、读通和LRU缓存,以确保高效的数据访问。该模式通过避免在使用后立即释放资源,而是将其保留在快速访问存储中,并重新使用它们,以避免再次获取资源的开销。

四、Caching模式的详细解释及实际示例

  1. 实际示例
    • Java中Caching设计模式的一个现实世界示例是图书馆的目录系统。通过缓存频繁搜索的图书结果,系统可以减少数据库负载并提高性能。当读者频繁搜索热门图书时,系统可以将这些搜索结果缓存起来。这样,当用户再次搜索同一本热门图书时,系统可以快速从缓存中获取结果,而无需每次都查询数据库。这不仅减少了数据库的负载,还提高了用户的响应速度,提升了用户体验。然而,系统还需要确保在添加新书或现有书被借出时,及时更新缓存,以保证信息的准确性。
  2. 通俗解释
    • Caching模式将频繁需要的数据保存在快速访问存储中,以提高性能。
  3. 维基百科解释
    • 在计算中,缓存是一种硬件或软件组件,用于存储数据,以便将来对该数据的请求能够更快地得到服务;存储在缓存中的数据可能是早期计算的结果或存储在其他地方的数据的副本。当请求的数据可以在缓存中找到时,发生缓存命中;当请求的数据在缓存中找不到时,发生缓存未命中。缓存命中时,数据从缓存中读取,这比重新计算结果或从较慢的数据存储中读取要快;因此,从缓存中服务的请求越多,系统的性能就越好。

五、Java中Caching模式的编程示例

在这个编程示例中,我们使用用户账户管理系统演示了不同的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;
        }
    }
    // 其他属性和方法...
    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("# 缓存已满!从缓存中删除 {}...", end.userId);
                cache.remove(end.userId); // 从缓存中删除 LRU 数据。
                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;
    // 其他属性和方法...
    public UserAccount readThrough(final String userId) {
        if (cache.contains(userId)) {
            LOGGER.info("# 在缓存中找到!");
            return cache.get(userId);
        }
        LOGGER.info("# 在缓存中未找到!转到数据库!");
        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.invalidate(userAccount.getUserId());
        } else {
            dbManager.writeToDb(userAccount);
        }
    }
    public static void clearCache() {
        if (cache!= null) {
            cache.clear();
        }
    }
    public static void flushCache() {
        LOGGER.info("# 刷新缓存...");
        Optional.ofNullable(cache)
           .map(LruCache::getCacheDataInListForm)
           .orElse(List.of())
           .forEach(DbManager::updateDb);
    }
    //... 省略了其他缓存策略的实现...
}

AppManager类有助于弥合主类和应用程序后端之间的通信差距。通过这个类初始化DB连接,并初始化选择的缓存策略/政策。在使用缓存之前,必须设置缓存的大小。根据选择的缓存策略,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("尝试在缓存中查找 {}", 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("保存记录!");
        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();
    }
    // 其他属性和方法...
}

下面是我们在应用程序的主类中所做的事情。

@Slf4j
public class App {
    public static void main(final String[] args) {
        boolean isDbMongo = isDbMongo(args);
        if (isDbMongo) {
            LOGGER.info("使用Mongo数据库引擎运行应用程序。");
        } else {
            LOGGER.info("使用'内存中'数据库运行应用程序。");
        }
        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() { /*... */ }
}

六、何时在Java中使用Caching模式

当出现以下情况时,应使用Caching模式:

  • 重复获取、初始化和释放相同资源会导致不必要的性能开销。
  • 重新计算或重新获取数据的成本明显高于从缓存中存储和检索数据的成本。
  • 对于读操作频繁、数据相对静态或数据更改不频繁的应用程序。

七、Caching模式在Java中的实际应用

  • 网页缓存,以减少服务器负载并提高响应时间。
  • 数据库查询缓存,以避免重复执行昂贵的SQL查询。
  • 缓存CPU密集型计算的结果。
  • 内容分发网络(CDN),用于缓存图像、CSS和JavaScript文件等静态资源,使其更接近最终用户。

八、Caching模式的优点和权衡

优点:

  • 提高性能:显著减少数据访问延迟,从而提高应用程序性能。
  • 减少负载:降低对底层数据源的负载,这可能导致成本节约和资源寿命的延长。
  • 可扩展性:通过有效地处理负载增加而无需按比例增加资源利用率,增强了应用程序的可扩展性。

权衡:

  • 复杂性:在缓存无效化、一致性和同步方面引入了复杂性。
  • 资源利用:需要额外的内存或存储资源来维护缓存。
  • 陈旧数据:如果在底层数据更改时缓存未正确无效化或更新,则存在提供过时数据的风险。

九、源码下载

Caching模式示例代码下载

通过本文的介绍,相信大家对Java中的Caching模式有了更深入的了解。在实际开发中,合理运用Caching模式可以提高应用程序的性能和响应速度,同时降低对资源的消耗。

你可能感兴趣的:(java,设计模式,开发语言)