- 本系统是使用SpringBoot开发的高并发限时抢购秒杀系统,除了实现基本的登录、查看商品列表、秒杀、下单等功能,项目中还针对高并发情况实现了系统缓存、降级和限流。
- Aliyun CentOS7.3
- IntelliJ IDEA + Navicat + Git + Chrome
- 压测工具Apache Jmeter
前端技术 | BootStrap | JQuery | Thymeleaf |
---|---|---|---|
后端技术 | SpringBoot | MyBatis | MySQL |
中间件技术 | Druid | Redis | RabbitMQ |
- 将请求尽量拦截在系统上游:传统秒杀系统之所以挂,请求都压倒了后端数据层,数据读写锁冲突严重,几乎所有请求都超时,流量虽大,下单成功的有效流量甚小,我们可以通过限流、降级等措施来最大化减少对数据库的访问,从而保护系统。
- 充分利用缓存:秒杀商品是一个典型的读多写少的应用场景,充分利用缓存将大大提高并发量
- 在秒杀活动中,当队列写入消息达到某一数值时,不再写入消息队列,而直接跳转到活动结束的页面
- 冗余(存储):在某些情况下处理数据的过程中会失败,消息中间件允许把数据持久化知道他们完全被处理
- 削峰:在访问量剧增的情况下,但是应用仍然需要发挥作用,但是这样的突发流量并不常见。而使用消息中间件采用队列的形式可以减少突发访问压力,不会因为突发的超时负荷要求而崩溃;消息队列是基于队列的,在秒杀活动中,当队列写入消息达到某一数值时,不再写入消息队列,而直接跳转到活动结束的页面
- 顺序保证:在大多数场景下,处理数据的顺序也很重要,大部分消息中间件支持一定的顺序性
- 缓冲:消息中间件通过一个缓冲层来帮助任务最高效率的执行
- 异步通信:通过把把消息发送给消息中间件,消息中间件并不立即处理它,后续在慢慢处理
- 接口的输出结果做了一个Result封装
- 对错误的代码做了一个CodeMsg的封装
- 访问缓存做了一个key的封装
- 用户端:inputPassToFormPass = MD5(明文+固定salt)
- 服务端:formPassToDBPass = MD5(inputPassToFormPass+ 随机salt)
- 好处:
- 第一次作用:防止用户明文密码在网络进行传输
- 第二次作用:防止数据库被盗,避免通过MD5反推出密码,双重保险
- 验证用户账号密码都正确情况下,通过UUID生成唯一id作为token,再将token作为key、用户信息作为value模拟session存储到redis,同时将token存储到cookie,保存登录状态,每次需要session,从缓存中取即可
- 好处: 在分布式集群情况下,服务器间需要同步,定时同步各个服务器的session信息,会因为延迟到导致session不一致,使用redis把session数据集中存储起来,解决session不一致问题
- 使用JSR303自定义校验器,实现对用户账号、密码的验证,使得验证逻辑从业务代码中脱离出来
- 优点1: 可以实现对项目中所有产生的异常进行拦截,在同一个类中实现统一处理。避免异常漏处理的情况。
- 优点2: 当Service 出现业务逻辑错误的时候,这个时候我们可以直接抛出异常,让拦截器来捕捉,捕捉之后,就不需要冗余的代码来return 一个不符合业务逻辑的返回值来作为输出。
- 优点3: 当参数校验不通过的时候,输出也是Result(CodeMsg),传给前端用于前端显示获取处理
- 本项目大量的利用了缓存技术,包括用户信息缓存(分布式session),商品信息的缓存,商品库存缓存,订单的缓存,页面缓存,对象缓存减少了对数据库服务器的访问
- 页面缓存:通过在手动渲染得到的html页面缓存到redis
- 对象缓存:包括对用户信息、商品信息、订单信息和token等数据进行缓存,利用缓存来减少对数据库的访问,大大加快查询速度。
- 页面静态化的主要目的是为了加快页面的加载速度,将商品详情和订单详情页面做成静态HTML(纯的HTML),数据的加载只需要通过ajax来请求服务器,实现前后端分离,静态页面无需连接数据库打开速度较动态页面会有明显提高,并且做了静态化HTML页面可以缓存在客户端的浏览器。
- 大量的缓存引用也出现了一个问题,如何识别不同模块中的缓存(key值重复,如何辨别是不同模块的key)
- 解决:利用一个抽象类,定义BaseKey(前缀),在里面定义缓存key的前缀以及缓存的过期时间从而实现将缓存的key进行封装。让不同模块继承它,这样每次存入一个模块的缓存的时候,加上这个缓存特定的前缀,以及可以统一制定不同的过期时间
- 描述:通过三级缓冲保护,1、本地标记 2、redis预处理 3、RabbitMQ异步下单,最后才会访问数据库,这样做是为了最大力度减少对数据库的访问
- 系统初始化,把商品库存数量stock加载到Redis
- 服务器接收秒杀请求,在秒杀阶段使用本地标记localOverMap(goodsId,boolean)对秒杀商品做标记,若被标记为true,表明商品秒杀完毕,直接返回秒杀结束,未被标记为true才查询redis,通过本地标记来减少对redis的访问
- Redis预减库存,如果库存已经到达临界值的时候,直接返回失败,即后面的大量请求无需给系统带来压力,通过Redis预减少库存减少数据库访问
- 通过redis缓存判断这个秒杀订单形成没有,避免同一用户重复秒杀。如果是重复秒杀,则需要对Redis的预减库存进行回增,并重重置本地标记localOverMap为false。
- 为了保护系统不受高流量的冲击而导致系统崩溃的问题,使用RabbitMQ用异步队列处理下单,实际做了一层缓冲保护,做了一个窗口模型,窗口模型会实时的刷新用户秒杀的状态。
- 后端RabbitMQ监听秒杀MIAOSHA_QUEUE的这名字的通道,如果有消息过来,获取到传入的信息,执行真正的秒杀之前,要判断数据库的库存,判断是否重复秒杀,然后执行秒杀事务(减库存,下订单,写入秒杀订单),秒杀订单还需要写到Redis中,方便判断是否重复秒杀。
- 客户端根据商品id用js轮询接口,用来获取处理状态
- 每次点击秒杀按钮,才会生成秒杀地址,秒杀地址不是写死的,是从服务端获取,动态拼接而成的地址
- 点击秒杀前,先让用户输入数学公式验证码,验证正确才能获取秒杀地址进行秒杀
- 优点:
- 防止恶意的机器人和爬虫,刷票软件恶意频繁点击按钮来刷请求秒杀地址接口的操作
- 分散用户的请求: 高并发下场景,在刚刚开始秒杀的那一瞬间,迎来的并发量是最大的,减少同一时间点的并发量,将并发量分流也是一种减少数据库以及系统压力的措施(使得1s中来10万次请求过渡为10s中来10万次请求)
- 限制同一用户一定时间内(如1 min)只能访问固定次数,可以使用拦截器减少对业务的侵入,在服务端对系统做一层保护