如果今天有上万人在同一时间抢限量商品
,昨天分享的方案基本撑不住。
不过面对这个情境,Redis 表示终於轮到我了!今天这篇文章会以 Node.js + Redis
为范例,带读者一起解决这个问题。
如何解决高并发情境的商品秒杀问题
回答问题所需具备的知识
衍伸问题
这算是资料库的常见面试题
,除了金融、电商
喜欢考这题外;社交平台、新创公司、游戏产业
也会换个形式考类似的题目。
因为这算是职缺所需具备的技术
,如果求职者没有相关经验可能就会止步於这个关卡。
如果有去以上产业的打算,最好有一定的 NoSQL(ex:Redis、MongoDB) 基础再去面试;因为在面试官的眼中
有基础跟完全不会的差距很大
,求职者可以没有实务经验,但明知产业会碰到这些技术还不去学习就是态度问题了。
通常这类型的秒杀活动都有固定档期,我会先将活动用的商品及库存数量同步到 Redis 资料库
;为了避免超卖,在 Client 端下单时会执行 Lua 脚本
,当秒杀到指定数量或是时间结束後就不再接受请求,活动结束後将 Redis 的资料同步到关联式资料库
。
如果有短时间大量访问、需要性能提升
的需求,往往会先想到 Redis 这个记忆体资料库。
Redis 为什麽快?
多执行绪
会耗费时间在上下文的切换
以及加锁
上面;而单执行绪
会依照请求顺序执行
,不需考虑同步以及加锁带来的效能问题。开多个 Redis 建立 Cluster
分散压力。epoll IO 多路复用
,可以用一条执行绪处理并发的网路请求。
这个有点抽象,我用
缴交作业
为情境说明:
假设你是一个老师,采用多路复用的原则批改作业,那你批改的顺序就是看谁先缴交作业
,而不是按照学号顺序批改(此作法中间有人没交作业就会卡住);这样就能避免大量无用操作,为非阻塞模式的实现。
Redis 资料保存方案
可以使用 RDB 、AOF
来做持久化。
定期操作
,如果 Redis 当机会遗失部分资料
,此方案适合大规模资料恢复
。完整纪录
所有资料的变化,因为采用日志追加的方式,所以就算当机也不会影响已经储存的日志,灾难复原的完成度高
;缺点是档案比 RDB 大
、大规模资料恢复速度较 RDB 慢
。Redis 资料淘汰机制
Redis 主要保存的都是热点资讯
,在储存资料有限的状态下(记忆体不足,无法写入新资料);就要合理的设定淘汰机制:
过期时间
的资料
原则上淘汰策略以「
volatile-lru、allkeys-lru
」为主(淘汰较少使用的资料)。
目标
使用技术
如果完全没有相关基础,建议先参考这篇文章来安装 Redis 以及它的 GUI 工具。
在 Redis Server 执行 EVAL 指令时,在结果回传前只会执行当下 Lua 脚本的逻辑
,其他 Client 端的命令须等待直到 EVAL 执行完为止。
Lua 脚本的逻辑应尽量简单以保证执行效率,否则会影响 Client 端的体验。
程序架构
主程序:redisSecKill.js
yarn add ioredis
)。prepare
函式,以 Hash type 建立参加秒杀的产品库存。secKill
函式模拟使用者购买行为,缓存并执行 Lua 脚本。const fs = require("fs");
const Redis = require("ioredis");
const redis = new Redis({
host: "127.0.0.1",
port: 6379,
password: "",
});
redis.on("error", function (error) {
console.error(error);
});
async function prepare(item_name) {
// 参加秒杀活动的商品库存
await redis.hmset(item_name, "Total", 100, "Booked", 0);
}
const secKillScript = fs.readFileSync("./secKill.lua");
async function secKill(item_name, user_name) {
// 1. 缓存脚本取得 sha1 值
const sha1 = await redis.script("load", secKillScript);
// console.log(sha1);
// 2. 透过 evalsha 执行脚本
// redis Evalsha 命令基本语法如下
// EVALSHA sha1 numkeys key [key ...] arg [arg ...]
redis.evalsha(sha1, 1, item_name, 1, "order_list", user_name);
}
function main() {
console.time("secKill");
const item_name = "item_name";
prepare(item_name);
for (var i = 1; i < 10000; i++) {
const user_name = "baobao" + i;
secKill(item_name, user_name);
}
console.timeEnd("secKill");
}
main();
Lua 脚本:secKill.lua
在 Lua 脚本执行逻辑:「确认下单数量」➜「取得商品库存」➜「如果库存足够就下单」➜「储存购买者资讯(List type)」。
local item_name = KEYS[1]
local n = tonumber(ARGV[1])
local order_list = ARGV[2]
local user_name = ARGV[3]
if not n or n == 0 then
return 0
end
local vals = redis.call("HMGET", item_name, "Total", "Booked");
local total = tonumber(vals[1])
local booked = tonumber(vals[2])
if not total or not booked then
return 0
end
if booked + n <= total then
redis.call("HINCRBY", item_name, "Booked", n)
redis.call("LPUSH", order_list, user_name)
return n
end
return 0
node redisSecKill.js
模拟秒杀补充
文章程序只是 MVP,现实状况还有很多要设计的:
- 哪些商品被列为秒杀,如何纪录。
- 设定秒杀开始、结束时间。
- 如何将 Redis 资料存入关联式资料库中。
考点:对 Redis Zset 这个资料型态的认知与应用
我会使用 Redis 这个记忆体资料库,建立一个有序集合(Zset)
来储存用户的资讯。
在设计上,用户(member)是唯一值,且每个用户都会关联一个点数(score),这样用户就可以按照分数来排序。
功能实现上会透过 zrevrange Leaderboard 0 99 withscores
这段指令来显示 TOP100 的点数排行榜。
- zrevrange:依照点数(score)检视排行榜
- Leaderboard:排行榜名称(可以自行定义)
- 0 99:TOP100 的意思
- withscores:连点数(score)一起显示
考点:是否了解过同类型的技术以及差异
简单的资料型态
,而 Redis 支援多种资料型态
(String、Hash、List、Set、Zset、Stream)。不支援资料持久化
,而 Redis 提供 RDB 跟 AOF 两种方案
。没有主从式架构
,而 Redis 支援主从式架构与读写分离
。同样身为记忆体资料库,Memcached 提供简单的使用方式,而 Redis 提供丰富的功能。
感谢大家的阅读,如果喜欢我的文章可以订阅
接收通知;如果有帮助到你,按Like
可以让我更有写文的动力,我们明天见~
我在 Medium 平台 也分享了许多技术文章
❝ 主题涵盖「MIS & DEVOPS、资料库、前端、後端、MICROSFT 365、GOOGLE 云端应用、自我修炼」希望可以帮助遇到相同问题、想自我成长的人。❞
除了有BETWEEN AND 之外,还有NOT BETWEEN AND (NOT BETWEEN A...
Socket.io 注意server side需要使用3.0.3版本 否则flutter clien...
前言 September LeetCoding Challenge 2021今天的题目是1137. ...
引言 昨天的题目学习到进位制以及「 ASCII code <-> 字元」转换, 关於 ...
好的,在我们结束Spring Boot API的架设後,再来我们要开始进入前端框架-Angular的...