当前位置: 首页 > news >正文

秒杀系统高并发优化

文章目录

  • 分析秒杀系统中的高并发
  • 前端高并发优化
    • 绑定一次性的秒杀事件
    • 使用CDN缓存
  • 后端高并发优化
    • 使用redis缓存
      • 暴露秒杀接口
      • 执行秒杀
      • 代码实现
    • 执行秒杀操作的高并发优化
      • 简单优化
      • 深度优化

分析秒杀系统中的高并发

我们先看一下秒杀系统的在整个执行流程,如下:
在这里插入图片描述

其中橙色的就是需要进行高并发优化的部分。
什么是高并发?
通俗来讲,高并发是指在同一个时间点,有很多用户同时的访问同一 API 接口或者 url 地址。它经常会发生在有大活跃用户量,用户高聚集的业务场景中。

在秒杀系统中,当秒杀开始时,就会产生大量的用户聚集,此时就会给后端服务器带来很大的压力。尤其是在出现热点商品时,大量用户同时对同一个商品进行秒杀操作,这样就会带来更大的网络延迟(后面会具体说明)。

前端高并发优化

绑定一次性的秒杀事件

当秒杀开始时,用户会习惯性的疯狂点击秒杀按钮,为防止客户端将重复的请求发给服务器,我们将秒杀按钮绑定一次性事件,并且在点击一次后禁用该按钮(因为由点击到秒杀成功速度很快,用户很难看到这一过程)
部分js代码如下:

$('#seckillBtn').one('click',function () {
     //禁用按钮
     $(this).addClass("disabled");
     //接口暴露 + 执行秒杀
     //......
}

使用CDN缓存

什么是CDN?
CDN:内容分发网络(Content Delivery Network),又称网络加速器,解决因访问量较大、服务器与客户端物理距离较远等多种原因造成的网络延迟问题。通过建立多个缓存服务器的方式,当客户端要访问服务器时会选择较近的缓存服务器进行访问从而实现加速。

CDN好处有哪些?

  1. 缩短内容储存地和需要去的地方之间的距离,减少了数据传输所花事件。
  2. 当用户访问的是CDN缓存的数据,那么用户请求就可以直接由CDN来做出响应,减轻了服务器端的压力。
  3. CDN具有很强的可靠性。有时,互联网上的内容会出错。服务器出现故障,网络变得拥挤,连接中断。CDN 让 Web 应用程序即使在面对这些问题时也能够为用户提供不间断的服务。
  4. CDN 特别适合用于保护网站免受拒绝服务 (DoS) 和分布式拒绝服务 (DDoS) 攻击。在这些攻击中,攻击者将大量垃圾网络流量引导至网站,试图使网站不堪重负并崩溃。凭借其众多服务器,CDN 能够比单个源服务器更好地吸收大量流量.

上面写的前两点好处就能很好的说明CDN对高并发的巨大优化。

秒杀系统应该将哪些资源部署到CDN呢?

在秒杀系统中,许多资源都是固定不变的,例如商品详情,有些CSS,JS资源等,我们可以将它们部署到CDN中,就算大量用户获取商品信息或者是刷新页面,这些请求都可以由CDN解决。

在这里插入图片描述

后端高并发优化

使用redis缓存

用户在秒杀商品时,后端需要频繁的从数据库获取数据。例如,商品一被瞬间被秒杀了1000次,那么服务器就访问了1000次数据库,并且每次获取的数据都相同,这样就会浪费大量时间。所以,我们可以使用redis将访问过的数据存在内存中,当下次访问该数据时,就直接可以从redis中获取。

哪些操作可以使用redis缓存呢?
我们分析一下秒杀过程对数据库进行了哪些操作,如下:

暴露秒杀接口:只对数据库进行了读操作。
执行秒杀:减少库存和插入购买明细,分别对数据库进行了插入和更新的写操作。

暴露秒杀接口

暴露秒杀接口只对数据库进行了读操作,我们使用Redis缓存完全没有问题。
在这里插入图片描述

执行秒杀

执行秒杀我们进行了两步操作,其中减少库存操作在高并发场景下如果使用redis缓存就会产生很大问题。

假设现在商品一库存为100,我们来分析一下使用redis缓存情况下,两个线程同时对商品一减库存会产生怎么样的问题:

我们这里有两种思路:

  1. 先更新Redis缓存,再同步到mysql
时间线程一线程二
t1更新redis缓存,库存变为99
t2更新redis缓存,库存变为98
t3同步到mysql,库存为98
t4同步到mysql,库存为99

这种思路下mysql中数据与redis中数据就会产生不一致。

  1. 先更新mysql,再同步到缓存
时间线程一线程二
t1更新mysql缓存,库存变为99
t2更新mysql缓存,库存变为98
t3同步到redis,库存为98
t4同步到redis,库存为99

这种思路同样会产生数据不一致的情况。

综上分析,秒杀操作是不能使用redis缓存的(使用分布式锁机制可以避免该问题),而且就算能够使用,该操作并没有对数据库进行写的操作,效率也不会有太大提升。

代码实现

首先我们需要加入maven依赖,如下:

      <!--添加Redis依赖 -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.7.3</version>
        </dependency>

        <!--prostuff序列化依赖 -->
        <dependency>
            <groupId>com.dyuproject.protostuff</groupId>
            <artifactId>protostuff-core</artifactId>
            <version>1.0.8</version>
        </dependency>
        <dependency>
            <groupId>com.dyuproject.protostuff</groupId>
            <artifactId>protostuff-runtime</artifactId>
            <version>1.0.8</version>
        </dependency>

RedisDao的实现

public class RedisDao {

    private JedisPool jedisPool;

    RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);

    public RedisDao(String ip,int port) {
        jedisPool = new JedisPool(ip,port);
    }

    public Seckill getSeckill(Long seckillId){
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String key = "seckillId:" + seckillId;
            byte[] bytes = jedis.get(key.getBytes());
            if(bytes != null){
                Seckill seckill = schema.newMessage();
                //反序列化
                ProtostuffIOUtil.mergeFrom(bytes,seckill,schema);
                return seckill;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        finally {
            if(jedis != null){
                jedis.close();
            }
        }
        return null;
    }
    public void putSeckill(Seckill seckill){
        Jedis jedis = null;
        try{
            jedis = jedisPool.getResource();
            String key = "seckillId:" + seckill.getSeckillId();
            //序列化
            byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema,
                    LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
            //最大超时时间:1小时
            int timeOut = 60*60;
            jedis.setex(key.getBytes(),timeOut,bytes);
        }catch (Exception e){
            e.printStackTrace();
        }
        finally {
            if(jedis != null){
                jedis.close();
            }
        }
    }
}

该部分有两个优化点:

  1. 使用二进制进行存储,相对于其它存储方式,存和取的效率更高。
  2. 使用 protostuff 序列化插件,效率十分卓越。

在Service中对该方法的调用

		//获取秒杀商品的相关信息
        Seckill seckill = redisDao.getSeckill(seckillId);
        //Redis缓存中没有该对象
        if(seckill == null){
        	//从数据库中获取
            seckill = seckillMapper.queryById(seckillId);
            //存入redis缓存
            redisDao.putSeckill(seckill);
        }

执行秒杀操作的高并发优化

简单优化

在这里插入图片描述
我们能够进行优化的就是网络延迟,其中造成网络延迟的重要原因就是锁机制。

当线程一进行秒杀操作时,获取到改商品的行级锁,此时线程二也对该商品进行秒杀,那么他将会进入阻塞状态,等待线程一释放该锁,这样就会花费大量时间。所以我们需要想办法来减少线程对锁的持有时间。

为了减少网络延迟,我们可以更换这两个操作的执行顺序,先插入购买明细再进行减库存。更换顺序后,原先用户获取商品的行级锁之后需要执行两步操作才会释放掉该锁,现在只需要执行完减库存操作就可以把锁释放掉,等待时间会减少一半。

问题:为什么我们只考虑减库存时获取的商品行级锁,不考虑插入购买明细时获取的锁呢?

大量用户在秒杀同一个商品时,减库存需要获取的是一个行级锁,而插入购买明细获取的锁锁的是插入的这一行,不会影响其它购买明细的插入.
在这里插入图片描述

深度优化

进行秒杀操作时,后端需要访问两次数据库,对数据库的大量访问同样会造成网络延迟。我们可以使用mysql的存储过程,将插入购买明细和减库存的操作放入存储过程中,秒杀操作就只需要直接调用该存储过程就可以了。我们可以直接在存储过程中设置事务机制,不再使用spring提供的事务机制。
存储过程的SQL设计如下:

delimiter $$
create procedure `seckill`.`execute_seckill`
(in p_user_phone bigint,in p_seckill_id bigint,in p_seckil_time timestamp ,out result int)
begin
  declare insert_count int default 0;
  /*开启事务机制*/
  start transaction;
  insert ignore into success_seckilled(seckill_id,user_phone,create_time,state)
  values (p_seckill_id,p_user_phone,p_seckil_time,0);
  select row_count() into insert_count;
  if(insert_count = 0) then
    rollback ;
    set result = -1;
  elseif (insert_count < 0) then
    rollback ;
    set result = -3;
  else
    update seckill
    set number = number - 1
    where seckill_id = p_seckill_id and number > 0 and p_seckil_time > start_time and p_seckil_time < end_time;
    select row_count() into insert_count;
    if(insert_count = 0)then
      rollback ;
      set result = 0;
    elseif (insert_count < 0) then
      rollback ;
      set result = -3;
    else
      commit ;
      set result = 1;
    end if;
  end if ;
end $$
delimiter ;

mybatis配置存储过程:

  1. mapper接口中添加抽象方法
	/**
     *直接数据库中进行秒杀操作
     * @param paramMap 参数集合
     */
    void seckillByProcedure(Map<String,Object> paramMap);
  1. 映射配置文件中配置调用存储过程的SQL
	<!--void seckillByProcedure(Map<String,Object> paramMap);-->
    <select id="seckillByProcedure" statementType="CALLABLE">
        call execute_seckill(
          #{userPhone,jdbcType=BIGINT,mode=IN},
           #{seckillId,jdbcType=BIGINT,mode=IN},
          #{seckillTime,jdbcType=TIMESTAMP,mode=IN},
          #{result ,jdbcType=INTEGER,mode= OUT}
        )
    </select>

Service层的调用改动

 public SeckillExecution executeSeckillByProcedure(Long seckillId, Long userPhone, String md5){
        if(md5 == null || !md5.equals(getMd5(seckillId))){
            return new SeckillExecution(seckillId,SeckillStateEnum.REWRITE);
        }
        try{
            Map<String,Object> paramMap = new HashMap<>();
            paramMap.put("seckillId",seckillId);
            paramMap.put("userPhone",userPhone);
            paramMap.put("seckillTime",new Date());
            paramMap.put("result",null);
            seckillMapper.seckillByProcedure(paramMap);
            int result = (int)paramMap.get("result");
            if(result == 1){
                SuccessSeckilled sk = successSeckilledMapper.queryByIdWithSeckill(seckillId,userPhone);
                return new SeckillExecution(seckillId,SeckillStateEnum.SUCCESS,sk);
            }else{
                return new SeckillExecution(seckillId,SeckillStateEnum.stateOf(result));
            }
        }catch (Exception e){
            return new SeckillExecution(seckillId,SeckillStateEnum.INNE_RERROR);
        }
    }

使用存储过程的优缺点分析:为大家推荐一篇写的很好的博文 存储过程优缺点分析

文章最后,我将秒杀系统完整代码的链接给大家:github

相关文章:

  • 神经网络是线性分类器吗,神经网络是线性的吗
  • socket编程
  • 多传感器融合定位技术
  • 2022第十一届PMO大会(线上会议)成功召开
  • 公众号如何运营?教你几招超实用的公众号运营方法
  • C# 第六章『交互式图形界面』◆第5节:FolderBrowserDialog类、DialogResult枚举
  • 氨基化/环氧化/胺化/羧基化/巯基改性/笼空状磺化聚苯乙烯微球相关制备
  • docker-compose安装mysql主流版本及差异
  • 牛客多校3 - Journey(建图,最短路)
  • python如何实现数据可视化,如何用python做可视化
  • 开源治理的基本实践与指导原则
  • 【APP测试】怎么对App进行功能测试
  • Mybatis-Plus复习
  • 8、JAVA入门——switch选择结构
  • Inno Setup 创建Revit安装包
  • Docker 笔记(1):介绍、镜像、容器及其基本操作
  • ES10 特性的完整指南
  • gulp 教程
  • NLPIR语义挖掘平台推动行业大数据应用服务
  • php的插入排序,通过双层for循环
  • PHP那些事儿
  • php中curl和soap方式请求服务超时问题
  • Python语法速览与机器学习开发环境搭建
  • SegmentFault 社区上线小程序开发频道,助力小程序开发者生态
  • 测试开发系类之接口自动化测试
  • 代理模式
  • 计算机常识 - 收藏集 - 掘金
  • 今年的LC3大会没了?
  • 紧急通知:《观止-微软》请在经管柜购买!
  • 开放才能进步!Angular和Wijmo一起走过的日子
  • 聊一聊前端的监控
  • 模仿 Go Sort 排序接口实现的自定义排序
  • 前言-如何学习区块链
  • mysql 慢查询分析工具:pt-query-digest 在mac 上的安装使用 ...
  • #ifdef 的技巧用法
  • #Js篇:单线程模式同步任务异步任务任务队列事件循环setTimeout() setInterval()
  • #我与Java虚拟机的故事#连载17:我的Java技术水平有了一个本质的提升
  • (android 地图实战开发)3 在地图上显示当前位置和自定义银行位置
  • (C语言)输入自定义个数的整数,打印出最大值和最小值
  • (板子)A* astar算法,AcWing第k短路+八数码 带注释
  • (分布式缓存)Redis持久化
  • (接口封装)
  • (强烈推荐)移动端音视频从零到上手(上)
  • (转)MVC3 类型“System.Web.Mvc.ModelClientValidationRule”同时存在
  • (转)项目管理杂谈-我所期望的新人
  • .mysql secret在哪_MySQL如何使用索引
  • .NET Core 中的路径问题
  • .NET6实现破解Modbus poll点表配置文件
  • .Net多线程总结
  • .net反编译的九款神器
  • @31省区市高考时间表来了,祝考试成功
  • @Resource和@Autowired的区别
  • @Transactional类内部访问失效原因详解
  • @拔赤:Web前端开发十日谈
  • [20160902]rm -rf的惨案.txt