Guava精讲(三)-Caches,同步DB数据到缓存

2023-09-17 16:07:15

在开发中,我们经常需要从数据库中读取数据并进行频繁的读取操作。缓存在各种场景中都有运用,例如,当一个值的计算或检索成本很高,而且在某个输入中需要多次使用该值时,就应该考虑使用缓存,因此将数据缓存在内存中可以显著提高应用程序的性能。

问题描述

假设我们正在开发一个电子商务网站,需要频繁地显示商品信息。商品信息存储在数据库中,并且我们希望将其缓存在内存中,以提高网站的响应速度和性能。

缓存与 ConcurrentMap 相似,但又不完全相同。最根本的区别在于,ConcurrentMap 会持久保存所有添加到其中的元素,直到它们被操作删除。另一方面,缓存通常被配置为自动被销毁,以限制其占用过多内存空间。在某些情况下,LoadingCache 即使不被销毁,也会因其自动加载缓存而发挥作用。

一般来说,Guava 缓存工具适用于以下情况:

  • 愿意通过消耗内存来提高数据存取速度。
  • 存在热键会被频繁查询。
  • 缓存不能存储超过 RAM 容量的数据。(Guava 缓存是应用程序单次运行的本地缓存。它们不会将数据存储在文件中或外部服务器上。)。

LoadingCache

LoadingCache 是通过附加的 CacheLoader 构建缓存。创建 CacheLoader 通常就像实现方法 V load(K key) throws Exception 一样简单。例如,你可以用下面代码创建一个 LoadingCache:

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .maximumSize(1000)
       .build(
           new CacheLoader<Key, Graph>() {
             public Graph load(Key key) throws AnyException {
               return createExpensiveGraph(key);
             }
           });

...
try {
  return graphs.get(key);
} catch (ExecutionException e) {
  throw new OtherException(e.getCause());
}

查询 LoadingCache 的常规方法是使用 get(K) 方法。该方法要么返回一个已缓存的值,要么使用缓存的 CacheLoader 将一个新值加载到缓存中。由于 CacheLoader 可能会抛出异常,因此 LoadingCache.get(K) 会抛出 ExecutionException。(如果LoadingCache抛出一个未检查异常,get(K) 将抛出一个UncheckedExecutionException)。我们可以选择使用 getUnchecked(K),它会用 UncheckedExecutionException 封装所有异常,但底层的 CacheLoader 通常会抛出ExecutionException。

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .expireAfterAccess(10, TimeUnit.MINUTES)
       .build(
           new CacheLoader<Key, Graph>() {
             public Graph load(Key key) { // no checked exception
               return createExpensiveGraph(key);
             }
           });
...
return graphs.getUnchecked(key);

批量查找可通过方法 getAll(Iterable<? extends K>) 执行。默认情况下,getAll 会对缓存中不存在的每个键单独调用 CacheLoader.load。当批量检索比多次单独查找更有效时,可以重载 CacheLoader.loadAll 来利用这一点。getAll(Iterable)的性能也会相应提高。

Callable

所有 Guava 缓存(无论是否正在加载)都支持 get(K, Callable<V>) 方法。该方法会返回与缓存中的键相关联的值,或从指定的 Callable 中计算该值并将其添加到缓存中。在加载完成之前,与该缓存相关的可观察状态不会被修改。该方法可简单替代传统的 "如果已缓存,则返回;否则创建、缓存并返回 "模式。

Cache<Key, Value> cache = CacheBuilder.newBuilder()
    .maximumSize(1000)
    .build(); // look Ma, no CacheLoader
...
try {
  // If the key wasn't in the "easy to compute" group, we need to
  // do things the hard way.
  cache.get(key, new Callable<Value>() {
    @Override
    public Value call() throws AnyException {
      return doThingsTheHardWay(key);
    }
  });
} catch (ExecutionException e) {
  throw new OtherException(e.getCause());
}
数据直接插入缓存

可通过 cache.put(key, value) 直接将数值插入缓存。这将覆盖缓存中指定键的任何先前数据。还可以使用 Cache.asMap() 视图中的任何 ConcurrentMap 方法对缓存进行更改。请注意,asMap 视图上的任何方法都不会导致数据被自动加载到缓存中。此外,该视图上的原子操作不在自动加载缓存的范围内,因此在使用 CacheLoader 或 Callable 加载值的缓存中,Cache.get(K, Callable<V>) 应始终优于 Cache.asMap().putIfAbsent()。请注意,Cache.get(K, Callable) 也可能将值插入底层缓存。

缓存销毁

即便缓存存取数据效率如此之高,但现实是通常内存不像硬盘空间可以不是无限制增加,毕竟成本比硬盘成本高很多,所以我们无法使用内存来缓存所有可以缓存的内容。因此开发者必须考虑到:缓存数据应该何时被销毁的问题?Guava 提供了三种基本的销毁类型:基于大小的销毁、基于时间的销毁和基于引用的销毁。

1. 基于内存使用大小的销毁方式

如果我们在开发过程中规定内存空间的使用不应该增长超过一定大小,只需使用 CacheBuilder.maximumSize(long)。缓存会自动销毁最近未使用或不常用的数据。

另外,如果不同的缓存数据具有不同的 "权重",我们可以使用 CacheBuilder.weigher(Weigher) 指定权重函数,并使用 CacheBuilder.maximumWeight(long) 指定最大缓存权重。除了与 maximumSize 要求相同的注意事项外,请注意权重是在创建数据时计的,此后将保持静态。

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .maximumWeight(100000)
       .weigher(new Weigher<Key, Graph>() {
          public int weigh(Key k, Graph g) {
            return g.vertices().size();
          }
        })
       .build(
           new CacheLoader<Key, Graph>() {
             public Graph load(Key key) { // no checked exception
               return createExpensiveGraph(key);
             }
           });
2. 定时销毁

CacheBuilder 提供两种定时销毁方法:

  • expireAfterAccess(long, TimeUnit) 自数据上次被读取/写入后,只有在指定的持续时间过后,数据才会过期
  • expireAfterWrite(long, TimeUnit) 在数据创建/更新后的指定时间内,使数据过期。

如何测试定时销毁??

测试定时销毁并不一定很痛苦......测试两秒钟的过期也不一定非要花上两秒钟。使用 Ticker 接口和 CacheBuilder.ticker(Ticker) 方法在缓存创建器中指定一个时间源,而不必等待系统时钟。

3. 基于引用的销毁

Guava 允许开发者通过对键或值使用弱引用和对值使用软引用来设置缓存,以允许对数据进行垃圾回收。

  • CacheBuilder.weakKeys() 使用弱引用存储键值。这样,如果键没有其他(强或软)引用,数据就能被垃圾回收。由于垃圾回收只依赖于身份相等,这将导致整个缓存使用身份(==)相等来比较键,而不是使用 equals()。
  • CacheBuilder.weakValues() 使用弱引用存储值。如果值没有其他(强或软)引用,数据就会被垃圾回收。由于垃圾回收只依赖于身份相等,这将导致整个缓存使用身份(==)相等来比较值,而不是使用 equals()。
  • CacheBuilder.softValues() 将值封装为软引用。软引用对象会根据内存需求,在全局范围内以最近使用最少的方式进行垃圾回收。由于使用软引用会影响性能,我们通常建议使用更可预测的最大缓存大小。使用 softValues() 会导致使用身份 (==) 平等而不是 equals() 来比较值。
4. 显式销毁

我们可以随时显式地使缓存数据失效,而不是等待数据被销毁。

  • 单数据缓存,使用 Cache.invalidate(key)
  • 批量缓存,使用 Cache.invalidateAll(keys)
  • 对所有数据,使用 Cache.invalidateAll()

销毁监听器

我们可以通过 CacheBuilder.removalListener(RemovalListener) 为缓存指定一个移除监听器,以便在数据被移除时执行某些操作。RemovalListener 会收到一个 RemovalNotification,其中指定了 RemovalCause、key 和 value。

请注意,RemovalListener 抛出的任何异常都会被记录(使用日志记录器)并覆盖。

CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection> () {
  public DatabaseConnection load(Key key) throws Exception {
    return openConnection(key);
  }
};
RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {
  public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
    DatabaseConnection conn = removal.getValue();
    conn.close(); // tear down properly
  }
};

return CacheBuilder.newBuilder()
  .expireAfterWrite(2, TimeUnit.MINUTES)
  .removalListener(removalListener)
  .build(loader);

何时进行清理?

使用 CacheBuilder 构建的缓存不会 "自动 "或在值过期后立即执行清理和销毁值等操作。相反,它会在写操作过程中进行少量维护,如果写操作很少,则会在偶尔的读操作过程中进行维护。

这样做的原因如下:如果我们想持续执行缓存维护,就需要单独创建一个线程,而线程的操作将与用户操作竞争共享锁。此外,某些环境限制创建线程,这将导致 CacheBuilder 在该环境中无法使用。

因此,我们将选择权交给了开发者。如果开发者需要的缓存是高吞吐量的,那么就不必担心执行缓存维护来清理过期数据等问题。如果开发者使用的缓存缓存很少进行写入操作,而且不希望清理工作阻塞缓存读取,那么就可以创建自己的维护线程,定期调用 Cache.cleanUp()。

如果要为很少写入的缓存安排定期缓存维护,只需使用 ScheduledExecutorService 安排维护即可。

Refresh缓存刷新

刷新与销毁不太一样。正如在 LoadingCache.refresh(K) 中指定的那样,刷新键会为该键加载一个新值,可能是异步加载。在键被刷新的同时,旧值(如果有的话)仍会返回,这与强制检索直到值被重新加载的销毁不同。

如果在刷新时出现异常,旧值会被保留,异常会被记录并吞没。

CacheLoader 可以通过重载 CacheLoader.reload(K,V)来指定刷新时使用的智能行为,这样就可以在计算新值时使用旧值。

// Some keys don't need refreshing, and we want refreshes to be done asynchronously.
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .maximumSize(1000)
       .refreshAfterWrite(1, TimeUnit.MINUTES)
       .build(
           new CacheLoader<Key, Graph>() {
             public Graph load(Key key) { // no checked exception
               return getGraphFromDatabase(key);
             }

             public ListenableFuture<Graph> reload(final Key key, Graph prevGraph) {
               if (neverNeedsRefresh(key)) {
                 return Futures.immediateFuture(prevGraph);
               } else {
                 // asynchronous!
                 ListenableFutureTask<Graph> task = ListenableFutureTask.create(new Callable<Graph>() {
                   public Graph call() {
                     return getGraphFromDatabase(key);
                   }
                 });
                 executor.execute(task);
                 return task;
               }
             }
           });

可以使用 CacheBuilder.refreshAfterWrite(long, TimeUnit) 为缓存添加自动定时刷新功能。与 expireAfterWrite 不同的是,refreshAfterWrite 将使键在指定的持续时间后符合刷新条件,但只有在查询数据时才会实际启动刷新(如果 CacheLoader.reload 被实现为异步,那么查询将不会因刷新而减慢)。因此,举例来说,你可以在同一个缓存上同时指定 refreshAfterWrite 和 expireAfterWrite,这样,每当一个数据符合刷新条件时,数据的过期计时器就不会被盲目重置,因此,如果一个数据符合刷新条件后没有被查询,它就会过期。

案例实践

咱们就以文章开头举例的应用场景来实现一个自动读取DB数据到缓存的代码。

步骤1:添加依赖

首先,我们需要在项目的构建文件(比如Maven的pom.xml文件)中添加Guava Cache的依赖。

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>30.1-jre</version>
</dependency>
步骤2:创建数据库连接

首先,我们需要创建一个数据库连接,以便从数据库中读取数据。

package cacher;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DatabaseConnection {
    private static final String URL = "jdbc:mysql://localhost:3306/quality_assurance";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "sql123456";

    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(URL, USERNAME, PASSWORD);
    }
}
步骤3:创建ProductOb
package cacher;

import lombok.Data;

@Data
public class Product {

    private String name;
    private Double price;
    private String id;

    public Product(String id, String name, Double price) {
        this.name = name;
        this.price = price;
        this.id = id;
    }
}
步骤4:创建缓存实例

接下来,我们需要创建一个缓存实例,用于存储从数据库中读取的数据。

package cacher;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

import java.util.concurrent.TimeUnit;

public class ProductCache {

    private static final int MAXIMUM_SIZE = 1000;
    /*
        缓存数据的过期时间,单位为分钟
     */
    private static final int EXPIRE_AFTER_WRITE_DURATION = 10;

    private static final Cache<Long, Product> cache = CacheBuilder.newBuilder()
            .maximumSize(MAXIMUM_SIZE)
            .expireAfterWrite(EXPIRE_AFTER_WRITE_DURATION, TimeUnit.MINUTES)
            .build();

    public static Cache<Long, Product> getCache() {
        return cache;
    }
}
步骤5:从数据库中读取数据并写入缓存

现在,我们可以使用数据库连接和缓存实例来从数据库中读取数据并写入缓存。

package cacher;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class ProductDao {
    public Product getProductById(long id) throws SQLException {
        // 先从缓存中查找数据
        Product product = ProductCache.getCache().getIfPresent(id);

        if (product == null) {
            // 如果缓存中不存在数据,则从数据库中读取数据
            Connection connection = DatabaseConnection.getConnection();
            PreparedStatement statement = connection.prepareStatement("SELECT * FROM products WHERE id = ?");
            statement.setLong(1, id);
            ResultSet resultSet = statement.executeQuery();


            if (resultSet.next()) {

                // 将从数据库中读取的数据存入缓存
                product = new Product(resultSet.getString("id"), resultSet.getString("name"), resultSet.getDouble("price"));
                ProductCache.getCache().put(id, product);
            }

            resultSet.close();
            statement.close();
            connection.close();
        }

        return product;
    }
}
步骤6:使用缓存中的数据

最后,我们开发CacheClient,实现使用缓存中的数据,而无需每次都访问数据库。

package cacher;

import java.sql.SQLException;

public class CacheClient {
    public static void main(String[] args) throws SQLException {
        ProductDao productDao = new ProductDao();
        long productId = 10000001;
        Product product = productDao.getProductById(productId);

        if (product != null) {
            System.out.println("Product found: " + product.getName() + ", Price: " + product.getPrice());
        } else {
            System.out.println("Product not found with ID: " + productId);
        }
    }
}

更多推荐

RocketMQ 源码分析——Producer

文章目录消息发送代码实现消息发送者启动流程检查配置获得MQ客户端实例启动实例定时任务Producer消息发送流程选择队列默认选择队列策略故障延迟机制策略*两种策略的选择技术亮点:ThreadLocal消息发送代码实现下面是一个生产者发送消息的demo(同步发送)主要做了几件事:初始化一个生产者(DefaultMQPro

四、线性支持向量机算法(LinearSVC,Linear Support Vector Classification)(有监督学习)

线性支持向量机,LinearSupportVectorClassification.与参数内核为线性的SVC类似(SVC(kernel=‘linear’)),但使用liblinear而非libsvm实现,因此在选择惩罚和损失函数时更具灵活性,并能更好地扩展到大量样本SVC(kernel=’linear’)和Linear

Meta | 对比解码:进一步提升LLM推理能力

深度学习自然语言处理原创作者:wkk为了改进LLM的推理能力,UniversityofCalifornia联合MetaAI实验室提出将ContrastiveDecoding应用于多种任务的LLM方法。实验表明,所提方法能有效改进LLM的推理能力。让我们走进论文一探究竟吧!论文:ContrastiveDecodingIm

《动手学深度学习 Pytorch版》 6.1 从全连接层到卷积

6.1.1不变性平移不变性(translationinvariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性”。局部性(locality):神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则

《动手学深度学习 Pytorch版》 7.1 深度卷积神经网络(LeNet)

7.1.1学习表征深度卷积神经网络的突破出现在2012年。突破可归因于以下两个关键因素:缺少的成分:数据数据集紧缺的情况在2010年前后兴起的大数据浪潮中得到改善。ImageNet挑战赛中,ImageNet数据集由斯坦福大学教授李飞飞小组的研究人员开发,利用谷歌图像搜索对分类图片进行预筛选,并利用亚马逊众包标注每张图片

QT基础教程(文本绘制)

文章目录前言一、普通文本绘制二、绘制旋转文本三、旋转文本升级总结前言本篇文章我们来讲解一下QT中使用QPainter来绘制文本的案例。一、普通文本绘制在Qt中,你可以使用QPainter类来绘制文本,包括普通文本、格式化文本和自定义文本效果。下面是使用QPainter绘制文本的基本方法和示例:1.绘制普通文本:使用QP

Shiro【核心功能、核心组件、项目搭建 、配置文件认证、数据库认证 】(一)-全面详解(学习总结---从入门到深化)

目录Shiro介绍_Shiro核心功能Shiro介绍_Shiro核心组件Shiro入门_项目搭建Shiro入门_配置文件认证Shiro入门_数据库认证Shiro认证_将Shiro对象交给容器管理Shiro介绍_Shiro简介Shiro是apache旗下的一个开源安全框架,它可以帮助我们完成身份认证,授权、加密、会话管理

解锁前端Vue3宝藏级资料 第五章 Vue 组件应用 5 (Vue 插件)

想了解Vue插件所以你看了官方文档却看不懂,或者你想知道Vue.use()方法和插件的关系。在本文档中,我们将参照文档讲解插件制作的基础知识,了解基础知识后,我们将制作与更实用的下拉菜单和脚本加载相关的插件。读完之后,您应该知道如何创建自己的插件以及如何添加插件。第一章Vue3项目创建1VueCLI创建vue项目第一章

Centos安装postgresql

一.执行安装命令与查看是否成功:1.yuminstall-ypostgresql-serverpostgresql-contrib2.安装后执行$psql--version或$psql-V可显示psql(PostgreSQL)9.2.243.另外,安装的同时还会创建postgres用户,Home为/var/lib/pg

构建无限画布,协作数字绘图 | 开源日报 0915

tldraw/tldrawStars:16.4kLicense:Apache-2.0tldraw是一个协作数字白板项目,可在tldraw.com上使用。它的编辑器、用户界面和其他底层库都是开源的,并且可以通过npm进行分发。您可以使用tldraw为产品创建一个即插即用的白板,或者将其作为构建自己无限画布应用程序的基础。

小程序开发一个多少钱啊

在今天的数字化时代,小程序已经成为一种非常流行的应用程序形式。由于它们的便捷性、易用性和多功能性,小程序吸引了越来越多的用户和企业。但是,很多人在考虑开发一个小程序时,都会遇到同一个问题:开发一个小程序需要多少钱?小程序的开发费用因人而异,取决于多种因素。下面,我们将为您详细列出影响小程序开发费用的主要因素。1、功能需

热文推荐