很难想象没有购物车的在线商店。几乎所有在线商店都必须具备购物车功能才能将产品销售给客户。为了构建可扩展的电子商务平台,您需要一个强大的框架和一个简单的存储系统。有时,许多开发人员专注于改进电子商务平台的前端性能以纠正这些问题。然而,真正的瓶颈仍然是缓慢的后端加载时间。缓慢的后端加载时间会严重影响您的搜索引擎排名。一个好的经验法则是后端加载时间不应超过总加载时间的 20%。一个好的后端加载时间目标是 200 毫秒或更短。在本教程中,您将了解如何使用 Node.js、Vue.js、Express 和 Redis 构建购物车应用程序。
本教程将向您展示如何通过使用 Node.js 创建一个基本的电子商务购物车应用程序来利用 Redis 的强大功能。通常,购物车数据存储在客户端作为 cookie。cookie 是存储在 Web 用户浏览器目录或数据文件夹中的小型文本文件。这样做的优点是您不需要在数据库中存储此类临时数据。但是,这将要求您在每个 Web 请求中发送 cookie,如果 cookie 很大,这会减慢请求速度。将购物车数据存储在 Redis 中是一个好主意,因为您可以随时非常快地检索商品,并在需要时持久化此数据。
使用 Node.js 构建电子商务应用程序更有意义,因为它由于其异步特性(能够同时处理多个并发用户)而确保了前端和后端加载时间之间的平衡。Node.js 帮助开发人员最大限度地利用事件循环和回调进行 I/O 操作。Node.js 运行单线程、非阻塞、异步编程,非常节省内存。
为了创建一个购物车,我们需要一个简单的存储系统,我们可以在其中收集产品和购物车的总计。Node.js 为我们提供了 express-session 包,它是 ExpressJS 的中间件。我们将使用 express-session 中间件来管理 Node.js 中的会话。会话存储在 express 服务器本身中。
默认的服务器端会话存储 MemoryStore 并非专门为生产环境设计。它在大多数情况下会泄漏内存,无法扩展到单个进程之外,并且仅用于调试和开发。为了管理多个用户的多个会话,我们必须创建一个全局映射并将每个会话对象放入其中。NodeJs 中的全局变量会占用内存,并且在生产级项目中可能成为可怕的安全漏洞。这可以通过使用外部会话存储来解决。我们必须将每个会话存储在存储中,以便每个会话仅属于一个用户。一个流行的会话存储是使用 Redis 构建的。
我们将首先设置应用程序的后端。让我们为应用程序创建一个新目录并初始化一个新的 Node.js 应用程序。打开您的终端并输入以下内容
克隆存储库
$ git clone https://github.com/redis-developer/basic-redis-shopping-chart-nodejs
您可以使用以下 docker compose 文件来运行 Redis Stack 服务器
version: '3'
services:
redis:
image: redis/redis-stack:latest
container_name: redis.redisshoppingcart.docker
restart: unless-stopped
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD}
ports:
- 127.0.0.1:${REDIS_PORT}:6379
networks:
- global
networks:
global:
external: true
我假设您已在本地环境中安装并运行了 Docker 和 Docker Compose。执行以下 compose CLI 以启动 Redis 服务器
$ docker network create global
$ docker-compose up -d --build
The docker-compose ps
shows the list of running Redis services:
$ docker-compose ps
Name Command State Ports
redis.redisshoppingcart.docker docker-entrypoint.sh redis ... Up 127.0.0.1:55000->6379/tcp
Node.js 是一个运行时环境,允许软件开发人员使用 JavaScript 启动 Web 应用程序的前端和后端。为了节省您的时间,目录 /server/src 已经为您创建。在这里,我们将通过添加以下子目录来创建我们的模块 -
路由将支持的请求(以及请求 URL 中编码的任何信息)转发到相应的控制器函数,而控制器函数从模型获取请求的数据,创建显示数据的 HTML 页面,并将其返回给用户以在浏览器中查看。服务包含您的实际业务逻辑。中间件函数是访问请求对象 (req)、响应对象 (res) 和应用程序请求-响应周期中的下一个中间件函数的函数。
% tree
.
├── controllers
│ ├── Cart
│ │ ├── DeleteItemController.js
│ │ ├── EmptyController.js
│ │ ├── IndexController.js
│ │ └── UpdateController.js
│ └── Product
│ ├── IndexController.js
│ └── ResetController.js
├── index.js
├── middleware
│ └── checkSession.js
├── products.json
├── routes
│ ├── cart.js
│ ├── index.js
│ └── products.js
└── services
└── RedisClient.js
6 directories, 13 files
让我们首先通过以下显示的 index.js 初始化应用程序服务器
// server/src/index.js
const express = require('express');
const redis = require('redis');
const rejson = require('redis-rejson');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const path = require('path');
const bodyParser = require('body-parser');
const cors = require('cors');
const RedisClient = require('./services/RedisClient');
rejson(redis);
require('dotenv').config();
const { REDIS_ENDPOINT_URI, REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, PORT } = process.env;
const app = express();
app.use(
cors({
origin(origin, callback) {
callback(null, true);
},
credentials: true
})
);
const redisEndpointUri = REDIS_ENDPOINT_URI
? REDIS_ENDPOINT_URI.replace(/^(redis\:\/\/)/, '')
: `${REDIS_HOST}:${REDIS_PORT}`;
const redisClient = redis.createClient(`redis://${redisEndpointUri}`, {
password: REDIS_PASSWORD
});
const redisClientService = new RedisClient(redisClient);
app.set('redisClientService', redisClientService);
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: 'someSecret',
resave: false,
saveUninitialized: false,
rolling: true,
cookie: {
maxAge: 3600 * 1000 * 3
}
})
);
app.use(bodyParser.json());
app.use('/', express.static(path.join(__dirname, '../../client-dist')));
const router = require('./routes')(app);
app.use('/api', router);
const port = PORT || 3000;
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
您会看到此 index.js 的职责只是设置服务器。它初始化所有中间件,设置视图引擎等。最后要做的是设置路由,将此责任委托给路由文件夹中的 index.js。
如上所示,app.use、app.set 和 app.listen 是端点,为了演示的目的,我们需要能够从篮子中添加和获取商品(保持简单)。我们需要定义我们的基本路由来获取所有产品、获取单个产品详细信息、删除产品和创建产品。
路由目录只负责定义我们的路由。在此文件夹中的 index.js 内,您会发现它的职责是设置我们的顶层路由并将它们的职责委托给它们各自的路由文件。每个相应的路由文件将进一步定义每个路由的任何其他子路由和控制器操作。
Web 服务器骨架已经有一个 ./routes 文件夹,其中包含索引、产品和购物车的路由。(如以下所示 https://github.com/redis-developer/basic-redis-shopping-chart-nodejs/tree/main/server/src/routes)
// routes/index.js
const fs = require('fs');
const express = require('express');
const router = express.Router();
module.exports = app => {
fs.readdirSync(__dirname).forEach(function (route) {
route = route.split('.')[0];
if (route === 'index') {
return;
}
router.use(`/${route}`, require(`./${route}`)(app));
});
return router;
};
路由是 Express 代码的一部分,它将 HTTP 动词(GET、POST、PUT、DELETE 等)、URL 路径/模式和处理该模式的函数相关联。有多种创建路由的方法。对于这个演示应用程序,我们将使用 express.Router 中间件,因为它允许我们将特定站点部分的路由处理程序分组在一起,并使用公共路由前缀访问它们。该模块需要 Express,然后使用它来创建一个 Router 对象。所有路由都在路由器上设置,然后导出。路由是使用路由器对象上的 .get() 或 .post() 方法定义的。所有路径都使用字符串定义(我们不使用字符串模式或正则表达式)。作用于某个特定资源(例如书籍)的路由使用路径参数从 URL 获取对象 ID。所有处理程序函数都从我们在上一节中创建的控制器模块导入。
控制器负责调用适当的操作。如果控制器的责任是渲染视图,它将从 app/views 目录渲染适当的视图。
// controller/Product/IndexController.js
const { products } = require('../../products.json');
class ProductIndexController {
constructor(redisClientService) {
this.redisClientService = redisClientService;
}
async index(req, res) {
const productKeys = await this.redisClientService.scan('product:*');
const productList = [];
if (productKeys.length) {
for (const key of productKeys) {
const product = await this.redisClientService.jsonGet(key);
productList.push(JSON.parse(product));
}
return res.send(productList);
}
for (const product of products) {
const { id } = product;
await this.redisClientService.jsonSet(`product:${id}`, '.', JSON.stringify(product));
productList.push(product);
}
return res.send(productList);
}
}
module.exports = ProductIndexController;
服务包含您的实际业务逻辑。服务层执行应用程序逻辑并将 CRUD 操作委托给数据库/持久存储(在本例中为 Redis)。让我们看看每种情况,并尝试了解数据是如何存储、修改和访问的
产品数据存储在外部 JSON 文件中。在第一次请求之后,此数据将以 JSON 数据类型保存在 Redis 中,例如
JSON.SET product:{productId} . '{ "id": "productId", "name": "Product Name", "price": "375.00", "stock": 10 }'.
购物车数据存储在哈希中,例如
HSET cart:{cartId} product:{productId} {productQuantity},
其中 cartId 是一个随机生成的的值,并存储在用户会话中。请注意,Redis 的哈希管理命令 HSET 存储 2 个键 - cart 和 product - 如下例所示。
HSET cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 1
产品数据修改方式如下
JSON.SET product:{productId} . '{ "id": "productId", "name": "Product Name", "price": "375.00", "stock": {newStock} }'.
JSON.SET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 . '{ "id": "e182115a-63d2-42ce-8fe0-5f696ecdfba6", "name": "Brilliant Watch", "price": "250.00", "stock": 1 }'
购物车数据修改方式如下
HSET cart:{cartId} product:{productId} {newProductQuantity} or HINCRBY cart:{cartId} product:{productId} {incrementBy}.
HSET cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 2
HINCRBY cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 1
HINCRBY cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 -1
产品可以从购物车中移除,如下所示
HDEL cart:{cartId} product:{productId}
HDEL cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6
购物车可以使用以下方式清除
HGETALL cart:{cartId} and then HDEL cart:{cartId} {productKey} in loop.
HGETALL cart:77f7fc881edc2f558e683a230eac217d => product:e182115a-63d2-42ce-8fe0-5f696ecdfba6, product:f9a6d214-1c38-47ab-a61c-c99a59438b12, product:1f1321bb-0542-45d0-9601-2a3d007d5842 => HDEL cart:77f7fc881edc2f558e683a230eac217d product:e182115a-63d2-42ce-8fe0-5f696ecdfba6, HDEL cart:77f7fc881edc2f558e683a230eac217d product:f9a6d214-1c38-47ab-a61c-c99a59438b12, HDEL cart:77f7fc881edc2f558e683a230eac217d product:1f1321bb-0542-45d0-9601-2a3d007d5842
请求重置数据时,可以删除所有购物车,如下所示
SCAN {cursor} MATCH cart:* and then DEL cart:{cartId} in loop.
SCAN {cursor} MATCH cart:* => cart:77f7fc881edc2f558e683a230eac217d, cart:217dedc2f558e683a230eac77f7fc881, cart:1ede77f558683a230eac7fc88217dc2f => DEL cart:77f7fc881edc2f558e683a230eac217d, DEL cart:217dedc2f558e683a230eac77f7fc881, DEL cart:1ede77f558683a230eac7fc88217dc2f
产品:SCAN {cursor} MATCH product:* 获取所有产品键,然后 JSON.GET {productKey}
SCAN {cursor} MATCH product:* => product:e182115a-63d2-42ce-8fe0-5f696ecdfba6, product:f9a6d214-1c38-47ab-a61c-c99a59438b12, product:1f1321bb-0542-45d0-9601-2a3d007d5842
=> JSON.GET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6, JSON.GET product:f9a6d214-1c38-47ab-a61c-c99a59438b1, JSON.GET product:1f1321bb-0542-45d0-9601-2a3d007d5842
购物车:HGETALL cart:{cartId} 获取产品的数量,然后 JSON.GET product:{productId} 在循环中获取产品数据。
HGETALL cart:77f7fc881edc2f558e683a230eac217d => product:e182115a-63d2-42ce-8fe0-5f696ecdfba6 (quantity: 1), product:f9a6d214-1c38-47ab-a61c-c99a59438b12 (quantity: 0), product:1f1321bb-0542-45d0-9601-2a3d007d5842 (quantity: 2) => JSON.GET product:e182115a-63d2-42ce-8fe0-5f696ecdfba6, JSON.GET product:f9a6d214-1c38-47ab-a61c-c99a59438b12, JSON.GET product:1f1321bb-0542-45d0-9601-2a3d007d5842
HGETALL 从哈希数据类型返回键和相应值的数组。使用您最喜欢的编辑器打开 RedisClient.js 文件,如下所示
// services/RedisClient.js
const { promisify } = require('util');
class RedisClient {
constructor(redisClient) {
['json_get', 'json_set', 'hgetall', 'hset', 'hget', 'hdel', 'hincrby', 'del', 'scan'].forEach(
method => (redisClient[method] = promisify(redisClient[method]))
);
this.redis = redisClient;
}
async scan(pattern) {
let matchingKeysCount = 0;
let keys = [];
const recursiveScan = async (cursor = '0') => {
const [newCursor, matchingKeys] = await this.redis.scan(cursor, 'MATCH', pattern);
cursor = newCursor;
matchingKeysCount += matchingKeys.length;
keys = keys.concat(matchingKeys);
if (cursor === '0') {
return keys;
} else {
return await recursiveScan(cursor);
}
};
return await recursiveScan();
}
jsonGet(key) {
return this.redis.json_get(key);
}
jsonSet(key, path, json) {
return this.redis.json_set(key, path, json);
}
hgetall(key) {
return this.redis.hgetall(key);
}
hset(hash, key, value) {
return this.redis.hset(hash, key, value);
}
hget(hash, key) {
return this.redis.hget(hash, key);
}
hdel(hash, key) {
return this.redis.hdel(hash, key);
}
hincrby(hash, key, incr) {
return this.redis.hincrby(hash, key, incr);
}
del(key) {
return this.redis.del(key);
}
}
module.exports = RedisClient;
流程相当简单。当请求被发送到此购物车应用程序上的端点时,例如 https://localhost:8081/. 它首先命中该端点的路由器,然后如果它是一个公共端点,例如此端点,它将进入处理该端点的控制器。作为类比,控制器就像一个经理,而服务是工人。控制器管理传入的工作 HTTP 请求,而服务接收它需要从经理那里获得的请求数据以执行其任务。
接下来,我们在名为 cart.js 的模块中创建了购物车路由。代码首先导入 Express 应用程序对象,使用它来获取 Router 对象,然后使用 get() 方法向它添加几个路由。最后,该模块返回 Router 对象。
首先,让我们将产品模型定义到我们的 controllers/Product/IndexController.js 文件中(https://github.com/redis-developer/basic-redis-shopping-chart-nodejs/tree/main/server/src/controllers/Product)。
我们的产品模型将尽可能简单,因为它包含产品名称、价格和图片。
{
"products": [
{
"id": "e182115a-63d2-42ce-8fe0-5f696ecdfba6",
"name": "Brilliant Watch",
"price": "250.00",
"stock": 2
},
{
"id": "f9a6d214-1c38-47ab-a61c-c99a59438b12",
"name": "Old fashion cellphone",
"price": "24.00",
"stock": 2
},
{
"id": "1f1321bb-0542-45d0-9601-2a3d007d5842",
"name": "Modern iPhone",
"price": "1000.00",
"stock": 2
},
{
"id": "f5384efc-eadb-4d7b-a131-36516269c218",
"name": "Beautiful Sunglasses",
"price": "12.00",
"stock": 2
},
{
"id": "6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345",
"name": "Stylish Cup",
"price": "8.00",
"stock": 2
},
{
"id": "efe0c7a3-9835-4dfb-87e1-575b7d06701a",
"name": "Herb caps",
"price": "12.00",
"stock": 2
},
{
"id": "x341115a-63d2-42ce-8fe0-5f696ecdfca6",
"name": "Audiophile Headphones",
"price": "550.00",
"stock": 2
},
{
"id": "42860491-9f15-43d4-adeb-0db2cc99174a",
"name": "Digital Camera",
"price": "225.00",
"stock": 2
},
{
"id": "63a3c635-4505-4588-8457-ed04fbb76511",
"name": "Empty Bluray Disc",
"price": "5.00",
"stock": 2
},
{
"id": "97a19842-db31-4537-9241-5053d7c96239",
"name": "256BG Pendrive",
"price": "60.00",
"stock": 2
}
]
}
复制 .env.example
到 .env
文件,并设置环境变量,如下所示:
REDIS_PORT=6379
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=demo
COMPOSE_PROJECT_NAME=redis-shopping-cart
如果您使用的是 Redis Cloud 而不是 localhost,则需要在 REDIS_HOST 下输入数据库端点(不带端口),而其他条目,如 REDIS_PORT 和 REDIS_PASSWORD 非常明显。
$ npm install
添加完这些内容后,您可以在终端中键入 npm install 来运行应用程序。运行此命令后,它将返回 Application is running on 3000。
$ npm run dev
$ npm run dev
> redis-shopping-cart-backend@1.0.0 dev
> nodemon src/index.js
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node src/index.js`
App listening on port 3000
现在我们已经运行了应用程序的后端,让我们开始开发它的前端。我们将利用 Vue.js - 一个强大的但简单的 JavaScript 框架来构建我们的前端 Web 客户端。它具有所有现代框架中最低的进入门槛之一,同时提供了高性能 Web 应用程序所需的所有功能。
.
├── README.md
├── babel.config.js
├── node_modules
├── package-lock.json
├── package.json
├── public
├── src
└── vue.config.js
根级别的文件(babel.config.js、package.json、node_modules)用于配置项目。最有趣的部分(至少现在)位于 src 目录(目录结构如下所示)。
main.js 文件是应用程序的主要 JavaScript 文件,它将加载所有公共元素并调用 App.vue 主屏幕。App.vue 是一个文件,其中包含特定页面或模板的 HTML、CSS 和 JavaScript。作为应用程序的入口点,这部分默认情况下由所有屏幕共享,因此它是编写此文件中的通知客户端部分的好地方。public/index.html 是静态的入口点,DOM 将从中加载。
% tree
.
├── App.vue
├── assets
│ ├── RedisLabs_Illustration.svg
│ └── products
│ ├── 1f1321bb-0542-45d0-9601-2a3d007d5842.jpg
│ ├── 42860491-9f15-43d4-adeb-0db2cc99174a.jpg
│ ├── 63a3c635-4505-4588-8457-ed04fbb76511.jpg
│ ├── 6d6ca89d-fbc2-4fc2-93d0-6ee46ae97345.jpg
│ ├── 97a19842-db31-4537-9241-5053d7c96239.jpg
│ ├── e182115a-63d2-42ce-8fe0-5f696ecdfba6.jpg
│ ├── efe0c7a3-9835-4dfb-87e1-575b7d06701a.jpg
│ ├── f5384efc-eadb-4d7b-a131-36516269c218.jpg
│ ├── f9a6d214-1c38-47ab-a61c-c99a59438b12.jpg
│ └── x341115a-63d2-42ce-8fe0-5f696ecdfca6.jpg
├── components
│ ├── Cart.vue
│ ├── CartItem.vue
│ ├── CartList.vue
│ ├── Info.vue
│ ├── Product.vue
│ ├── ProductList.vue
│ └── ResetDataBtn.vue
├── config
│ └── index.js
├── main.js
├── plugins
│ ├── axios.js
│ └── vuetify.js
├── store
│ ├── index.js
│ └── modules
│ ├── cart.js
│ └── products.js
└── styles
└── styles.scss
8 directories, 27 files
在 client 目录下,在 src 子目录下,打开 App.vue 文件。您将看到以下内容
<template>
<v-app>
<v-container>
<div class="my-8 d-flex align-center">
<div class="pa-4 rounded-lg red darken-1">
<v-icon color="white" size="45">mdi-cart-plus</v-icon>
</div>
<h1 class="ml-6 font-weight-regular">Shopping Cart demo</h1>
</div>
</v-container>
<v-container>
<v-row>
<v-col cols="12" sm="7" md="8">
<info />
<product-list :products="products" />
</v-col>
<v-col cols="12" sm="5" md="4" class="d-flex flex-column">
<cart />
<reset-data-btn class="mt-6" />
</v-col>
</v-row>
<v-footer class="mt-12 pa-0">
© Copyright 2021 | All Rights Reserved Redis
</v-footer>
</v-container>
</v-app>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import Cart from '@/components/Cart';
import ProductList from '@/components/ProductList';
import ResetDataBtn from '@/components/ResetDataBtn.vue';
import Info from '@/components/Info';
export default {
name: 'App',
components: {
ProductList,
Cart,
ResetDataBtn,
Info
},
computed: {
...mapGetters({
products: 'products/getProducts'
})
},
async created() {
await this.fetchProducts();
},
methods: {
...mapActions({
fetchProducts: 'products/fetch'
})
}
};
</script>
这是客户端代码。这里 API 返回(除其他外)适合在 Maps 上使用的图标的链接。如果您遵循流程,您将看到地图标记正在直接使用包含的 URL 加载这些图标。
$ cd client
$ npm run serve
> redis-shopping-cart-client@1.0.0 serve
> vue-cli-service serve
INFO Starting development server...
98% after emitting CopyPlugin
DONE Compiled successfully in 7733ms 7:15:56 AM
App running at:
- Local: https://localhost:8081/
- Network: https://192.168.43.81:8081/
Note that the development build is not optimized.
To create a production build, run npm run build.
让我们点击第一个项目“256GB Pendrive”,并尝试查看此产品。将其添加到购物车后,您将使用 redis-cli monitor 命令看到以下输出。
1613320256.801562 [0 172.22.0.1:64420] "json.get" "product:97a19842-db31-4537-9241-5053d7c96239"
1613320256.803062 [0 172.22.0.1:64420] "hget"
...
1613320256.805950 [0 172.22.0.1:64420] "json.set" "product:97a19842-db31-4537-9241-5053d7c96239" "." "{\"id\":\"97a19842-db31-4537-9241-5053d7c96239\",\"name\":\"256BG Pendrive\",\"price\":\"60.00\",\"stock\":1}"
1613320256.807792 [0 172.22.0.1:64420] "set" "sess:Ii9njXZd6zeUViL3tKJimN5zU7Samfze"
...
1613320256.823055 [0 172.22.0.1:64420] "scan" "0" "MATCH" "product:*"
...
1613320263.232527 [0 172.22.0.1:64420] "hgetall" "cart:bdee1606395f69985e8f8e01d3ada8c4"
1613320263.233752 [0 172.22.0.1:64420] "set" "sess:gXk5K9bobvrR790-HFEoi3bQ2kP9YmjV" "{\"cookie\":{\"originalMaxAge\":10800000,\"expires\":\"2021-02-14T19:31:03.233Z\",\"httpOnly\":true,\"path\":\"/\"},\"cartId\":\"bdee1606395f69985e8f8e01d3ada8c4\"}" "EX" "10800"
1613320263.240797 [0 172.22.0.1:64420] "scan" "0" "MATCH" "product:*"
1613320263.241908 [0 172.22.0.1:64420] "scan" "22" "MATCH" "product:*"
…
"{\"cookie\":{\"originalMaxAge\":10800000,\"expires\":\"2021-02-14T19:31:03.254Z\",\"httpOnly\":true,\"path\":\"/\"},\"cartId\":\"4bc231293c5345370f8fab83aff52cf3\"}" "EX" "10800"
将购物车数据存储在 Redis 中是一个好主意,因为它允许您随时快速检索数据并在需要时持久化此数据。与在会话中存储整个购物车数据的 Cookie 相比,Cookie 膨胀且操作速度相对较慢,在 Redis 中存储购物车数据可以加快购物车读写性能,从而改善用户体验。