(17)SpringBoot 2.X 高并发秒杀系统总结

(17)SpringBoot 2.X 高并发秒杀系统总结

1.系统介绍

  • 本系统是使用SpringBoot开发的高并发限时抢购秒杀系统,除了实现基本的登录、查看商品列表、秒杀、下单等功能,项目中还针对高并发情况实现了系统缓存、降级和限流。

2.开发环境和工具

  • Aliyun CentOS7.3
  • IntelliJ IDEA + Navicat + Git + Chrome
  • 压测工具Apache Jmeter

3.开发技术

前端技术BootStrapJQueryThymeleaf
后端技术SpringBootMyBatisMySQL
中间件技术DruidRedisRabbitMQ

4.秒杀优化方向

  • 将请求尽量拦截在系统上游:传统秒杀系统之所以挂,请求都压倒了后端数据层,数据读写锁冲突严重,几乎所有请求都超时,流量虽大,下单成功的有效流量甚小,我们可以通过限流、降级等措施来最大化减少对数据库的访问,从而保护系统。
  • 充分利用缓存:秒杀商品是一个典型的读多写少的应用场景,充分利用缓存将大大提高并发量

5.项目亮点

5.1 使用分布式Seesion,可以实现让多台服务器同时可以响应。
5.2 使用redis做缓存提高访问速度和并发量,减少数据库压力,利用内存标记减少redis的访问
  • 在秒杀活动中,当队列写入消息达到某一数值时,不再写入消息队列,而直接跳转到活动结束的页面
5.3 使用页面静态化,加快用户访问速度,提高QPS,缓存页面至浏览器,前后端分离降低服务器压力
5.4 使用消息队列完成异步下单,提升用户体验,削峰和降流
  • 冗余(存储):在某些情况下处理数据的过程中会失败,消息中间件允许把数据持久化知道他们完全被处理
  • 削峰:在访问量剧增的情况下,但是应用仍然需要发挥作用,但是这样的突发流量并不常见。而使用消息中间件采用队列的形式可以减少突发访问压力,不会因为突发的超时负荷要求而崩溃;消息队列是基于队列的,在秒杀活动中,当队列写入消息达到某一数值时,不再写入消息队列,而直接跳转到活动结束的页面
  • 顺序保证:在大多数场景下,处理数据的顺序也很重要,大部分消息中间件支持一定的顺序性
  • 缓冲:消息中间件通过一个缓冲层来帮助任务最高效率的执行
  • 异步通信:通过把把消息发送给消息中间件,消息中间件并不立即处理它,后续在慢慢处理
5.5 安全性优化:双重md5密码校验
5.6 秒杀接口地址的隐藏
5.7 数学公式验证码
5.8 接口限流防刷
5.9 优雅的代码编写
  • 接口的输出结果做了一个Result封装
  • 对错误的代码做了一个CodeMsg的封装
  • 访问缓存做了一个key的封装

6 实现技术点

6.1 两次md5密码校验
  • 用户端:inputPassToFormPass = MD5(明文+固定salt)
  • 服务端:formPassToDBPass = MD5(inputPassToFormPass+ 随机salt)
  • 好处:
    • 第一次作用:防止用户明文密码在网络进行传输
    • 第二次作用:防止数据库被盗,避免通过MD5反推出密码,双重保险
6.2 分布式session
  • 验证用户账号密码都正确情况下,通过UUID生成唯一id作为token,再将token作为key、用户信息作为value模拟session存储到redis,同时将token存储到cookie,保存登录状态,每次需要session,从缓存中取即可
  • 好处: 在分布式集群情况下,服务器间需要同步,定时同步各个服务器的session信息,会因为延迟到导致session不一致,使用redis把session数据集中存储起来,解决session不一致问题
6.3 JSR303参数检验
  • 使用JSR303自定义校验器,实现对用户账号、密码的验证,使得验证逻辑从业务代码中脱离出来
6.4 全局异常处理
  • 优点1: 可以实现对项目中所有产生的异常进行拦截,在同一个类中实现统一处理。避免异常漏处理的情况。
  • 优点2: 当Service 出现业务逻辑错误的时候,这个时候我们可以直接抛出异常,让拦截器来捕捉,捕捉之后,就不需要冗余的代码来return 一个不符合业务逻辑的返回值来作为输出。
  • 优点3: 当参数校验不通过的时候,输出也是Result(CodeMsg),传给前端用于前端显示获取处理
6.5 页面缓存 + 对象缓存,Redis缓解数据库压力
  • 本项目大量的利用了缓存技术,包括用户信息缓存(分布式session),商品信息的缓存,商品库存缓存,订单的缓存,页面缓存,对象缓存减少了对数据库服务器的访问
  • 页面缓存:通过在手动渲染得到的html页面缓存到redis
  • 对象缓存:包括对用户信息、商品信息、订单信息和token等数据进行缓存,利用缓存来减少对数据库的访问,大大加快查询速度。
6.6 页面静态化,前后端分离
  • 页面静态化的主要目的是为了加快页面的加载速度,将商品详情和订单详情页面做成静态HTML(纯的HTML),数据的加载只需要通过ajax来请求服务器,实现前后端分离,静态页面无需连接数据库打开速度较动态页面会有明显提高,并且做了静态化HTML页面可以缓存在客户端的浏览器。
6.7 通用缓存Key封装
  • 大量的缓存引用也出现了一个问题,如何识别不同模块中的缓存(key值重复,如何辨别是不同模块的key)
  • 解决:利用一个抽象类,定义BaseKey(前缀),在里面定义缓存key的前缀以及缓存的过期时间从而实现将缓存的key进行封装。让不同模块继承它,这样每次存入一个模块的缓存的时候,加上这个缓存特定的前缀,以及可以统一制定不同的过期时间
6.8 本地内存标记 + redis预处理 + RabbitMQ异步下单 + 客户端轮询
  • 描述:通过三级缓冲保护,1、本地标记 2、redis预处理 3、RabbitMQ异步下单,最后才会访问数据库,这样做是为了最大力度减少对数据库的访问
  1. 系统初始化,把商品库存数量stock加载到Redis
  2. 服务器接收秒杀请求,在秒杀阶段使用本地标记localOverMap(goodsId,boolean)对秒杀商品做标记,若被标记为true,表明商品秒杀完毕,直接返回秒杀结束,未被标记为true才查询redis,通过本地标记来减少对redis的访问
  3. Redis预减库存,如果库存已经到达临界值的时候,直接返回失败,即后面的大量请求无需给系统带来压力,通过Redis预减少库存减少数据库访问
  4. 通过redis缓存判断这个秒杀订单形成没有,避免同一用户重复秒杀。如果是重复秒杀,则需要对Redis的预减库存进行回增,并重重置本地标记localOverMap为false。
  5. 为了保护系统不受高流量的冲击而导致系统崩溃的问题,使用RabbitMQ用异步队列处理下单,实际做了一层缓冲保护,做了一个窗口模型,窗口模型会实时的刷新用户秒杀的状态。
  6. 后端RabbitMQ监听秒杀MIAOSHA_QUEUE的这名字的通道,如果有消息过来,获取到传入的信息,执行真正的秒杀之前,要判断数据库的库存,判断是否重复秒杀,然后执行秒杀事务(减库存,下订单,写入秒杀订单),秒杀订单还需要写到Redis中,方便判断是否重复秒杀。
  7. 客户端根据商品id用js轮询接口,用来获取处理状态
6.9 秒杀接口地址的隐藏
  • 每次点击秒杀按钮,才会生成秒杀地址,秒杀地址不是写死的,是从服务端获取,动态拼接而成的地址
6.10 数学公式验证码
  • 点击秒杀前,先让用户输入数学公式验证码,验证正确才能获取秒杀地址进行秒杀
  • 优点:
    • 防止恶意的机器人和爬虫,刷票软件恶意频繁点击按钮来刷请求秒杀地址接口的操作
    • 分散用户的请求: 高并发下场景,在刚刚开始秒杀的那一瞬间,迎来的并发量是最大的,减少同一时间点的并发量,将并发量分流也是一种减少数据库以及系统压力的措施(使得1s中来10万次请求过渡为10s中来10万次请求)
6.11 接口限流防刷
  • 限制同一用户一定时间内(如1 min)只能访问固定次数,可以使用拦截器减少对业务的侵入,在服务端对系统做一层保护

7.项目难点及解决方案

7.1 缓存穿透和缓存雪崩以及缓存一致性等问题
7.1.1 缓存穿透
  • 指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。

  • 解决方案

    • 如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓存中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴!
    • 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
7.1.2 缓存雪崩
  • 缓存雪崩指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都到达数据库
  • 解决方案
    • 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现,如设置token的有效期为两天,商品列表缓存为30s;
    • 为了防止缓存在同一时间大面积过期导致的缓存雪崩,给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存
      • 缓存标记: 记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存
      • 缓存数据: 它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。 这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存
    • 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。
    • 也可以进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩,如在系统初始化的时候就将秒杀商品库存加载到Redis
7.1.3 缓存一致性问题
  • 首先尝试从缓存读取,读到数据则直接返回;如果读不到,就读数据库,并将数据会写到缓存,并返回。
  • 需要更新数据时,先更新数据库,然后把缓存里对应的数据失效掉(删掉)
7.2 大量的使用缓存,对于缓存服务器,也有很大的压力,思考如何减少redis的访问?
  • 服务器接收秒杀请求,在秒杀阶段使用本地标记localOverMap(goodsId,boolean)对秒杀商品做标记,若被标记为true,说明商品库存不足,直接返回秒杀结束,未被标记为true才查询redis,通过本地内存标记来减少对redis的访问
7.3 在高并发请求的业务场景,大量请求来不及处理,甚至出现请求堆积时候?
  • 消息队列,用来异步处理请求。每次请求过来,先不去处理请求,而是放入消息队列,然后在后台布置一个监听器,分别监听不同业务的消息队列,有消息来的时候,才进行秒杀业务逻辑。这样防止多个请求同时操作的时候,数据库连接过多的异常
7.4 如何保证一个用户不能重复下单
  • Redis层:当该用户完成秒杀,写入订单时,同时将该秒杀订单写到Redis,当该用户再次秒杀时,通过查询Redis缓存便可判断是否重复下单。
  • 数据库层:秒杀订单表中为userId和goodsId建立唯一索引,使得第一个记录可以插入,第二个则出错,然后通过事务回滚,防止一个用户同时发出多个请求的处理,秒杀到多个商品
7.5 超卖问题
  • sql要多加一个stock_count > 0 ,使用数据库特性来保证超卖的问题,只有stock_count还大于0的时候才去读stock_count然后减1操作,只有减库存成功,整个秒杀逻辑才能完成,如果库存不足,则通过事务回滚使得下单失败。
@Update("update miaosha_goods set stock_count=stock_count-1 where goods_id=#{goodsId} and stock_count>0")
	public void reduceStock(MiaoshaGoods goods);  
7.6 页面静态化及什么是浏览器缓存?
  • 将纯HTML静态页面缓存在客户端浏览器,数据的加载只需要通过ajax来请求服务器,实现前后端分离,静态页面无需连接数据库打开速度较动态页面会有明显提高,减少了带宽,也加快用户访问的速度。

  • 浏览器缓存就是把一个已经请求过的Web资源(如html页面,图片,js,数据等)拷贝一份副本储存在浏览器中。缓存会根据进来的请求保存输出内容的副本。当下一个请求来到的时候,如果是相同的URL,缓存会根据缓存机制决定是直接使用副本响应访问请求,还是向源服务器再次发送请求。比较常见的就是浏览器会缓存访问过网站的网页,当再次访问这个URL地址的时候,如果网页没有更新,就不会再次下载网页,而是直接使用本地缓存的网页。只有当网站明确标识资源已经更新,浏览器才会再次下载网页。

  • 本地缓存过期后,浏览器会向服务器发送请求,request中会携带以下两个字段:

    • If-Modified-Since:值为之前response中Last-Modified;
      
    • If-None-Match:值为之前response中Etag(如果存在的话);
      
  • 其中在图右侧的“filemodified?”判断中,服务器会读取请求头这两个值,判断出客户端缓存的资源是否最新,如果是的话服务器就会返回HTTP/304 NotModified响应头,但没有响应体。客户端收到304响应后,就会从缓存中读取对应的资源;否则返回HTTP/200和响应体

7.7 秒杀架构设计理念?
  • 限流: 鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。

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

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

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

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

7.8 .秒杀系统架构设计思路
7.8.1 将请求拦截在系统上游,降低下游压力:秒杀系统特点是并发量极大,但实际秒杀成功的请求数量却很少,所以如果不在前端拦截很可能造成数据库读写锁冲突,最终请求超时。
7.8.2 利用缓存:利用缓存可极大提高系统读写速度。
7.8.3 消息队列:消息队列可以削峰,将拦截大量并发请求,这也是一个异步处理过程,后台业务根据自己的处理能力,从消息队列中主动的拉取请求消息进行业务处理。
7.9 假如减了库存用户没有支付,库存怎么还原继续参加抢购?
  • 设定一个最长付款时间,比如30分钟,后台有个定时任务(使用定时器Timer),轮训超过30分钟的待付款订单(数据库里面判定订单状态),然后关闭订单,恢复库存

8.优化前后压测对比

8.1 商品列表页 页面缓存优化前后压测对比
  • 页面缓存优化前 1000 * 10 QPS:222
    页面缓存优化前
  • 页面缓存优化后 1000 * 10 QPS:353
    页面缓存优化后
8.2 秒杀接口优化前后压测对比
  • 优化前 1000 * 10 QPS:231
    优化前
  • 优化后 1000 * 10 QPS:1034
    优化后

9. 高并发秒杀系统项目地址

10.具体技术实现细节

  1. 明文密码两次MD5处理
  2. Redis通用缓存Key封装
  3. JSR303 参数校验 + 全局异常处理
  4. 自定义参数解析器
  5. 页面优化技术(页面缓存 + 对象缓存)
  6. 页面优化技术(页面静态化 ,前后端分离)
  7. 使用RabbitMQ实现高并发接口优化
  8. 秒杀地址隐藏
  9. 数学公式验证码
  10. 接口限流防刷
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值