(转)开源项目miaosha(上)

时间:2021-11-02 05:13:02

石墨文档:https://shimo.im/docs/iTDoZs4CVfICgSfV/

(二期)19、开源秒杀项目miaosha解读(上)

【课程19】几张图.xmind0.6MB

【课程19】开源秒...解读.xmind0.1MB

【课程19】秒杀项...要点.xmind68.6KB

【课程19】秒杀项...方向.xmind74.7KB

【课程19预习】秒...解读.xmind0.5MB

【课程19】redis的运用.xmind47.8KB

什么是秒杀

秒杀场景一般会在电商网站举行一些活动或者节假日在12306网站上抢票时遇到。对于电商网站中一些稀缺或者特价商品,电商网站一般会在约定时间点对其进行限量销售,因为这些商品的特殊性,会吸引大量用户前来抢购,并且会在约定的时间点同时在秒杀页面进行抢购。

秒杀系统场景特点

瞬时并发量大

  • 秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。

库存量少

  • 秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。

业务简单

  • 秒杀业务流程比较简单,一般就是下订单减库存。
秒杀项目设计要点
设计理念

限流: 鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。

削峰:对于秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值。高峰值流量是压垮系统很重要的原因,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路。实现削峰的常用的方法有利用缓存和消息中间件等技术。

异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。

内存缓存:秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。

可拓展:当然如果我们想支持更多用户,更大的并发,最好就将系统设计成弹性可拓展的,如果流量来了,拓展机器就好了。像淘宝、京东等双十一活动时会增加大量机器应对交易高峰。

设计难点要点
  • 对现有业务冲击
  • 高并发应用负载高
  • 突然增加网络与服务宽带
  • 直接下单
  • 控制商品页面购买按钮点亮
  • 下单前置检查
优化方向
  • 前端:

页面静态化将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素。通过CDN来抗峰值。

禁止重复提交用户提交之后按钮置灰,禁止重复提交。

  • 服务端:

(1)将请求尽量拦截在系统上游(不要让锁冲突落到数据库上去)。传统秒杀系统之所以挂,请求都压倒了后端数据层,数据读写锁冲突严重,并发高响应慢,几乎所有请求都超时,流量虽大,下单成功的有效流量甚小。以12306为例,一趟火车其实只有2000张票,200w个人来买,基本没有人能买成功,请求有效率为0。

(2)充分利用缓存,秒杀买票,这是一个典型的读多写少的应用场景,大部分请求是车次查询,票查询,下单和支付才是写请求。一趟火车其实只有2000张票,200w个人来买,最多2000个人下单成功,其他人都是查询库存,写比例只有0.1%,读比例占99.9%,非常适合使用缓存来优化。

(3)消息队列:消息队列可以削峰,将拦截大量并发请求,这也是一个异步处理过程,后台业务根据自己的处理能力,从消息队列中主动的拉取请求消息进行业务处理。

(4)用户限流:在某一时间段内只允许用户提交一次请求,比如可以采取IP限流。

  • 数据库层:

数据库层是最脆弱的一层,一般在应用设计时在上游就需要把请求拦截掉,数据库层只承担“能力范围内”的访问请求。所以,上面通过在服务层引入队列和缓存,让最底层的数据库高枕无忧。

(转)开源项目miaosha(上)

(秒杀项目参考逻辑)

 
项目介绍

。本项目是使用Redis作为缓存的一个秒杀项目实例。

各个方案
1. 直接更新数据库:

磁盘IO,开发机器实测2280 OPS,速度太低,当出现海量请求时会导致大量请求线程被阻塞,拒绝后续请求,拖垮整个tomcat和DB。

2. redis+消息队列+更新数据库(秒杀和下单操作分离)
  • a、用户请求过来,将请求入消息队列;
  • b、消息处理,先减redis库存量,如果减库存成功,则生成下单token存入redis(设定有效期,比如2分钟之内下单有效),等待用户下单(这样就避免下单也面对大量并发);如果减库存失败,则消息记录回到消息队列中,等待再次处理;
  • c、用户下单:判断token是否失效(比对时间)了,如果未失效则扣减库存(也可能扣减库存失败),生成订单;如果已经失效了,则redis库存增加1; 如何确保下单token过期了释放资格?JOB 每分钟扫token缓存,如果失效了的则清除调,并回馈redis缓存(redis库存+1);
  • d、前端用户如何获知抢购成功了(获得了下单资格):ajax轮训查询接口。 说明:为什么要采用轮询而不是用实时的websocket推送?经测试,一台tomcat最多能连接3000个websocket,如果类似抢购的大量用户抢购,机器肯定是扛不住这么多长连接的,而查询用户是否抢购成功也只是查询的redis,因此采用轮询是很好的选择。
  • e、为什么要秒杀和下单操作分离?
  • 一方面,秒杀接口可以阻挡大部分并发流程,从而让下单操作错开并发高峰;
  • 另一方面,可以让秒杀操作和下单操作从业务上相分离,使得秒杀操作可以独立于订单相关业务。
3. 防刷过滤器+redis+消息队列+更新数据库

针对第2方案中可能出现被辅助软件而已刷单的现象,可以增加过滤器:如果用户在指定时间内请求多少次,则认为是恶意用户,可以直接将该用户加入黑名单,并在后续的消息队列处理中不给黑名单的用户分配资格。

技术运用
  • 前端
  • ajax
  • countdown
  • 后端
  • redis作为缓存、消息队列
逻辑处理

(转)开源项目miaosha(上)

(页面逻辑)

(转)开源项目miaosha(上)

(后台接口逻辑)

(转)开源项目miaosha(上)

(消息队列异步处理流程图)

(转)开源项目miaosha(上)

(总流程时序图)

秒杀设计时序图.puml3.5KB

  1. 秒杀详情页,等待秒杀开始
  1. 秒杀开始,获取秒杀链接
  1. 进入秒杀,限流,判断是否重复秒杀,把请求消息推入redis消息处理队列
  1. 消息处理器监听到消息后开始处理:
  1. 监测黑名单
  1. 判断秒杀是否已结束
  1. 先减redis库存(占位)
  1. 生成下单token,存入redis供前端查询
  1. 前端查询秒杀结果
  1. 成功:显示下单按钮,用户使用token去下单
  1. 用户下单
  1. 检查token有效性,检查库存
  1. 减库存,下单。(真正减库存)

拦截器:

  • 恶意IP检测拦截器
  • 获取真实ip
  • 匹配ip是否是正常ip,是否在黑名单库
  • 使用redis增加ip的访问次数
  • 超过限定次数就加入到黑名单库
  • 恶意用户检测拦截器
  • 同ip监测拦截
数据预处理

商品库存信息预先生成,服务器启动时加载。

请求过滤

入口只有活动开启前才能获得

入口恶意用户检测:多秒内多少次请求---可以记录最近10次请求时间,和前第九次请求时间对比

实时限流器
  • 实时限流:限制正在处理的请求量(通过消息队列获取正在处理的请求数目)为库存的100倍请求(这个可自定义);
  • 如果出现了限流器满了,但仍然有库存的情况怎么办?直接拒绝请求,允许用户重新提交请求 ##请求减库存
  • 请求通过了过滤之后,交给消息队列减库存+下单 ##消息队列处理
  • 消息队列再次过滤请求是否是恶意的用户
  • 否则,执行减库存+下单
限流处理

限制用户维度访问频率

需要考虑的恶意行为
  • ip黑名单
  • 用户黑名单
代码结构

(转)开源项目miaosha(上)

redis的运用

Redis是一个分布式缓存系统,支持多种数据结构,我们可以利用Redis轻松实现一个强大的秒杀系统。作为缓存和消息队列使用

分析

Redis incr 可以实现原子性的递增,可应用于高并发的秒杀活动、分布式序列号生成等场景。

Redis丰富的数据结构适用于多种场景,如消息队列。

redis消息队列

message-trunk是以redis为基础搭建的轻量级高性能消息总线(队列),和主流MQ相比使用起来更灵巧简便。

git地址:https://gitee.com/1028125449/message-trunk

使用方法:

  • 获取消息队列全局对象MessageTrunk(可以用spring注入),put入消息即可。
// 获取MessageTrunk实例
MessageTrunk mt = (MessageTrunk) SpringBeanUtils.getBean("messageTrunk");

Message<DemoMessage> message = new Message<DemoMessage>(MessageType.DEMO_MESSAGE, new DemoMessage(value));

// 消息入MT
mt.put(message);

  • 消息处理器:继承AbstarctMessageHandler抽象类。
public class DemoHandler extends AbstarctMessageHandler<DemoMessage>{
 
 private static Log logger = LogFactory.getLog(DemoHandler.class);

 public DemoHandler(){
  // 说明该handler监控的消息类型
  super(MessageType.DEMO_MESSAGE);
 }

 /**
  * 监听到消息后处理方法
  */
 @Override
 public void handle(DemoMessage message){
        // do handle
 }

 @Override
 public void handleFailed(DemoMessage obj){
  // handle failed
 }
}
数据结构
  • 哈希
  • 存储黑名单(ip,用户)
  • 秒杀处理请求列表
  • 字符串
  • 增减库存
  • 商品描述是否结束标志
  • 下单token
  • ...
  • 列表
  • 用户访问记录
MySQL的存储过程

我们常用的操作数据库语言 SQL 语句在执行的时候需要要先编译,然后执行,而存储过程( Stored Procedure )是一组为了完成特定功能的 SQL 语句集,经编译后存储在数据库中,用户通过指定存储过程的名字并给定参数(如果该存储过程带有参数)来调用执行它。

FOUND_ROWS : 获取上一个select语句查询到的行数;
ROW_COUNT : 获取上一条update, insert ,delete 影响的行数;

MySQL存储过程创建的格式:CREATE PROCEDURE 过程名 ([过程参数 [,...]])

[特性 ...] 过程体

mysql> DELIMITER //

mysql> CREATE PROCEDURE proc1(OUT s int)

-> BEGIN

-> SELECT COUNT(*) INTO s FROM user;

-> END

-> //

mysql> DELIMITER ;

用call和你过程名以及一个括号,括号里面根据需要,加入参数,参数包括输入参数、输出参数、输入输出参数。

参数:

IN 输入参数:表示该参数的值必须在调用存储过程时指定,在存储过程中修改该参数的值不能被返回,为默认值

OUT 输出参数:该值可在存储过程内部被改变,并可返回

INOUT 输入输出参数:调用时指定,并且可被改变和返回

存储过程通常有以下优点:

(1).存储过程增强了SQL语言的功能和灵活性。存储过程可以用流控制语句编写,有很强的灵活性,可以完成复杂的判断和较复杂的运算。

(2).存储过程允许标准组件是编程。存储过程被创建后,可以在程序中被多次调用,而不必重新编写该存储过程的SQL语句。而且数据库专业人员可以随时对存储过程进行修改,对应用程序源代码毫无影响。

(3).存储过程能实现较快的执行速度。如果某一操作包含大量的Transaction-SQL代码或分别被多次执行,那么存储过程要比批处理的执行速度快很多。因为存储过程是预编译的。在首次运行一个存储过程时查询,优化器对其进行分析优化,并且给出最终被存储在系统表中的执行计划。而批处理的Transaction-SQL语句在每次运行时都要进行编译和优化,速度相对要慢一些。

(4).存储过程能过减少网络流量。针对同一个数据库对象的操作(如查询、修改),如果这一操作所涉及的Transaction-SQL语句被组织程存储过程,那么当在客户计算机上调用该存储过程时,网络中传送的只是该调用语句,从而大大增加了网络流量并降低了网络负载。

(5).存储过程可被作为一种安全机制来充分利用。系统管理员通过执行某一存储过程的权限进行限制,能够实现对相应的数据的访问权限的限制,避免了非授权用户对数据的访问,保证了数据的安全。