搭建一个简单的秒杀系统 —— Python篇

给大家的福利

概述:

redis是内存型数据库,读写速度远快于mysql这类磁盘型数据库,常用来作缓存。
rabbitmq消息队列,可以理解为生产者消费者模型,用队列来存储任务,生产与消费解耦。
前言
在介绍架构之前,我们需要先知道秒杀系统面临的难点是什么。

首先在普通的系统中,最大的瓶颈是在于底层的数据库端。因为底层数据库(比如常见的mysql)是磁盘存储的,所以读写IO较慢,而且连接数有限。

而在秒杀业务场景,最大的特点是瞬时的高并发,即在短时间内会有大量的请求到来。如果让所有请求都打到底层数据库上,很大可能数据库会直接崩掉,即使数据库能承受住大量的连接请求,但大量的请求读写都会导致大量的锁冲突,导致响应速度大大减慢。而响应速度对于用户体验来说,无疑是十分重要的。

所以在这里,需要明确第一个目标:让尽可能少、尽可能有效的请求打到底层数据库。

当我们回头再考虑这个业务场景,其实绝大部分的请求都不应该打到底层数据库。因为一般商品库存可能只有抢购用户数的百分之一,甚至更少。所以我们需要一些机制、策略,提前将无效的请求返回。

而站在整个网站设计的角度,第二个目标:越上层越容易实现,越有效。

这里的层指:

页面层
网络层
应用层
服务层
数据层
例如在前端页面层,如果不做处理,用户在点击抢购按钮以后,见网页没有响应,可能会再点击3-4次甚至更多,这样可能会导致最终有80%的请求都是重复无效的。但只需要前端在设计时,将点击后的按钮置灰,防止用户多次点击发送请求。即简单又有效。
以下简单指出各层可实施的策略:

页面层(简单的实现可以屏蔽 90%的请求)
按钮置灰,禁止用户重复提交
验证码
网络层
通过ip限制一定时间内的请求次数
应用层
动静分离,压缩缓存处理(CDN nginx)
一个页面最占用资源、带宽的是cs js 图片等静态资源
避免所有请求都到服务器的硬盘上取
根据uid限频,页面缓存技术(web服务器 nginx)
反向代理 + 负载均衡 (nginx)
服务层
微服务
redis
消息队列 削峰 异步处理
数据层
读写分离
分库分表
集群
每一层具体实现起来都是一个很大的架构,这里我们主要专注于服务层,使用redis+消息队列。

基础架构

img核心:服务异步拆分,减少耦合,使用缓存,加快响应。

避免同步的请求执行,如:请求→订单→支付→修改库存→结束返回,这种模型在高并发场景下,阻塞多,响应慢,服务器压力大,不可取。

这里实现的架构是: 1. 请求→返回 2. 支付→返回 3. 修改库存

这种服务拆分归功于 消息队列。核心思想是,将接收到的请求 存储到队列中就可以响应用户了,后端在队列中取出请求再做后续操作即可。简单理解就是,我们将请求记录下来,晚点空闲了再处理。

基础数据存储
数据、请求的存储情况如:

mysql中存储商品信息、订单信息
redis存入商品信息、设置计数器、存储成功订单的数据结构等
rabbitmq创建队列
订单队列(用户提交请求)
延迟队列(订单必须在15分钟内支付)
成交队列(订单支付成功,等待写入数据库)
流程
订单请求
redis计数器
假设我们只有100件商品库存,但可能会收到10万条抢购请求。也就是会有将近9.9万条无效的请求,所以我们要将这些请求阻隔。

最简单的方法,也是我们使用的方法:实现一个count变量,每个请求进入都加一,当count大于100时则直接返回失败即可。

这里我们使用redis也是因为内存读写速度要远大于类似mysql的磁盘读写。

def plus_counter(goods_id, storage=100):
count = redis_conn.incr(“counter:”+str(goods_id))
if count > storage:
return False
return True

最初版本在这里加了分布式锁,后续整理项目仔细思考,确定不需要加分布式锁。原因是:

原因是:

redis提供的incr命令是原子性的
redis是单线程模型
此处应用程序不需要获取数据,经过逻辑判断以后再写入数据库。仅是单一语句,获取结果。
如果我们的场景是:

now = redis.get(xxx)
if now==yyy:
new = 123
elif now ==www:
new = 456

redis.set(xxx, new)
那么由于在我们做逻辑判断是过程中,其他客户端可能会修改xxx的值,导致错误。在这种场景下,从get到set之间就需要加锁,保证这期间数据不会受其他客户端的影响。

但是由于我们计数器应用仅需要执行incr语句,获取返回值。而redis中incr指令是原子性的,且是单线程通过队列串行执行的,所以能保证incr在执行期间不会受到其他线程的影响。所以不需要加锁。

订单队列
异步拆分服务的关键。为了提高响应速度,我们只需要将请求订单任务保存下来(消息队列),就可以直接返回用户了。而不需要将请求转到后端做大量的判断、处理、数据库读写操作后才返回用户。所有可以大大的加快响应速度。后端可以随时从队列中取出请求再做各自处理,即使等抢购活动结束再进行底层数据库读写也没有问题。

所以核心思路就是把请求放入队列,然后直接返回用户即可。

计数器+1

flag = plus_counter(goods_id)

成功申请

if flag:
# 生成唯一的订单号
order_id = uuid.uuid1()
# 订单信息(也是请求任务信息)
order_info = {
“goods_id”: goods_id,
“user_id”: user_id,
“order_id”: str(order_id)
}
try:
# 进入订单队列
enter_order_queue(order_info)

    res["status"] = True
    res["msg"] = "抢购成功,请在15分钟之内付款!"
    res["order_id"] = str(order_id)
 
    return jsonify(res)
except Exception as e:
    print("log: ", e)
    res["status"] = False
    res["msg"] = "抢购出错,请重试." + str(e)
    return jsonify(res), 202

enter_order_queue是将订单请求(订单信息),也就是order_info发送到对应的队列。与之对应的消费者,只需要将该订单信息写入数据库对应的订单表即可。

注意:此时订单还没支付,所以数据库表中可以设置一个status字段,标识订单的状态。

唯一标识
不局限于uuid,可用毫秒时间戳之类的唯一标识。

可以看到上面代码中,我们利用uuid生成了一个唯一标识作为订单号,并且返回给用户。

主要的作用是:

标识订单。因为订单请求仅仅只是被我们入队列,消费者可能还没开始处理。(即订单可能还未被创建在数据库中)
返回给用户,可用于后续的支付操作。
当用户支付时需要校验用户与对应的单号是否正确,这里我们仍用redis,以提高查询速度。

所以在上面的基础上,我们需要加多一步,将订单信息写入redis。

order_info = {
“goods_id”: goods_id,
“user_id”: user_id,
“order_id”: str(order_id)
}
try:
# 在redis中创建这个订单
create_order(order_info)

enter_order_queue(order_info)
res["status"] = True
res["msg"] = "抢购成功,请在15分钟之内付款!"
res["order_id"] = str(order_id)
return jsonify(res)

订单的结构这里采用字典,提高检索效率。插入如:

redis_conn.hset(“order:”+str(goods_id), str(order_id), str(user_id))
超时队列
正如前面所见,我们提示用户在15分钟之内支付,符合日常业务场景。

在消息队列中有延迟队列的应用,符合我们的超时需求。所以我们同样用消息队列来实现这一业务需求。即我们在创建订单时,同样将订单信息传入队列中。

try:
# redis保存订单信息
create_order(order_info)
# 订单队列
enter_order_queue(order_info)
# 超时队列
enter_overtime_queue(order_info)
最终,当一个订单请求通过计数器后,需要经历的三个过程如上。无论是redis或是rabbitmq消息队列,都是内存操作,速度都是足够快的。不需要经过数据层即可响应用户。

至此,一个订单“创建”完成。

支付请求
订单请求完成后,用户会获得订单号。用户必须在15分钟内完成支付操作。在执行支付时需要考虑:

检查用户和对应的订单号是否正确
create_order(order_info) 时,我们已将订单信息写入redis。可从这里取得数据做校验
检查订单是否超时
如果我们设置的超时队列超过指定时间,则队列里的请求会被处理(消费)
我们只需要将超时的单号写入redis即可做校验
支付成功入成交队列
同理于订单队列。只需将成交的订单信息写入消息队列中,后续系统空闲时再写入数据库即可。
也是为了提高用户响应速度,用户不需要等待数据库io完成后才收到结果。
代码流程为:

order_staus = check_order(order_info) # 检查订单状态
if order_staus:
if order_staus == -1: # 人为设定 -1 表示超时
res[“msg”] = “订单已超时”
return jsonify(res), 202
else:
# 支付函数
pay()
# 直接写入队列和redis
enter_paid_queue(order_info)
paid_order(order_info)

    res["status"] = True
    res["msg"] = "支付成功!!!!"
    return jsonify(res)

但订单通过检查、并支付完成后。我们还需要将成交的订单写入redis,记录状态(用于其他判断)。再将订单请求写入队列即可返回。全程内存操作,速度快,带来了快响应。之后,我们可以等抢购活动结束后,系统比较空闲的时间将订单同步到底层数据库,同步数据。

总览
所以两个核心的操作是:

通过rabbitmq消息队列异步拆分服务,加快了响应的速度
通过redis内存读写,减少操作时间
再总结整个框架:

用户提交订单

通过redis计数器筛选

成功则返回标识,然后入订单队列 + 超时队列

标识与用户信息写入redis,用于后续验证支付
订单队列,mysql监听,写入mysql的订单历史表
超时订单队列有计时功能,一定时间内未支付,订单失效,抢购失败。写入redis(标志失败)
失败直接返回

订单服务结束

用户支付订单

验证订单以及检查是否已超时(是否已在redis相关结构内)

成功支付则入支付队列

mysql监听这个队列,执行库存同步操作。
写入redis
失败或超时直接返回

支付服务结束

img

网络安全零基础入门指南

大家如果对黑客技术感兴趣,这里我整合并且整理成了一份【282G】的网络安全/黑客技术从零基础入门到进阶资料包, 无偿分享!!!

零基础Python学习资源介绍

① 网络安全所有方向的学习路线图,清楚各个方向要学什么东西

拿下NISP证书之后,身为普通的你可以:

1、跨越90%企业的招聘硬门槛 2、 增加70%就业机会 3、 拿下奇安信、腾讯等全国TOP100大厂敲门砖 4、 体系化得到网络安全技术硬实力 5、IT大佬年薪可达30w+

路线对应学习视频

国内外网安书籍、文档

护网行动资料

其中关于HW护网行动,也准备了对应的资料,这些内容可相当于比赛的金手指!

网络安全面试题军

更多内容为防止和谐,可以扫描获取~

获取方式:
在这里插入图片描述

  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值