Redis应用实战 - 秒杀场景(Node.js版本)
有志者,事竟成!如果你在学习数据库,那么本文《Redis应用实战 - 秒杀场景(Node.js版本)》,就很适合你!文章讲解的知识点主要包括MySQL、分布式、Redis、Node.js、秒杀,若是你对本文感兴趣,或者是想搞懂其中某个知识点,就请你继续往下看吧~
写在前面
公司随着业务量的增加,最近用时几个月时间在项目中全面接入
CREATE TABLE `seckill_goods` ( `id` INTEGER NOT NULL auto_increment, `fk_good_id` INTEGER, `amount` INTEGER, `start_time` DATETIME, `end_time` DATETIME, `is_valid` TINYINT ( 1 ), `comment` VARCHAR ( 255 ), `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, PRIMARY KEY ( `id` ) ) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `orders` ( `id` INTEGER NOT NULL auto_increment, `order_no` VARCHAR ( 255 ), `good_id` INTEGER, `user_id` INTEGER, `status` ENUM ( '-1', '0', '1', '2' ), `order_type` ENUM ( '1', '2' ), `scekill_id` INTEGER, `comment` VARCHAR ( 255 ), `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, PRIMARY KEY ( `id` ) ) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
CREATE TABLE `goods` ( `id` INTEGER NOT NULL auto_increment, `name` VARCHAR ( 255 ), `thumbnail` VARCHAR ( 255 ), `price` INTEGER, `status` TINYINT ( 1 ), `stock` INTEGER, `stock_left` INTEGER, `description` VARCHAR ( 255 ), `comment` VARCHAR ( 255 ), `created_at` DATETIME NOT NULL, `updated_at` DATETIME NOT NULL, PRIMARY KEY ( `id` ) ) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
产品表在此次业务中不是重点,以下逻辑都以
INSERT INTO `redis_app`.`seckill_goods` ( `id`, `fk_good_id`, `amount`, `start_time`, `end_time`, `is_valid`, `comment`, `created_at`, `updated_at` ) VALUES ( 1, 1, 200, '2020-06-20 00:00:00', '2023-06-20 00:00:00', 1, '...', '2020-06-20 00:00:00', '2021-06-22 10:18:16' );
秒杀接口开发
首先,说一下
// 引入moment库处理时间相关数据 const moment = require('moment'); // 引入数据库model文件 const seckillModel = require('../../dbs/mysql/models/seckill_goods'); const ordersModel = require('../../dbs/mysql/models/orders'); // 引入工具函数或工具类 const UserModule = require('../modules/user'); const { random_String } = require('../../utils/tools/funcs'); class Seckill { /** * 秒杀接口 * * @method post * @param good_id 产品id * @param accessToken 用户Token * @param path 秒杀完成后跳转路径 */ async doSeckill(ctx, next) { const body = ctx.request.body; const accessToken = ctx.query.accessToken; const path = body.path; // 基本参数校验 if (!accessToken || !path) { return ctx.throwException(20001, '参数错误!'); }; // 判断此产品是否加入了抢购 const seckill = await seckillModel.findOne({ where: { fk_good_id: ctx.params.good_id, } }); if (!seckill) { return ctx.throwException(30002, '该产品并未有抢购活动!'); }; // 判断是否有效 if (!seckill.is_valid) { return ctx.throwException(30003, '该活动已结束!'); }; // 判单是否开始、结束 if(moment().isBefore(moment(seckill.start_time))) { return ctx.throwException(30004, '该抢购活动还未开始!'); } if(moment().isAfter(moment(seckill.end_time))) { return ctx.throwException(30005, '该抢购活动已经结束!'); } // 判断是否卖完 if(seckill.amount
至此,秒杀接口用传统的关系型数据库就实现完成了,代码并不复杂,注释也很详细,不用特别的讲解大家也都能看懂,那它能不能正常工作呢,答案显然是否定的
通过
{ amount: 200, start_time: '2020-06-20 00:00:00', end_time: '2023-06-20 00:00:00', is_valid: 1, comment: '...', }
其次,创建
if (redis.call('hexists', KEYS[1], KEYS[2]) == 1) then local stock = tonumber(redis.call('hget', KEYS[1], KEYS[2])); if (stock > 0) then redis.call('hincrby', KEYS[1], KEYS[2], -1); return stock end; return 0 end;
最后,完成代码,完整代码如下:
// 引入相关库 const moment = require('moment'); const Op = require('sequelize').Op; const { v4: uuidv4 } = require('uuid'); // 引入数据库model文件 const seckillModel = require('../../dbs/mysql/models/seckill_goods'); const ordersModel = require('../../dbs/mysql/models/orders'); // 引入Redis实例 const redis = require('../../dbs/redis'); // 引入工具函数或工具类 const UserModule = require('../modules/user'); const { randomString, checkObjNull } = require('../../utils/tools/funcs'); // 引入秒杀key前缀 const { SECKILL_GOOD, LOCK_KEY } = require('../../utils/constants/redis-prefixs'); // 引入避免超卖lua脚本 const { stock, lock, unlock } = require('../../utils/scripts'); class Seckill { async doSeckill(ctx, next) { const body = ctx.request.body; const goodId = ctx.params.good_id; const accessToken = ctx.query.accessToken; const path = body.path; // 基本参数校验 if (!accessToken || !path) { return ctx.throwException(20001, '参数错误!'); }; // 判断此产品是否加入了抢购 const key = `${SECKILL_GOOD}${goodId}`; const seckill = await redis.hgetall(key); if (!checkObjNull(seckill)) { return ctx.throwException(30002, '该产品并未有抢购活动!'); }; // 判断是否有效 if (!seckill.is_valid) { return ctx.throwException(30003, '该活动已结束!'); }; // 判单是否开始、结束 if(moment().isBefore(moment(seckill.start_time))) { return ctx.throwException(30004, '该抢购活动还未开始!'); } if(moment().isAfter(moment(seckill.end_time))) { return ctx.throwException(30005, '该抢购活动已经结束!'); } // 判断是否卖完 if(seckill.amount
这里代码主要做个四个修改:
- 步骤2,判断产品是否加入了抢购,改为去
Redis
中查询 - 步骤7,判断登录用户是否已抢到,因为不在维护抢购活动
id
,所以改为使用用户id
、产品id
和状态status
判断 - 步骤8,扣库存,改为使用
lua
脚本去Redis
中扣库存 - 对扣库存和写入数据库操作进行加锁
订单的操作仍然在
Mysql数据库中进行,因为大部分的请求都在步骤5被拦截了,剩余请求
Mysql是完全有能力处理的。
再次通过
Jmeter进行测试,发现订单表正常,库存量扣减正常,说明超卖问题和限购已经解决。
其他问题
秒杀场景的其他技术
基于Redis
支持高并发、键值对型数据库和支持原子操作等特点,案例中使用Redis
来作为秒杀应对方案。在更复杂的秒杀场景下,除了使用Redis
外,在必要的的情况下还需要用到其他一些技术:- 限流,用漏斗算法、令牌桶算法等进行限流
- 缓存,把热点数据缓存到内存里,尽可能缓解数据库访问的压力
- 削峰,使用消息队列和缓存技术使瞬间高流量转变成一段时间的平稳流量,比如客户抢购成功后,立即返回响应,然后通过消息队列异步处理后续步骤,发短信,写日志,更新一致性低的数据库等等
- 异步,假设商家创建一个只针对粉丝的秒杀活动,如果商家的粉丝比较少(假设小于1000),那么秒杀活动直接推送给所有粉丝,如果用户粉丝比较多,程序立刻推送给排名前1000的用户,其余用户采用消息队列延迟推送。(1000这个数字需要根据具体情况决定,比如粉丝数2000以内的商家占99%,只有1%的用户粉丝超过2000,那么这个值就应该设置为2000)
- 分流,单台服务器不行就上集群,通过负载均衡共同去处理请求,分散压力
这些技术的应用会让整个秒杀系统更加完善,但是核心技术还是
Redis
,可以说用好Redis
实现的秒杀系统就足以应对大部分场景。Redis
健壮性
案例使用的是单机版Redis
,单节点在生产环境基本上不会使用,因为- 不能达到高可用
- 即便有着
AOF
日志和RDB
快照的解决方案以保证数据不丢失,但都只能放在master
上,一旦机器故障,服务就无法运行,而且即便采取了相应措施仍不可避免的会造成数据丢失。
因此,
Redis
的主从机制和集群机制在生产环境下是必须的。Redis
分布式锁的问题- 单点分布式锁,案例提到的分布式锁,实际上更准确的说法是单点分布式锁,是为了方便演示,但是,单点
Redis
分布式锁是肯定不能用在生产环境的,理由跟第2点类似 - 以主从机制(多机器)为基础的分布式锁,也是不够的,因为
redis
在进行主从复制时是异步完成的,比如在clientA
获取锁后,主redis
复制数据到从redis
过程中崩溃了,导致锁没有复制到从redis
中,然后从redis
选举出一个升级为主redis
,造成新的主redis
没有clientA
设置的锁,这时clientB
尝试获取锁,并且能够成功获取锁,导致互斥失效。
针对以上问题,
redis
官方设计了Redlock
,在Node.js
环境下对应的资源库为node-redlock
,可以用npm
安装,至少需要3个独立的服务器或集群才能使用,提供了非常高的容错率,在生产环境中应该优先采用此方案部署。- 单点分布式锁,案例提到的分布式锁,实际上更准确的说法是单点分布式锁,是为了方便演示,但是,单点
总结
秒杀场景的特点可以总结为瞬时并发访问、读多写少、限时和限量,开发中还要考虑避免超卖现象以及类似黄牛抢票的限购问题,针对以上特点和问题,分析得到开发的原则是:数据写入内存而不是写入硬盘,异步处理而不是同步处理,扣库存操作原子执行以及对单用户购买进行加锁,而
Redis正好是符合以上全部特点的工具,因此最终选择
Redis来解决问题。
秒杀场景是一个在电商业务中相对复杂的场景,此篇文章只是介绍了其中最核心的逻辑,实际业务可能更加复杂,但只需要在此核心基础上进行扩展和优化即可。
秒杀场景的解决方案不仅仅适合秒杀,类似的还有抢红包、抢优惠券以及抢票等等,思路都是一致的。
解决方案的思路还可以应用在单独限购、第二件半价以及控制库存等等诸多场景,大家要灵活运用。
项目地址
https://github.com/threerocks/redis-seckill
参考资料
https://time.geekbang.org/column/article/307421
https://redis.io/topics/distlock
以上就是《Redis应用实战 - 秒杀场景(Node.js版本)》的详细内容,更多关于mysql的资料请关注golang学习网公众号!

- 上一篇
- 【熬夜整理近百份大厂面经】2022校招提前批面经总结分享(腾讯、字节、阿里、百度、京东等招聘信息+必考点+简历书写)

- 下一篇
- 视频号直播自定义组件接入电商系统
-
- 洁净的小伙
- 很好,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,帮助很大,总算是懂了,感谢博主分享技术文章!
- 2023-01-16 06:49:57
-
- 爱笑的朋友
- 太细致了,已加入收藏夹了,感谢博主的这篇技术文章,我会继续支持!
- 2023-01-15 06:07:32
-
- 大意的钻石
- 这篇文章内容真及时,楼主加油!
- 2023-01-14 14:34:47
-
- 无辜的香水
- 这篇博文真是及时雨啊,细节满满,很好,已加入收藏夹了,关注up主了!希望up主能多写数据库相关的文章。
- 2023-01-14 04:26:38
-
- 数据库 · MySQL | 14小时前 | 索引 数据类型 字符集 存储引擎 CREATETABLE
- MySQL新建表操作指南与建表技巧
- 462浏览 收藏
-
- 数据库 · MySQL | 1个月前 | 条件判断
- CASEWHEN条件判断的嵌套使用详解与实战场景分析
- 469浏览 收藏
-
- 数据库 · MySQL | 1个月前 | java php
- CSV文件批量导入MySQL的性能优化秘籍大揭秘
- 289浏览 收藏
-
- 数据库 · MySQL | 1个月前 |
- GaleraCluster多主集群配置与冲突解决攻略
- 239浏览 收藏
-
- 数据库 · MySQL | 1个月前 | 窗口函数实战
- MySQL窗口函数实战案例深度剖析
- 315浏览 收藏
-
- 数据库 · MySQL | 1个月前 | 自定义函数
- MySQL插件开发入门:自定义函数(UDF)编写指南
- 184浏览 收藏
-
- 数据库 · MySQL | 1个月前 |
- Windows系统MySQL8.0免安装版配置攻略
- 227浏览 收藏
-
- 数据库 · MySQL | 1个月前 | MySQL错误 数据库诊断
- 深度解析错误代码1045/1217/1205的根本原因及解决方案
- 202浏览 收藏
-
- 数据库 · MySQL | 1个月前 | sql注入 编码规范
- 防范SQL注入必备:编码规范与工具推荐指南
- 140浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 508次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 484次学习
-
- 笔灵AI生成答辩PPT
- 探索笔灵AI生成答辩PPT的强大功能,快速制作高质量答辩PPT。精准内容提取、多样模板匹配、数据可视化、配套自述稿生成,让您的学术和职场展示更加专业与高效。
- 14次使用
-
- 知网AIGC检测服务系统
- 知网AIGC检测服务系统,专注于检测学术文本中的疑似AI生成内容。依托知网海量高质量文献资源,结合先进的“知识增强AIGC检测技术”,系统能够从语言模式和语义逻辑两方面精准识别AI生成内容,适用于学术研究、教育和企业领域,确保文本的真实性和原创性。
- 22次使用
-
- AIGC检测-Aibiye
- AIbiye官网推出的AIGC检测服务,专注于检测ChatGPT、Gemini、Claude等AIGC工具生成的文本,帮助用户确保论文的原创性和学术规范。支持txt和doc(x)格式,检测范围为论文正文,提供高准确性和便捷的用户体验。
- 30次使用
-
- 易笔AI论文
- 易笔AI论文平台提供自动写作、格式校对、查重检测等功能,支持多种学术领域的论文生成。价格优惠,界面友好,操作简便,适用于学术研究者、学生及论文辅导机构。
- 40次使用
-
- 笔启AI论文写作平台
- 笔启AI论文写作平台提供多类型论文生成服务,支持多语言写作,满足学术研究者、学生和职场人士的需求。平台采用AI 4.0版本,确保论文质量和原创性,并提供查重保障和隐私保护。
- 35次使用
-
- golang MySQL实现对数据库表存储获取操作示例
- 2022-12-22 499浏览
-
- Go语言实战之实现一个简单分布式系统
- 2022-12-23 220浏览
-
- 分享Redis高可用架构设计实践
- 2023-01-24 286浏览
-
- 搞一个自娱自乐的博客(二) 架构搭建
- 2023-02-16 244浏览
-
- B-Tree、B+Tree以及B-link Tree
- 2023-01-19 235浏览