学习

如何使用 Redis 和 CQRS 模式构建电子商务应用程序

Will Johnston
作者
Will Johnston, Redis 的开发者增长经理
Prasan Kumar
作者
Prasan Kumar, Redis 的技术解决方案开发人员
GITHUB 代码

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

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

什么是命令和查询责任分离 (CQRS)?#

命令查询责任分离 (CQRS) 是微服务架构中的一个关键模式。它将读取(查询)和写入(命令)解耦,从而允许读取和写入工作负载独立工作。

命令(写入)侧重于更高的持久性和一致性,而查询(读取)侧重于性能。这使微服务能够将数据写入较慢的记录系统磁盘数据库,同时在缓存中预取和缓存这些数据以进行实时读取。

这个想法很简单:将命令(例如“订购此产品”(写入操作))与查询(例如“显示我的订单历史记录”(读取操作))分开。CQRS 应用程序通常基于消息,并依赖于 最终一致性.

以下示例数据架构演示了如何将 Redis 与 CQRS 一起使用

图中说明的架构使用更改数据捕获模式(称为“集成 CDC”)来跟踪命令数据库上的更改状态,并将更改状态复制到查询数据库(Redis)。这是 CQRS 中使用的常见模式。

实现 CDC 需要

  1. 1.从记录系统获取数据快照
  2. 2.执行最终化为将数据加载到目标缓存数据库的 ETL 操作
  3. 3.设置一种机制来持续地将记录系统中的更改流式传输到缓存
提示

虽然可以使用触发器和函数使用 Redis 实现自己的 CDC 机制,但 Redis Cloud 带有自己的集成 CDC 机制,可以为您解决此问题。

可能使用 CQRS 的原因#

为了提高应用程序性能,分别扩展您的读取和写入操作。

考虑以下场景:您有一个电子商务应用程序,允许客户使用产品填充购物车。该网站有一个“立即购买”按钮,以便于订购这些产品。从一开始,您可能会设置并填充一个产品数据库(可能是 SQL 数据库)。然后,您可能会编写一个后端 API 来处理创建订单、创建发票、处理付款、处理履行和更新客户订单历史记录的过程……一次性完成所有操作。

这种同步订单处理方法似乎是个好主意。但是,您很快就会发现,随着客户数量的增加和销售量的增长,数据库的速度会变慢。实际上,大多数应用程序的读取次数远远超过写入次数。您应该分别扩展这些操作。

您决定需要快速处理订单,以便客户不必等待。然后,有时间的时候,您可以创建发票、处理付款、处理履行等。

因此,您决定将这些步骤分开。使用 CQRS 的微服务方法允许您分别扩展您的读取和写入,并有助于解耦您的微服务。在 CQRS 模型中,单个服务负责端到端地处理整个命令。一个服务不应依赖于另一个服务才能完成命令。

电子商务应用程序的微服务 CQRS 架构#

让我们看一下演示应用程序的架构

  1. 1.products service: 处理从数据库查询产品并将其返回到前端
  2. 2.orders service: 处理验证和创建订单
  3. 3.order history service: 处理查询客户的订单历史记录
  4. 4.payments service: 处理订单付款
  5. 5.api gateway: 将服务统一到一个端点下
  6. 6.mongodb/ postgresql: 充当用于存储订单、订单历史记录、产品等的写入优化数据库
信息

您不需要在演示应用程序中使用 MongoDB/ Postgresql 作为写入优化数据库;您也可以使用其他 prisma 支持的数据库 。这只是一个例子。

在微服务架构中使用 CQRS#

请注意,在当前架构中,所有服务都使用相同的底层数据库。即使您在技术上将读取和写入分开,也无法独立扩展写入优化数据库。这就是 Redis 的用武之地。如果您将 Redis 放在写入优化数据库之前,您可以将其用于读取,同时写入写入优化数据库。Redis 的好处是它对读取和写入速度很快,这就是它成为缓存和 CQRS 的最佳选择的原因。

信息

出于本教程的目的,我们没有重点介绍如何在服务之间协调通信,例如如何处理新的订单以进行付款。该过程使用 Redis Streams,并在我们的 服务间通信指南中概述。

提示

当您的电子商务应用程序最终需要在全球范围内扩展时,Redis Cloud 为读取和写入提供主动-主动地理分布,以实现本地延迟以及 99.999% 的正常运行时间。

让我们看一下一些示例代码,这些代码有助于使用 Redis 和主数据库 (MongoDB/ Postgressql) 促进 CQRS 模式。

使用 Next.js 和 Tailwind 的电子商务应用程序前端#

该电子商务微服务应用程序包含一个前端,使用 Next.js 和 TailwindCSS构建。应用程序后端使用 Node.js。数据存储在 Redis 和 MongoDB/ Postgressql 中,使用 Prisma。您将在下面找到电子商务应用程序前端的屏幕截图

  • Dashboard: 显示产品列表并具有搜索功能

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

订单历史记录: 下单后,顶部导航栏中的“订单”链接会显示订单状态和历史记录

注意

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

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

使用 Redis 和主数据库 (MongoDB/ Postgressql) 构建 CQRS 微服务应用程序#

让我们看一下 order service 的示例代码,并查看 CreateOrder 命令(写入操作)。然后,我们看一下 order history service 以查看 ViewOrderHistory 命令(读取操作)。

创建订单命令 API#

以下代码显示了创建订单的示例 API 请求和响应。

创建订单请求#

docs/api/create-order.md
// POST https://api-gateway/orders/createOrder
{
  "products": [
    {
      "productId": "11002",
      "qty": 1,
      "productPrice": 4950
    },
    {
      "productId": "11012",
      "qty": 2,
      "productPrice": 1195
    }
  ]
}

创建订单响应#

{
  "data": "d4075f43-c262-4027-ad25-7b1bc8c490b6", //orderId
  "error": null
}

当您发出请求时,它会通过 API 网关发送到 orders service。最终,它会调用 createOrder 函数,该函数如下所示

server/src/services/orders/src/service-impl.ts
const createOrder = async (
  order: IOrder,
  //...
) => {
  if (!order) {
    throw 'Order data is mandatory!';
  }

  const userId = order.userId || USERS.DEFAULT; // Used as a shortcut, in a real app you would use customer session data to fetch user details
  const orderId = uuidv4();

  order.orderId = orderId;
  order.orderStatusCode = ORDER_STATUS.CREATED;
  order.userId = userId;
  order.createdBy = userId;
  order.statusCode = DB_ROW_STATUS.ACTIVE;
  order.potentialFraud = false;

  order = await validateOrder(order);

  const products = await getProductDetails(order);
  addProductDataToOrders(order, products);

  await addOrderToRedis(order);

  await addOrderToPrismaDB(order);

  //...

  return orderId;
};
信息

为了教程的简单性,我们在同一个服务中将数据添加到主数据库和 Redis(双重写入)。如前所述,一种常见的模式是让您的服务写入一个数据库,然后分别使用 CDC 机制来更新另一个数据库。例如,您可以直接写入 Redis,然后使用 RedisGears 来处理 Redis 和主数据库在后台的同步。在本教程中,我们没有详细说明如何处理同步,而是重点介绍数据如何在 Redis 中存储和访问。

提示

如果您使用的是 Redis Cloud,您可以利用 集成 CDC 机制来避免自己编写代码。

请注意,在前面的代码块中,我们调用了 addOrderToRedis 函数来将订单存储在 Redis 中。我们使用 Redis OM for Node.js 将订单实体存储在 Redis 中。该函数如下所示

server/src/services/orders/src/service-impl.ts
import { Schema, Repository } from 'redis-om';
import { getNodeRedisClient } from '../utils/redis/redis-wrapper';

//Redis Om schema for Order
const schema = new Schema('Order', {
  orderId: { type: 'string', indexed: true },

  orderStatusCode: { type: 'number', indexed: true },
  potentialFraud: { type: 'boolean', indexed: false },
  userId: { type: 'string', indexed: true },

  createdOn: { type: 'date', indexed: false },
  createdBy: { type: 'string', indexed: true },
  lastUpdatedOn: { type: 'date', indexed: false },
  lastUpdatedBy: { type: 'string', indexed: false },
  statusCode: { type: 'number', indexed: true },
});

//Redis OM repository for Order (to read, write and remove orders)
const getOrderRepository = () => {
  const redisClient = getNodeRedisClient();
  const repository = new Repository(schema, redisClient);
  return repository;
};

//Redis indexes data for search
const createRedisIndex = async () => {
  const repository = getRepository();
  await repository.createIndex();
};

const addOrderToRedis = async (order: OrderWithIncludes) => {
  if (order) {
    const repository = getOrderRepository();
    //insert Order in to Redis
    await repository.save(order.orderId, order);
  }
};

示例 Order 视图使用 RedisInsight

提示

下载 RedisInsight 以查看您的 Redis 数据或在工作台中使用原始 Redis 命令。

订单历史记录 API#

以下代码显示了获取客户订单历史记录的示例 API 请求和响应。

订单历史记录请求#

docs/api/view-order-history.md
// GET https://api-gateway/orderHistory/viewOrderHistory

订单历史记录响应#

{
  "data": [
    {
      "orderId": "d4075f43-c262-4027-ad25-7b1bc8c490b6",
      "userId": "USR_22fcf2ee-465f-4341-89c2-c9d16b1f711b",
      "orderStatusCode": 4,
      "products": [
        {
          "productId": "11002",
          "qty": 1,
          "productPrice": 4950,
          "productData": {
            "productId": "11002",
            "price": 4950,
            "productDisplayName": "Puma Men Race Black Watch",
            "variantName": "Race 85",
            "brandName": "Puma",
            "ageGroup": "Adults-Men",
            "gender": "Men",
            "displayCategories": "Accessories",
            "masterCategory_typeName": "Accessories",
            "subCategory_typeName": "Watches",
            "styleImages_default_imageURL": "https://host.docker.internal:8080/images/11002.jpg",
            "productDescriptors_description_value": "<p>This watch from puma comes in a heavy duty design. The assymentric dial and chunky..."
          }
        },
        {
          "productId": "11012",
          "qty": 2,
          "productPrice": 1195,
          "productData": {
            "productId": "11012",
            "price": 1195,
            "productDisplayName": "Wrangler Women Frill Check Multi Tops",
            "variantName": "FRILL CHECK",
            "brandName": "Wrangler",
            "ageGroup": "Adults-Women",
            "gender": "Women",
            "displayCategories": "Sale and Clearance,Casual Wear",
            "masterCategory_typeName": "Apparel",
            "subCategory_typeName": "Topwear",
            "styleImages_default_imageURL": "https://host.docker.internal:8080/images/11012.jpg",
            "productDescriptors_description_value": "<p><strong>Composition</strong><br /> Navy blue, red, yellow and white checked top made of 100% cotton, with a jabot collar, buttoned ..."
          }
        }
      ],
      "createdBy": "USR_22fcf2ee-465f-4341-89c2-c9d16b1f711b",
      "lastUpdatedOn": "2023-07-13T14:11:49.997Z",
      "lastUpdatedBy": "USR_22fcf2ee-465f-4341-89c2-c9d16b1f711b"
    }
  ],
  "error": null
}

当您发出请求时,它会通过 API 网关发送到 order history service。最终,它会调用 viewOrderHistory 函数,该函数如下所示

server/src/services/order-history/src/service-impl.ts
const viewOrderHistory = async (userId: string) => {
  const repository = OrderRepo.getRepository();
  let orders: Partial<IOrder>[] = [];
  const queryBuilder = repository
    .search()
    .where('createdBy')
    .eq(userId)
    .and('orderStatusCode')
    .gte(ORDER_STATUS.CREATED) //returns CREATED and PAYMENT_SUCCESS
    .and('statusCode')
    .eq(DB_ROW_STATUS.ACTIVE);

  console.log(queryBuilder.query);
  orders = <Partial<IOrder>[]>await queryBuilder.return.all();
};
信息

请注意, order history service 只需要访问 Redis 来获取所有订单。这是因为我们处理了 Redis 和主数据库之间的存储和同步 orders service

您可能习惯于将 Redis 用作缓存,并且同时存储和检索字符串化的 JSON 值或哈希值。但是,仔细查看上面的代码。在其中,我们将订单存储为 JSON 文档,然后使用 Redis OM 来搜索属于特定用户的订单。Redis 在这里充当搜索引擎,能够加速查询并独立于主数据库(在本例中为 MongoDB/ Postgressql)进行扩展。

准备好使用 CQRS 模式使用 Redis 吗?#

希望本教程能帮助您了解如何使用 CQRS 模式使用 Redis。它可以帮助减轻主数据库的负载,同时仍然允许您存储和搜索 JSON 文档。有关此主题的更多资源,请查看以下链接

其他资源#