学习

如何使用 Redis 进行查询缓存

Will Johnston
作者
Will Johnston, Redis 开发者增长经理
Prasan Kumar
作者
Prasan Kumar, Redis 技术解决方案开发者

什么是查询缓存?#

您是否遇到过数据库查询变慢的情况?查询缓存是您加速数据库查询所需的技术,它可以通过使用不同的缓存方法来降低成本!想象一下,您构建了一个电商应用。它刚开始规模很小,但正在快速增长。现在,您拥有庞大的产品目录和数百万客户。

这对业务有利,但对技术来说却是一种挑战。您对主数据库(MongoDB/PostgreSQL)的查询开始变慢,即使您已经尝试过优化。即便能挤出一点额外的性能,也不足以满足您的客户。

为何应使用 Redis 进行查询缓存#

Redis 是一个内存数据存储,以缓存闻名。使用 Redis 可以减少主数据库的负载,同时加快数据库读取速度。

对于任何电商应用来说,有一种特定类型的查询是最常被请求的。如果您猜到是产品搜索查询,那就对了!

为了改进电商应用中的产品搜索,您可以实现以下任一缓存模式

  • 缓存预取:整个产品目录可以在 Redis 中预先缓存,应用可以在 Redis 上执行任何产品查询,类似于主数据库。
  • 旁路缓存 模式:Redis 根据前端请求的搜索参数按需填充。
提示

如果您使用 Redis Cloud,由于其支持 JSON 和搜索,旁路缓存更容易实现。您还可以获得额外功能,如实时性能、高可扩展性、弹性和容错能力。您还可以利用高可用性特性,如 Active-Active 地理冗余。

本教程侧重于 旁路缓存 模式。此设计模式的目标是为提高读取操作性能而设置 最优 缓存(按需加载)。对于缓存,您可能熟悉“缓存未命中”,即在缓存中找不到数据,以及“缓存命中”,即在缓存中找到数据。让我们看看旁路缓存模式在使用 Redis 处理“缓存未命中”和“缓存命中”时是如何工作的。

使用 Redis 的旁路缓存(缓存未命中)#

此图说明了在“缓存未命中”情况下,旁路缓存模式所采取的步骤。要理解其工作原理,请考虑以下流程

  1. 1.应用从后端请求数据。
  2. 2.后端检查数据是否在 Redis 中可用。
  3. 3.数据未找到(缓存未命中),因此从数据库获取数据。
  4. 4.从数据库返回的数据随后存储到 Redis 中。
  5. 5.然后将数据返回给应用。

使用 Redis 的旁路缓存(缓存命中)#

既然您已经了解了“缓存未命中”是什么样子,接下来我们看看“缓存命中”。这是相同的图,但用绿色高亮显示了“缓存命中”的步骤。

  1. 1.应用从后端请求数据。
  2. 2.后端检查数据是否在 Redis 中可用。
  3. 3.然后将数据返回给应用。

旁路缓存模式在您需要以下情况时很有用

  1. 1.频繁查询数据:当您有大量读取操作时(如电商应用中),旁路缓存模式可以为后续数据请求提供即时性能提升。
  2. 2.按需填充缓存:旁路缓存模式在请求数据时填充缓存,而不是预先缓存,从而节省空间和成本。当不确定哪些数据需要缓存时,这非常有用。
  3. 3.注重成本:由于缓存大小与云中缓存存储的成本直接相关,因此大小越小,支付的费用就越少。
提示

如果您使用 Redis Cloud 和使用 JDBC 驱动程序的数据库,您可以利用 Redis 智能缓存,它允许您在不更改代码的情况下为应用添加缓存。 单击此处了解更多!

电商应用的微服务架构#

本教程其余部分讨论的电商微服务应用采用以下架构

  1. 1.products service:负责从数据库查询产品并将其返回给前端
  2. 2.orders service:负责验证和创建订单
  3. 3.order history service:负责查询客户的订单历史记录
  4. 4.payments service:负责处理订单支付
  5. 5.digital identity service:负责存储数字身份和计算身份评分
  6. 6.api gateway:在单个端点下统一服务
  7. 7.mongodb/ postgresql:用作主数据库,存储订单、订单历史记录、产品等
  8. 8.redis:用作 流处理器 和缓存数据库
信息

在演示应用中,您无需使用 MongoDB/PostgreSQL 作为主数据库;您也可以使用其他 Prisma 支持的数据库。这只是一个示例。

使用 Next.js 和 Tailwind 的电商应用前端#

电商微服务应用包括一个前端,使用 Next.js 和 TailwindCSS 构建。应用后端使用 Node.js。数据使用 Prisma 存储在 Redis 和 MongoDB/PostgreSQL 中。您将在下方找到电商应用前端的截图

  • Dashboard:显示带搜索功能的产品列表

Shopping Cart:将产品添加到购物车,然后使用“立即购买”按钮结账

注意

以下是克隆本教程中使用的应用源代码的命令

git clone --branch v4.2.0 https://github.com/redis-developer/redis-microservices-ecommerce-solutions

在微服务应用中使用 Redis 和主数据库(MongoDB/PostgreSQL)进行缓存#

在我们的示例应用中,产品服务发布了一个用于过滤产品的 API。以下是对该 API 的调用示例

按筛选条件获取产品的请求#

docs/api/get-products-by-filter.md
// POST http://localhost:3000/products/getProductsByFilter
{
  "productDisplayName": "puma"
}

按筛选条件获取产品的响应(缓存未命中)#

{
  "data": [
    {
      "productId": "11000",
      "price": 3995,
      "productDisplayName": "Puma Men Slick 3HD Yellow Black Watches",
      "variantName": "Slick 3HD Yellow",
      "brandName": "Puma",
      "ageGroup": "Adults-Men",
      "gender": "Men",
      "displayCategories": "Accessories",
      "masterCategory_typeName": "Accessories",
      "subCategory_typeName": "Watches",
      "styleImages_default_imageURL": "http://host.docker.internal:8080/images/11000.jpg",
      "productDescriptors_description_value": "<p style=\"text-align: justify;\">Stylish and comfortable, ...",
      "createdOn": "2023-07-13T14:07:38.020Z",
      "createdBy": "ADMIN",
      "lastUpdatedOn": "2023-07-13T14:07:38.020Z",
      "lastUpdatedBy": null,
      "statusCode": 1
    }
    //...
  ],
  "error": null,
  "isFromCache": false
}

按筛选条件获取产品的响应(缓存命中)#

{
  "data": [
    //...same data as above
  ],
  "error": null,
  "isFromCache": true // now the data comes from the cache rather DB
}

使用 Redis 和主数据库(MongoDB/PostgreSQL)实现旁路缓存#

以下代码显示了用于在主数据库中搜索产品的函数

server/src/services/products/src/service-impl.ts
async function getProductsByFilter(productFilter: Product) {
  const prisma = getPrismaClient();

  const whereQuery: Prisma.ProductWhereInput = {
    statusCode: DB_ROW_STATUS.ACTIVE,
  };

  if (productFilter && productFilter.productDisplayName) {
    whereQuery.productDisplayName = {
      contains: productFilter.productDisplayName,
      mode: 'insensitive',
    };
  }

  const products: Product[] = await prisma.product.findMany({
    where: whereQuery,
  });

  return products;
}

您只需调用主数据库(MongoDB/PostgreSQL),根据产品 displayName 属性上的筛选条件查找产品。您可以设置多个列以实现更好的模糊搜索,但为了本教程的目的,我们将其简化了。

直接使用主数据库而不使用 Redis 一开始可行,但最终会变慢。这就是为什么您可能使用 Redis 来加速。旁路缓存模式有助于平衡性能与成本。

旁路缓存的基本决策树如下。

当前端请求产品时

  1. 1.使用请求内容(即搜索参数)形成一个哈希。
  2. 2.检查 Redis,查看该哈希是否存在对应的值。
  3. 3.是否缓存命中?如果找到了该哈希对应的数据,则返回该数据;流程到此停止。
  4. 4.是否缓存未命中?如果未找到数据,则从主数据库中读取数据,随后在返回之前将其存储到 Redis 中。

这是实现此决策树的代码

server/src/services/products/src/routes.ts
const getHashKey = (_filter: Document) => {
  let retKey = '';
  if (_filter) {
    const text = JSON.stringify(_filter);
    retKey = crypto.createHash('sha256').update(text).digest('hex');
  }
  return 'CACHE_ASIDE_' + retKey;
};

router.post(API.GET_PRODUCTS_BY_FILTER, async (req: Request, res: Response) => {
  const body = req.body;
  // using node-redis
  const redis = getNodeRedisClient();

  //get data from redis
  const hashKey = getHashKey(req.body);
  const cachedData = await redis.get(hashKey);
  const docArr = cachedData ? JSON.parse(cachedData) : [];

  if (docArr && docArr.length) {
    result.data = docArr;
    result.isFromCache = true;
  } else {
    // get data from primary database
    const dbData = await getProductsByFilter(body); //method shown earlier

    if (body && body.productDisplayName && dbData.length) {
      // set data in redis (no need to wait)
      redis.set(hashKey, JSON.stringify(dbData), {
        EX: 60, // cache expiration in seconds
      });
    }

    result.data = dbData;
  }

  res.send(result);
});
提示

您需要根据您的具体用例决定最适合的过期时间或生存时间 (TTL)。

准备好使用 Redis 进行查询缓存了吗?#

您现在已经知道如何使用 Redis 以及最常见的缓存模式之一(旁路缓存)进行缓存。可以根据需要逐步采用 Redis,使用不同的策略/模式。有关微服务的更多资源,请查看以下链接

附加资源#

使用 Redis 的微服务

通用