以下是克隆本教程中使用的应用程序的源代码的命令
git clone --branch v4.2.0 https://github.com/redis-developer/redis-microservices-ecommerce-solutions
命令查询责任分离 (CQRS) 是微服务架构中的一个关键模式。它将读取(查询)和写入(命令)解耦,从而允许读取和写入工作负载独立工作。
命令(写入)侧重于更高的持久性和一致性,而查询(读取)侧重于性能。这使微服务能够将数据写入较慢的记录系统磁盘数据库,同时在缓存中预取和缓存这些数据以进行实时读取。
这个想法很简单:将命令(例如“订购此产品”(写入操作))与查询(例如“显示我的订单历史记录”(读取操作))分开。CQRS 应用程序通常基于消息,并依赖于 最终一致性.
以下示例数据架构演示了如何将 Redis 与 CQRS 一起使用
图中说明的架构使用更改数据捕获模式(称为“集成 CDC”)来跟踪命令数据库上的更改状态,并将更改状态复制到查询数据库(Redis)。这是 CQRS 中使用的常见模式。
实现 CDC 需要
虽然可以使用触发器和函数使用 Redis 实现自己的 CDC 机制,但 Redis Cloud 带有自己的集成 CDC 机制,可以为您解决此问题。
为了提高应用程序性能,分别扩展您的读取和写入操作。
考虑以下场景:您有一个电子商务应用程序,允许客户使用产品填充购物车。该网站有一个“立即购买”按钮,以便于订购这些产品。从一开始,您可能会设置并填充一个产品数据库(可能是 SQL 数据库)。然后,您可能会编写一个后端 API 来处理创建订单、创建发票、处理付款、处理履行和更新客户订单历史记录的过程……一次性完成所有操作。
这种同步订单处理方法似乎是个好主意。但是,您很快就会发现,随着客户数量的增加和销售量的增长,数据库的速度会变慢。实际上,大多数应用程序的读取次数远远超过写入次数。您应该分别扩展这些操作。
您决定需要快速处理订单,以便客户不必等待。然后,有时间的时候,您可以创建发票、处理付款、处理履行等。
因此,您决定将这些步骤分开。使用 CQRS 的微服务方法允许您分别扩展您的读取和写入,并有助于解耦您的微服务。在 CQRS 模型中,单个服务负责端到端地处理整个命令。一个服务不应依赖于另一个服务才能完成命令。
让我们看一下演示应用程序的架构
products service
: 处理从数据库查询产品并将其返回到前端orders service
: 处理验证和创建订单order history service
: 处理查询客户的订单历史记录payments service
: 处理订单付款api gateway
: 将服务统一到一个端点下mongodb/ postgresql
: 充当用于存储订单、订单历史记录、产品等的写入优化数据库您不需要在演示应用程序中使用 MongoDB/ Postgresql 作为写入优化数据库;您也可以使用其他 prisma 支持的数据库 。这只是一个例子。
请注意,在当前架构中,所有服务都使用相同的底层数据库。即使您在技术上将读取和写入分开,也无法独立扩展写入优化数据库。这就是 Redis 的用武之地。如果您将 Redis 放在写入优化数据库之前,您可以将其用于读取,同时写入写入优化数据库。Redis 的好处是它对读取和写入速度很快,这就是它成为缓存和 CQRS 的最佳选择的原因。
出于本教程的目的,我们没有重点介绍如何在服务之间协调通信,例如如何处理新的订单以进行付款。该过程使用 Redis Streams,并在我们的 服务间通信指南中概述。
当您的电子商务应用程序最终需要在全球范围内扩展时,Redis Cloud 为读取和写入提供主动-主动地理分布,以实现本地延迟以及 99.999% 的正常运行时间。
让我们看一下一些示例代码,这些代码有助于使用 Redis 和主数据库 (MongoDB/ Postgressql) 促进 CQRS 模式。
该电子商务微服务应用程序包含一个前端,使用 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
让我们看一下 order service 的示例代码,并查看 CreateOrder 命令(写入操作)。然后,我们看一下 order history service 以查看 ViewOrderHistory 命令(读取操作)。
以下代码显示了创建订单的示例 API 请求和响应。
// 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 函数,该函数如下所示
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 中。该函数如下所示
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 请求和响应。
// 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
函数,该函数如下所示
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。它可以帮助减轻主数据库的负载,同时仍然允许您存储和搜索 JSON 文档。有关此主题的更多资源,请查看以下链接