秒杀项目总结

news2025/1/15 6:36:36

秒杀就是同一个时刻有大量的请求争抢购买同一个商品,并且完成交易的过程

也就是大量的并发读和并发写

先制作一个增删改查的秒杀系统,但是想让这个系统支持高并发访问就没那么容易了,

如何让这个秒杀系统面对百万级的请求流量不出故障,如何保证高并发情况下的数据一致性问题

rabbitmq主要是用来做一些异步,解耦模块,也可以用来流量削峰

课程介绍:

先是搭建项目

然后是分布式共享session

然后是开发秒杀功能(简单的增删改查),进行压力测试(可能会出现商品超卖,并发扛不住的问题):这里主要是四个功能,商品列表,商品详情,秒杀,订单详情

最后进行优化(主要是三方面的优化,页面优化,服务优化,接口安全上的优化)

页面优化:主要是缓存(页面缓存,url缓存,对象缓存),还有静态化分离(页面静态化,前后端分离),还有静态资源的优化

服务优化:RabbitMQ消息队列,分布式锁

安全优化:隐藏秒杀的地址(前期这个地址不会出来,只有到点了,才会出现秒杀的地址,让用户进行秒杀),验证码(区分是真实的用户还是脚本),接口限流(对于频繁访问的ip做一些控制)

总共五张表,用户表,商品表,秒杀商品表,订单表,秒杀订单表

由于商品可能秒杀和不秒杀价格可能同时存在,所以特地建一张秒杀商品表,区分于商品表

1.数据库

用户表t_user

(1)id  用户id  也就是手机号码

(2)nickname  昵称

(3)password  MD5(MD5(password+salt)+salt)  经过两次md5加密

(4)salt

(5)head  头像

(6)register_date  注册时间

(7)last_login_date 最后一次登陆时间

(8)login_count    登录次数

商品表t_goods

(1)id  商品id

(2) goods_name

(3)goods_title

(4)goods_img

(5)goods_detail  

(6)goods_price

(7)goods_stock  商品库存 ,-1表示数量没有限制

订单表t_order

(1)id  商品id

(2) user_id   用户id,谁创建的这个订单

(3)goods_id  商品id

(4)delivery_addr_id   收货地址id

(5)goods_name  商品名称

(6)goods_count  商品数量,买几个

(7)goods_price  商品单价

(8)order_channel  下单渠道,1表示pc端,2表示安卓,3表示ios

(9)status  订单状态:0表示新建未支付,1表示已支付,2表示已发货,3表示已收货,4表示已退款,5表示已完成

(10)create_date   订单创建时间

(11)pay_date   支付时间

t_seckill_goods  秒杀商品表

(1)id  秒杀商品id

(2)goods_id  商品id

(3)seckill_price  秒杀价格

(4)stock_count  库存数量

(5)start_date  秒杀开始时间

(6)end_date   秒杀结束时间

t_seckill_order 秒杀订单表

(1)id  秒杀订单id

(2)user_id   用户id

(3)order_id   订单id

(4)goods_id   商品id 

秒杀商品表    有人说这个表是多余的,直接在商品表中增加一个字段,这个字段为0表示普通商品,这个字段为1,表示秒杀商品,但是这样有一些问题,商品的秒杀时和平常的价格是不一样的

1. 登录功能

登陆成功就会跳转到商品列表页面list.html,点击商品,进入详情页detail.html

而且登陆成功需要生成一个登录凭证ticket,将登录凭证保存到cookie里面

//生成一个随机字符串作为用户的ticket
String  ticket=UUIDUtil.uuid();

//user是用户实体类对象,将ticket字符串:user对象添加到session里面
request.getSession().setAttribute(ticket,user);


//再将session添加到cookie里面,第三个参数是这个cokkie字段的名字
CookieUtil.setCookie(request,response,"userTicket",ticket);

 登录后我们查看cookie:

 2.分布式session

如果我们将登录代码部署在多台服务器上面,

ngnix采用的默认的负载均衡策略是轮询,请求会按照时间顺序逐一分发到后端服务器上去

也就是说:一开始我们可能是在tomcat1上面去登录,这样用户信息就存在tomcat1的session里面,接下来请求可能就被分发到tomcat2上面,此时tomcat2的session里面没有用户信息,于是又要重新登录,这就是分布式session问题

有以下几个解决问题的方法:

(1)Session复制

就是将其中一台服务器的session复制给其他服务器

这样做的优点在于不需要修改代码,只需修改tomcat的配置即可,非常简单

缺点是session的同步传输会占用内网的带宽,多台tomcat同步的话性能会指数级下降,每一台服务器都保存相同的session特别占用内存,造成了内存浪费(冗余)

(2)前端存储 

这样的优点是不占用服务器端内存

但是缺点是存在安全风险,cookie很容易被拦截,而且大小也受制于cookie的大小

(3)Session粘滞

当用户发出第一个request后,负载均衡器动态的把该用户分配到某个节点,并记录该节点的路由,以后该用户的所有request都绑定到这个路由,该用户只会与该server发生交互

缺点是:如果某台服务器挂掉了,Session就会丢失

(4)后端集中存储(也叫使用redis实现分布式session)

就是将这个session存储在redis里,请求来了之后,不管哪个服务器分配来处理这个请求,去redis里面查出这个session

我们采取的是后端集中存储的方法,将session存储到redis里面,不管哪个服务器要用户信息,直接去redis中获取

使用redis实现分布式session,有两种实现方法

方法一:使用spring session来实现

引入redis和spring session的依赖

在application.yml配置文件中进行配置

redis:
  host:192.168.10.100   //redis服务器的ip地址
  port:6379
  database:0  //默认操作几号数据库
  ......
     

 方式二:

@Autowired 
private  RedisTemplate   redisTemplate;

//生成一个随机字符串作为用户的ticket
String  ticket=UUIDUtil.uuid();

//存入redis,user表示用户实体类对象
redisTemplate.opsForValue.set("user"+ticket,user);


//将ticket放入到cookie里面
CookieUtil.setCookie(request,response,"userTicket",ticket);
User  user=redisTemplate.opsForValue().get("user:"+userTicket);
if(user==null)
{
   跳转到登录页面
}
else
{
   跳转到商品列表页面
}

注意:这里虽然使用了redis,但是这里我们用的redis只是装一个根节点,哨兵,主从复制,读写分离,集群都没有考虑

redis的高级数据结构位图bitmaps,HyperLogLog,GEO没有涉及

磁盘持久化方案也没有涉及

3.拦截器判断用户有没有登录

之前每次都要从session或者redis里面根据ticket字符串来获取user对象,然后去数据库中查询这个用户是否存在,

具体参考这篇博客: 拦截器Interceptor_Pr Young的博客-CSDN博客

4.秒杀功能

用户登录成功后,跳转到商品列表页,商品列表页有一个详情按钮,点击详情跳转到商品详情页,在这个页面可以进行秒杀(在秒杀还没开始的时候,秒杀按钮是灰色的,不能按的,倒计时结束,这个秒杀按钮才可以按),秒杀成功后就会进入订单页

这个功能里需要创建四张数据表   商品表,秒杀商品表,订单表,秒杀订单表

还需要开发  商品列表页,商品详情页,订单页三个页面

商品列表页:

 商品详情页

 怎么实现倒计时功能呢?

设置一个秒杀开始时间t1和秒杀结束时间t2

当前的时间在t1之前就,秒杀还未开始

当前的时间在t1之后,在t2之前,就开始秒杀

当前时间在t2之后,就结束秒杀

点击秒杀按钮的时候就会调用秒杀方法,先查此时数据库中该商品是否有库存(去秒杀商品表中查找),并且判断当前用户是否已经秒杀过(去秒杀订单表中查找,如果表中已经存在这个用户id表示这个用户已经秒杀过了,不能再秒杀了)

5.使用JMeter进行压测

当100个人同时去点击这个立即秒杀

压力测试:测的是并发,

QPS:Queries Per Second,每秒的查询率,是一台查询服务器每秒能够响应的查询次数(每秒执行查询sql的次数)

TPS:Transactions Per Second,意思是每秒事务数,从客户机发送请求开始计时,到服务机收到请求然后发送给客户机处理结果,客户机收到处理结果停止计时

JMeter:选择某一个页面进行压力测试

1000个线程 访问这个页面,吞吐量为262

测试商品列表接口和秒杀接口(商品列表接口只需要读取数据,而秒杀接口需要更新数据)

商品列表页windows优化前qps:1332,Linux优化前qps  207

秒杀接口:5000个用户同时秒杀id=1的商品

window优化前qps:785   linux优化前qps:

qps小还不是问题,问题在于发现库存变为负数(也就是超卖了)

而且还有个问题就是:秒杀接口没有隐藏起来(虽然按钮是灰色的,但是你知道秒杀路径的话依然可以直接访问这个路径)

6.第一个优化  使用缓存

(1)页面缓存:使用的是thymleaf模板,需要从服务器端查询数据,然后将数据全部放到浏览器做展示示,可以将这些数据放到redis里面

缓存商品列表页

//redis中获取页面,如果可以获取到页面,直接返回页面
String html=redisTemplate.opsForValue().get("goodsList");
if(StringUtil.isEmpty(html)==false)
{
     return   html;
}
else//页面为空就要将其存入缓存
{
   先去获取到html,然后存到redis里面
   redisTemplate.opsForValue().set("goodsList",html);
   return  html;
}

(2)url缓存:把商品详情页也缓存起来,还要传入一个商品ID

String html=redisTemplate.opsForValue().get("goodsList"+goodsId);

尽管做了缓存,还是需要从redis中发送整个页面给前端,于是后面还会进行前后端分离

(3)对象缓存:

吞吐量变成了2394(之前是1332)

7.第二个优化:“页面静态化:即使使用页面缓存,还是需要将一整个thymleaf引擎发送给前端,前后端分离,前端就是html,里面的一些动态数据才需要从后端发给前端

静态化商品详情页面

以上两个优化都是页面优化,接下来是接口优化

8.接口优化

即使加了缓存之后,有些接口也要访问数据库,比如去数据库中获取某件商品的库存,然后扣减库存,这部分就要通过redis来扣减库存

即使你用了redis来扣减库存不需要去数据库中读取数据了,但是我们仍然需要频繁的和redis进行交互,redis放在一个单独的服务器上,所以我们还是需要频繁的和redis服务器进行交互,可以通过内存标记来减少对redis服务器的访问

下单操作也要进行优化,下单的时候如果直接去找数据库,数据库仍然是扛不住这么大量的并发,可以用队列,先让请求进入到队列里面进行缓冲,通过队列进行异步下单增强用户体验

总结:(1)通过redis预减库存,减少对数据库的访问

      (2)内存标记,减少对redis的访问

    (3) 请求进入队列缓存,异步下单

redis预减库存,如果库存不足,直接返回库存不足,这样已经可以大幅提高性能

如果库存是足的,就将这个请求封装成一个对象,发送给rabbitmq,这样前期大量的请求过来依然可以快速处理掉,后面消息队列再慢慢处理(起到一个流量削峰的作用),此时显示排队中,

也就是将redis中每个秒杀商品:这个秒杀商品的库存存入到redis里面

使用topic模式

@Configuration
public  class   RabbitMQTopicConfig
{
    @Bean
    public  Queue    queue()
    {
        return   new   Queue("seckillQueue");//队列名称就叫queue,消息持久化
 
    }

    //定义交换机
    pubcli  TopicExchange  topicExchange()
    {
       return  new  TopicExchange("topicExchange");
    }

 
    @Bean  //绑定队列到交换机,需要带上路由键route key
    public    Binding  binding()
    {
       return   BindingBuilder.bind(seckillQueue()).to(topicExchange()).with("*.queue.#");
    }
}

发送方:

 接收方:

@RabbitListen(queues="seckillQueue")
//接收到消息开始下单
public   void    receive(String message)
{
        //将字符串转为对象
        SeckillMessage seckillMessage = JsonUtil.jsonStr2Object(message, SeckillMessage.class);
        
        //获取要秒杀的商品的id以及用户对象
        Long goodsId = seckillMessage.getGoodsId();
        User user = seckillMessage.getTUser();

        //根据商品id获取商品对象
        GoodsVo goodsVo = itGoodsServicel.findGoodsVobyGoodsId(goodsId);

        if (goodsVo.getStockCount() < 1) 
        {
            return;
        }

        //判断是否重复抢购,查询redis中是否已经有该用户id:该秒杀商品id这一行数据
        TSeckillOrder tSeckillOrder = (TSeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
        if (tSeckillOrder != null) 
        {
            return;
        }
        
        //正式下单
        itOrderService.secKill(user, goodsVo);
}
//将请求封装成一个对象
 SeckillMessage seckillMessag = new SeckillMessage(user, goodsId);


//发送这个对象到消息队列中
mqSender.send(seckillMessag);

9.最后是接口安全性保障

也就是黄牛把脚本准备好,快速抢秒杀商品,导致很多正真的用户抢不到秒杀商品

隐藏接口地址

用验证码隔离脚本和延长请求时间长度

接口限:漏桶算法(也可以采用令牌桶算法)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/159245.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

02-final、finally、finalize的区别?

1.final final是java中的修饰符&#xff0c;用于修饰属性&#xff08;变量&#xff09;、方法、类。 1.被final修饰的变量不可以被改变,如果修饰引用,那么表示引用不可变,引用指向的内容可变. 被修饰的常量,在编译阶段会存入常量池中. 2.被final修饰的方法不可以被重写, 被修…

图片播放器的实现1——利用Image2LCD提取图片数据并显示

以下内容源于朱有鹏嵌入式课程的学习与整理&#xff0c;如有侵权请告知删除。 参考内容 &#xff08;1&#xff09;https://xiefor100.blog.csdn.net/article/details/71941527 &#xff08;2&#xff09;https://xiefor100.blog.csdn.net/article/details/78529519 内容总结 …

2022. 12 青少年软件编程(图形化) 等级考试试卷(四级)

2022年12月 青少年软件编程&#xff08;图形化&#xff09; scratch等级考试试卷&#xff08;四级&#xff09; 分数&#xff1a; 100 题数&#xff1a; 28 一、单选题(共 15题&#xff0c;共 30分) 1.运行下列程序&#xff0c; 变量“结果”的值为&#xff1f; &#xff08; &…

WXSS 如何进行编译?

过往中小企业或技术团队开发一个 App 的时间成本和人力成本居高难下&#xff0c;但是随着微信上线小程序&#xff0c;更像是为这部分群体打开了一扇天窗&#xff0c;此后小程序呈现出井喷式发展的状态&#xff0c;不仅微信&#xff0c;支付宝、百度、抖音等超级 App 都跟上步伐…

【C++核心编程】C++全栈体系(十)

C核心编程 第四章 类和对象 六、继承 继承是面向对象三大特性之一 有些类与类之间存在特殊的关系&#xff0c;例如下图中&#xff1a; 我们发现&#xff0c;定义这些类时&#xff0c;下级别的成员除了拥有上一级的共性&#xff0c;还有自己的特性。 这个时候我们就可以考…

华为DHCPv6实验配置

目录 配置AR1作为DHCPv6服务器为PC1分配IPv6地址 配置AR2作为DHCPv6服务器&#xff0c;AR1作为DHCPv6中继器为PC2分配IPv6地址 配置AR3作为DHCPv6 PD服务器为AR1分配地址前缀 什么是DHCP PD 配置AR1作为DHCPv6服务器为PC1分配IPv6地址 AR1 DHCPv6服务器端配置 ipv6 …

解决N+1问题的另一种方法 - 关联的多结果集ResultSet

如果我的博客对你有帮助&#xff0c;欢迎进行评论✏️✏️、点赞&#x1f44d;&#x1f44d;、收藏⭐️⭐️&#xff0c;满足一下我的虚荣心&#x1f496;&#x1f64f;&#x1f64f;&#x1f64f; 。 从版本 3.2.3 开始&#xff0c;MyBatis 提供了另一种解决 N1 查询问题的方…

C语言——位段

文章目录思维导图&#xff1a;一. 什么是位段二.位段的内存分配三.位段的跨平台问题四.位段的应用结语:思维导图&#xff1a; 一. 什么是位段 位段的声明和结构体类似&#xff0c;但是有2个不同&#xff1a; 位段的成员必须是int、unsigned int 或 signed int(在很多平台上cha…

python自学之《21天学通Python》(9)——基于tkinter的GUI编程

第12章 基于tkinter的GUI编程 Windows的图形用户界面非常方便用户操作&#xff0c;因此&#xff0c;Windows操作系统得到了广大个人计算机用户的欢迎。在Python中&#xff0c;也可以编写美观的GUI界面应用程序与项目。tkinter是Python自带的用于GUI编程的模块&#xff0c;tkin…

【论文速递】CVPR2022 - 学习 什么不能分割:小样本分割的新视角

【论文速递】CVPR2022 - 学习 什么不能分割:小样本分割的新视角 【论文原文】&#xff1a;Learning What Not to Segment: A New Perspective on Few-Shot Segmentation 获取地址&#xff1a;https://openaccess.thecvf.com/content/CVPR2022/papers/Lang_Learning_What_Not_…

Linux--线程控制--线程相关函数--tid--0109 10

1.如何理解线程 定义&#xff1a;在一个程序里的一个执行路线就叫做线程&#xff08;thread&#xff09;。 更准确的定义是&#xff1a;线程是“一个进程内部的控制序列”。 每个进程都有自己的进程地址空间和task_struct结构体&#xff0c;如果我们通过一定的方式在创建进程…

【记录】ChatGPT|近期两次更新一览(更新至2023年1月12日)

如果你还没有使用过ChatGPT&#xff0c;可以先看看我的上一篇文章&#xff1a;【记录】ChatGPT&#xff5c;注册流程、使用技巧与应用推荐&#xff08;更新至2022年12月14日&#xff09;。   昨天晚上&#xff0c;ChatGPT突然很多人都无法登录&#xff0c;包括我。我当时以为…

SpringBoot+Redis+@Cacheable实现缓存功能

SpringBootRedisCacheable实现缓存功能一、pom文件加入Redis与cache的依赖和yml配置二、EnableCaching允许使用注解进行缓存三、Redis配置四、业务逻辑1.UserController2.UserService3.UserServiceImpl4.AdminServiceImpl5.Cacheable和CachePut区别五、测试1.执行saveUser方法2…

剑指offer----C语言版----第十七天----面试题23:链表中环的入口节点

目录 1. 链表中环的入口节点 1.1 环形链表Ⅰ 1.1.1 题目描述 1.1.2解题思路 1.1.3 扩展问题 1.2 环形链表Ⅱ 1.2.1 题目描述 1.2.2 思路分析 1. 链表中环的入口节点 在leetcode上的剑指offer专栏没有收录这道题目&#xff0c;但Leetcode上是有这道题目的&#xff0c;环…

U3D客户端框架之 音效管理器 与 Fmod介绍安装导入Unity

一、Fmod介绍与安装导入Unity 1.Fmod与Unity内置Audio播放器对比 Unity内置的Audio底层使用的是FMOD&#xff0c;但是功能不够齐全&#xff0c;高级一点的功能如混合(Mix)等无法使用&#xff1b; 音效管理应该和Unity工程解耦合&#xff0c;这样子可以减轻音效设计师的负担&a…

ArcGIS基础实验操作100例--实验86矢量面重叠分析

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 空间分析篇--实验86 矢量面重叠分析 目录 一、实验背景 二、实验数据 三、实验步骤 &#xff08;1&am…

初阶指针详解✍

目录1.内存和地址2.指针变量的大小3.指针类型的意义意义1&#xff1a;指针访问权限的大小意义2&#xff1a;指针类型决定指针的步长4.野指针野指针成因如何规避野指针5.指针的运算指针加减整数指针减指针指针的比较运算6.指针与数组的关系7.二级指针1.内存和地址 内存是电脑上特…

2、C语言程序规范

目录 1. 代码缩进 2. 变量、常量命名规范 3. 函数的命名规范 4. #include指令 5. 注释 6. main函数 7.函数返回值 8. 变量赋初值 俗话说&#xff0c;“没有规矩&#xff0c;不成方圆。” 如&#xff1a;第一个程序 #include <stdio.h>void main(){printf("…

基于java Springmvc+mybatis 电影院售票管理系统设计和实现以及文档

基于java Springmvcmybatis 电影院售票管理系统设计和实现以及文档 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留…

vue报错汇总

项目场景&#xff1a; 使用vue报错汇总。 1、项目启动不报错也不成功 提示&#xff1a;这里描述项目中遇到的问题&#xff1a; 项目启动时&#xff0c;一直启动不成功&#xff0c;末句提示 98% emitting Copyplugin… 原因分析&#xff1a; 最有可能是因为require或者import了…