以下是克隆本教程中使用的应用源代码的命令
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 进行读取。Redis 的优点在于它对读写都很快,因此是缓存和 CQRS 的最佳选择。
在本教程中,我们不着重介绍服务之间的通信协调方式,例如如何处理新订单的支付。该过程使用 Redis Streams,并在我们的 服务间通信指南 中进行了概述。
当你的电商应用最终需要在全球范围内扩展时,Redis Cloud 提供 Active-Active 地域分布,实现本地延迟的读写,并提供 99.999% 的可用时间。
让我们来看一些示例代码,这些代码有助于将 Redis 和主数据库 (MongoDB/PostgreSQL) 与 CQRS 模式结合使用。
电商微服务应用包括一个前端,使用 Next.js 和 TailwindCSS 构建。应用后端使用 Node.js。数据使用 Prisma 存储在 Redis 和 MongoDB/PostgreSQL 中。下面是你将看到的电商应用前端截图
仪表盘
: 显示产品列表,并提供搜索功能购物车: 将产品添加到购物车,然后点击“立即购买”按钮结账
订单历史记录
: 下单后,顶部导航栏中的“订单”链接会显示订单状态和历史记录
以下是克隆本教程中使用的应用源代码的命令
git clone --branch v4.2.0 https://github.com/redis-developer/redis-microservices-ecommerce-solutions
让我们来看一下 order service
的示例代码,并查看 CreateOrder
命令(写入操作)。然后我们再看看 order history service
来查看 ViewOrderHistory
命令(读取操作)。
以下代码显示了创建订单的 API 请求和响应示例。
// POST http://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 中。我们使用 用于 Node.js 的 Redis OM 将订单实体存储在 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);
}
};
使用 RedisInsight 查看示例 订单
下载 RedisInsight 来查看你的 Redis 数据或在工作台中使用原始 Redis 命令。
以下代码显示了获取客户订单历史记录的 API 请求和响应示例。
// GET http://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": "http://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": "http://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 来获取所有订单。这是因为我们在 orders service
中处理了 Redis 和主数据库之间的存储和同步。
你可能习惯于将 Redis 用作缓存,同时存储和检索字符串化的 JSON 值或哈希值。但是,请仔细看上面的代码。在代码中,我们将订单存储为 JSON 文档,然后使用 Redis OM 搜索属于特定用户的订单。在这里,Redis 的工作方式就像一个搜索引擎,能够加快查询速度并独立于主数据库(在本例中是 MongoDB/PostgreSQL)进行扩展。
希望本教程能帮助你理解如何将 Redis 与 CQRS 模式结合使用。它可以帮助减轻主数据库的负载,同时仍然允许你存储和搜索 JSON 文档。有关此主题的更多资源,请查看下面的链接
使用 Redis 构建微服务
通用