一个秒杀系统的结构设计

阿里一位大牛曾经说过,应用性能拓展的三要素是:缓存,异步,批处理

秒杀业务在如今的电商平台中十分常见,如淘宝双十一秒杀,Nike官网的秒杀等等。同时,也是考验程序员架构能力和综合知识掌握的一个重要部分,本次博客根据笔者多年代码经验就秒杀业务讨论一下它的架构设计

秒杀面对的问题

1. 高并发下单请求

秒杀系统的一个重要特征就是在一瞬间的高并发下单请求,此时,一个Tomcat是扛不住上万QPS的。此时我们可以设置分布式集群,Nginx负载均衡,Redis缓存,流量削峰等等手段方法来迎接这么一个十万百万级别QPS的高并发请求

2. 盗刷接口

譬如去年中秋节在网上疯传的阿里中秋月饼事件

秒杀活动最怕的是刷单问题,脚本刷单会造成大量频繁一致的请求,同时还会造成秒杀活动的不公平性。防止这种不断发起的重复请求,我们可以在前端设置请求间隔,譬如1s内不能多次点击,后台对同一身份进行令牌请求限制。或者设置动态变换的接口

3. 订单超买

其实超卖或者少卖是和业务逻辑有关的,有些促销活动可以接受超买,有些促销活动不接受超买。在本次秒杀系统设计中,我们设置的业务逻辑是不可超买

在高并发的情况下,可以通过在数据库中设置隔离级别,乐观锁等等方法实现

秒杀业务的设计方案

1. 秒杀业务分布式水平扩展

业务性能达到瓶颈,此时最简单的就是再增加一台一模一样的后台服务器,进行分布式处理。然后使用nginx作为静态服务器,进行负载均衡(常用算法如轮询)和反向代理

  • 通过tomcat-log来记载nginx的访问日志

  • 保证nginx和后台服务tomcat的长连接

  • Nginx主要是通过epoll多路复用模型,master worker进程模型和协程机制达到高性能的要求

2. 查询缓存之Redis缓存

众所周知,当缓存离用户越近,效果越好,但同时,代价也越大。首先我们可以将商品数据存储在Redis缓存当中

  • redis缓存共分为单机版,sentinel哨兵模式和cluster模式
  • 在单机版中,我们可以把商品详情存入redis中,当修改商品或者下单减库存以后对该商品详情进行更新
  • 同时,还要设置每一个key值的过期时间
3. 查询缓存之nginx缓存
  • 有两种缓存方式,一种是nginx proxy cache,另一种是nginx lua cache
  • 其中,第一种是将后台的数据通过nginx存入文件中,这样其实性能比较差
  • 第二种通过nginx lua dic可以将数据放入内存中,增加效率,也可以通过nginx lua redis直接到redis中把数据读取出来
4. 查询缓存之页面静态化
  • nginx对静态文件的优化不是特别好,我们可以将静态页面放到CDN(Content Distributed Network)中
  • 当用户发出DNS请求时,发送到DNS服务器,DNS服务器查询到是CNAME地址,然后把DNS请求解析发送给CNAME服务器进行DNS解析(CNAME地址就指向阿里云的服务器),然后CNAME服务器会找到用户IP 的归属地,然后交给用户归属地的服务器查看是否有请求的静态文件,有的话就进分发,没有就交给我们本来的nginx进行解析,把静态资源发送给用户
  • 我们可以将请求后的json数据也作为和js,css,html,image一样的静态文件保存下来
  • 也可以通过phantomjs将页面请求后渲染的HTML保存下来,在服务端完成html,css甚至js的load渲染成纯HTML文件以后直接以静态资源部署在CDN上
5. 下单预减库存异步处理
  • 在处理了静态查询页面之后,我们还需要对下单操作进行优化
  • 最常见的方式是使用redis预先减库存,然后把库存消息通过消息中间件如RocketMQ把消息异步持久化到数据库中
  • 同时,引入RocketMQ的Transaction机制,其中的sendTransaction用到了两阶段提交
  • 为了保证checkLocalTransaction()可以check到响应的状态,我们需要LogData(指订单操作流水)因为订单流水的单独行锁和商品ID的行锁热点不同,所以订单流水的行锁更新可以接受
  • 同时,当库存售罄以后,我们可以在redis中打上标记,直接对下单请求抛出“库存不足”异常,减轻数据库的压力
6. 下单操作之SQL优化
  • 如果使用数据库update做隐式加排他锁时:

    update item_stock set stock = stock - {amount} where item_id = {itemId} and stock >= {amount}
    

    我们需要注意在Innodb中,把item_id设置为索引,这样会把表锁变为行锁

  • 同时,我们也可以在读未提交的隔离级别中使用乐观锁:

    update item_stock set stock = stock - {amount} and version = {version} + 1 where item_id = {item_id} and version = {version}
    
7. 流量削峰之令牌桶
  • 所谓令牌桶算法的基本思路是:每个请求尝试获取一个令牌,后端只处理持有令牌的请求,生产令牌的速度和效率我们都可以自己限定,guava提供了RateLimter的api供我们使用
  • 我们还可以在发放令牌时加上验证机制,如身份,商品、活动是否存在,以此来防止盗刷
8. 流量削峰之队伍泄洪
  • 这里我们使用队列的方式,其实和令牌桶的处理有相似之处

  • 开一个线程池,根据配置自动读取线程池核心线程数,一次只能并发执行核心线程数个请求。其余请求阻塞在队列中

    threadPoolExecutor = new ThreadPoolExecutor(20, 40, 1L, TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(100),
                    new ThreadPoolExecutor.CallerRunsPolicy());
    threadPoolExecutor.submit(() -> {
        String orderLogId = orderService.initOrderLog(orderDTO.getItemId(),orderDTO.getAmount());
        if (!mqProducer.transactionAsyncReduceStock(orderDTO,orderLogId)) {
            throw new ReturnException(EmReturnError.UNKNOWN_ERROR, "下单失败");
        }
        return null;
    });
    
9. 服务器的配置
  • 首先,我们可以配置Tomcat的连接

    server.tomcat.accept-count:100 #等待队列长度
    server.tomcat.max-connections:10000 #最大可被连接数 1000
    server.tomcat.max-threads:200 #最大工作线程数  800
    server.tomcat.min-spare-threads:10 #最小工作线程数  100
    
  • 其次,我们可以配置cache-control对页面静态化进行缓存回源设置

  • 同时,也可以配置Mysql的一些设置

    max_connection = 100
    innodb_file_per_table = 1
    innodb_buffer_pool_size = 1G
    innodb_log_file_size = 256M 
    innodb_log_buffer_size = 16M # 日志发生切换时,仍然有缓冲可以写日志
    innodb_flush_log_at_trx_commit =2 #只调用write方法写入linux的cache中,每隔1s后调用flush方法
    							   =1 #只要事物提交,立刻把log刷盘
    							   =0 #每隔1s,flush到磁盘中
    innodb_data_file_path=ibdata1:1G;ibdata2:1G;ibdata3:1G:autoextend #mysql文件分区
    

结构图

此处的架构没有考虑分布式和CDN
在这里插入图片描述

发布了100 篇原创文章 · 获赞 142 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/coder_what/article/details/104048637
今日推荐