马上双十一,教你用 Python 实现秒杀系统
以下文章来源于简说Python ,作者SMEB
Python小白首选,输入优惠码“python”立减10元
作者 | SMEB
来源 | 简说Python(ID:xksnh888)
责编 | 刘静
Redis是内存型数据库,读写速度远快于MySQL这类磁盘型数据库,常用来作缓存。 RabbitMQ消息队列,可以理解为生产者消费者模型,用队列来存储任务,生产与消费解耦。
前言
页面层; 网络层; 应用层; 服务层; 数据层。
以下简单指出各层可实施的策略:
页面层(简单的实现可以屏蔽 90%的请求)
按钮置灰,禁止用户重复提交 验证码
通过ip限制一定时间内的请求次数
一个页面最占用资源、带宽的是cs js 图片等静态资源 避免所有请求都到服务器的硬盘上取 动静分离,压缩缓存处理(CDN nginx) 根据uid限频,页面缓存技术(web服务器 nginx) 反向代理 + 负载均衡 (nginx)
微服务 redis 消息队列 削峰 异步处理
读写分离 分库分表 集群
基础架构
基础数据存储
mysql中存储商品信息、订单信息 redis存入商品信息、设置计数器、存储成功订单的数据结构等 rabbitmq创建队列
订单队列(用户提交请求) 延迟队列(订单必须在15分钟内支付) 成交队列(订单支付成功,等待写入数据库)
流程
以下所有代码都是截取核心部分,完整代码参看
github:https://github.com/Sssmeb/seckilling
订单请求
redis计数器
def plus_counter(goods_id, storage=100):
count = redis_conn.incr("counter:"+str(goods_id))
if count > storage:
return False
return True
代码实现增加了分布式锁。相关知识可以看:https://www.jianshu.com/p/cf311cfb1689
订单队列
# 计数器+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发送到对应的队列。与之对应的消费者,只需要将该订单信息写入数据库对应的订单表即可。
唯一标识
不局限于uuid,可用毫秒时间戳之类的唯一标识。
标识订单。因为订单请求仅仅只是被我们入队列,消费者可能还没开始处理。(即订单可能还未被创建在数据库中) 返回给用户,可用于后续的支付操作。
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))
超时队列
try:
create_order(order_info)
enter_order_queue(order_info)
enter_overtime_queue(order_info)
最终,当一个订单请求通过计数器后,需要经历的三个过程如上。无论是redis或是rabbitmq消息队列,都是内存操作,速度都是足够快的。不需要经过数据层即可响应用户。
支付请求
检查用户和对应的订单号是否正确
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 失败或超时直接返回 支付服务结束
注意
代码持续更新,完整代码:https://github.com/Sssmeb/seckilling (觉得有帮助的可以给个star) 本架构只专注于服务层的业务架构,有很多没有涉及的点(高可用,数据一致性等),一个完整的抢购系统是一个非常庞大的。 文中没有介绍mysql数据层相关的操作,一方面是为了提示大家,在高并发的情景下应该尽可能的避免这类的磁盘io操作。另一方面,mysql数据层相关操作是在消息队列 消费者进行操作的,这里不详解操作。只注重整体架构。具体操作见代码。
☞90后技术宅研发Magi一夜爆红,新一代知识化结构搜索新时代来了?
☞英特尔推出世界最大 FPGA 芯片;任正非表示华为尚未直接和美国公司商谈5G技术授权;OpenTitan开源……☞世界上最大儿童色情暗网网站被查封!美国司法部通过追踪比特币交易揪出幕后罪犯