很难想象一个没有购物车的在线商店。几乎每个在线商店都必须具备购物车功能,才能向客户销售产品。为了构建可扩展的电子商务平台,你需要一个强大的框架和一个简单的存储系统。有时,许多开发者专注于改进电子商务平台的前端性能来解决这些问题。然而,真正的瓶颈仍然是后端加载时间慢。缓慢的后端加载时间会对你的搜索引擎排名产生严重影响。一个好的经验法则是,后端加载时间不应超过总加载时间的 20%。后端加载时间的良好目标是 200 毫秒或更短。在本教程中,你将看到如何使用 Node.js、Vue.js、Express 和 Redis 构建一个购物车应用。
本教程将向你展示如何通过使用 Node.js 创建一个基本的电子商务购物车应用来利用 Redis 的强大功能。通常,购物车数据以 cookie 的形式存储在客户端。Cookie 是存储在网络用户的浏览器目录或数据文件夹中的小型文本文件。这样做的好处是你无需将这些临时数据存储在数据库中。然而,这需要你在每次 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
命令 docker-compose ps
显示正在运行的 Redis 服务列表:
$ 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 的职责仅仅是设置服务器。它初始化所有中间件,设置视图引擎等。最后要做的是通过将路由设置的职责委托给 routes 文件夹内的 index.js 来设置路由。
如上所示,app.use、app.set 和 app.listen 是端点,出于本次演示的目的,我们需要能够向购物车添加和获取商品(保持简单)。我们需要定义获取所有商品、获取单个商品详情、移除商品和创建商品的基本路由。
routes 目录只负责定义我们的路由。在该文件夹中的 index.js 文件中,你会看到它的职责是设置我们的顶级路由,并将它们的职责委托给各自的路由文件。每个相应的路由文件将进一步定义各自的附加子路由和控制器动作。
Web 服务器骨架已经有一个 ./routes 文件夹,其中包含 index、products 和 cart 的路由。(如 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 存储了两个键 - 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;
流程非常简单。一旦请求发送到此购物车应用的某个端点,例如 http://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
> [email protected] 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 在这里返回,其中包括适用于地图使用的图标链接。如果您按流程操作,您会看到地图标记直接使用包含的 URL 加载这些图标。
$ cd client
$ npm run serve
> [email protected] 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: http://localhost:8081/
- Network: http://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 相比,将购物车数据存储在 Redis 中可以加快购物车的读写性能,从而改善用户体验。