秒杀系统设计解密

高并发系统设计(秒杀系统)

有幸参与过大型电商的秒杀抢购抽奖系统设计开发,面对过150W每秒的高并发业务场景,所以对类似场景的一些细节还是比较清楚的。这里就给大家介绍下秒杀抢购抽奖系统的一些设计细节和解决方案。



一、介绍

电商领域有很多秒杀的场景,什么是秒杀场景呢?简单的讲就是购买的人数远远大于商品的数量。比如双11的9块9秒杀iphone,1499抢购茅台等等。那秒杀场景有哪些问题挑战呢?高并发、业务隔离、网络带宽的突增、库存的扣减。通过以下的详细介绍,我们来一一解决上面的问题。

1.整体架构

在这里插入图片描述

从上图的我们看到负载层,我们可以使用硬负载和软负载。在应用层,我们水平拆分,形成独立的微服务。而数据持久层,使用的是redis,数据库使用的mysql。

2.系统架构

在这里插入图片描述

我们将秒杀系统拆分成四个主要部分:静态资源,秒杀前台系统,秒杀中台系统,秒杀后台系统。静态资源:css、js等静态文件。可以直接在cdn层面缓存的。前台系统:对商品详情页面静态化处理,动态数据是通过接口从服务端获取,实现前后端分离,静态页面无需连接数据库打开速度较动态页面会有明显提高。同时前台提供restful服务,供前端调用。提供独立的RPC服务,整合后端业务依赖服务。后台系统:主要负责数据异步持久化、数据统计监控等。

二、设计原则

在这里插入图片描述
图中是我们秒杀业务场景的瞬时流量。试想,如果图中的峰值流量我们全部透到应用业务逻辑,后果是不堪设想的。

1.缓存

缓存的主要目的是:将一些相对静态数据缓存起来。缓存的层级很多:CDN、JVM缓存、redis等等。
在这里插入图片描述

浏览器缓存和CDN

浏览器缓存:当你请求 HTTP 请求后收到一个 HTTP 响应体的时候,浏览器会判断响应头中是否有缓存的标识,如果有,则会把请求内容存入硬盘中(或者内存中),下次的准备发起相同请求时浏览器会自行判断缓存内容是否有效,如果有效则不进行请求,直接获取缓存内容。
CDN缓存:秒杀商品详情页静态化。实际也就是,将动态页面伪静态化存储CDN,让用户直接访问。这样的好处如下:
1、大大提升访问速度,不需要去访问数据库,或者缓存来获取哪些数据;
2、搜索引擎更便于抓取,搜索引擎SEO排名更容易提高;
3、稳定性更好,如果后端应用或者数据库出了问题,会直接影响网站的访问,而静态网页就避免了如此情况。
CDN回源缓存的设置
cache control是服务端告诉客户端本次响应response可不可以缓存,以什么样的策略去缓存;
private:客户端可以缓存/默认设置;
public:客户端和代理服务器都可以缓存;
max-age = xxx:缓存的内容将在xxx秒后失效;
no-cache:强制向服务端再验证一次;
no-store:不缓存请求的任何返回内容;

分布式缓存
redis是一个分布式缓存系统。其支持多种丰富的数据结构。
使用redis也会遇到一些问题:
一致性问题。当然一致性问题可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存。
缓存穿透和缓存雪崩问题。缓存穿透,即故意去请求缓存中不存在的数据,导致所有的请求都回源到数据库上,从而数据库异常。解决方案:一、利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。二、采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。我们也可以做缓存预热,就是商品提前加载到缓存中。缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都回源到数据库上,从而导致数据库异常。解决方案:一、给缓存的失效时间,加上一个随机值,避免集体失效。二、使用互斥锁,但是该方案吞吐量明显下降了。三、多级缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作。当然我们还可以将数据写入hbase,由hbase再做一级缓存。

JVM本地缓存
大家都知道,redis和我们的应用绝大部分情况是在不同的虚拟机上的,这里应用层读取redis时,就需要跨虚拟机,其中会涉及到网络开销等。这些都会对服务有影响。
此时,我们就需要引入jvm缓存。就是将一些相对静态的数据,如:商品名称、商品卖点等信息,缓存在应用中。这里,我们可以采用ConcurrentHashMap来实现。但是我们的jvm内存是有限的,只要内存中对象稍微管理不当,都会引起应用的fullgc等问题。我们可以使用一些开源的本地缓存,如:Guava Cache。
Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。在某些场景下,尽管LoadingCache 不回收元素,它也是很有用的,因为它会自动加载缓存。Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。

public void putCache(String key, String cacheKey, String json, long timeOut, long maxNumSize) {
        if (StringUtils.isBlank(json)) {
            return;
        }
        try {
            Cache<String, String> cache = CACHEMAP.get(key);
            if (cache != null) {
                cache.put(cacheKey, json);
            } else {
                synchronized (TransactionJVMCache.class) {
                    if (CACHEMAP.get(key) == null) {
                        Cache<String, String> reCache = CacheBuilder.newBuilder().expireAfterWrite(timeOut, TimeUnit.SECONDS).maximumSize(maxNumSize).build();
                        CACHEMAP.put(key, reCache);
                    }
                }
            }
        } catch (Exception e) {
            LOGGER.error("TransactionJVMCache.putCache.exception", e);
        }
    }

2.削峰

削峰的主要目的是:稳定服务端,节约服务器成本。本质就是错开用户请求,减少或过滤用户无效请求。

消息队列削峰
要对流量进行削峰,最容易的解决方案就是用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息消费。
消息队列有很多种方式,如:redis(list、zset、set数据结构实现)、MQ(ActiveMQ、RabbitMQ、RocketMQ等)、kafka。这些方式优缺点这里就不多介绍了。
那消息队列有什么特点呢?
1.异步性
将同步操作,通过以发送消息的方式,进行了异步化处理。减少了同步等待的时间。如:库存的扣减、订单的创建、短信验证码的发送等。
2.松耦合
消息队列减少了服务之间的耦合性,不同的服务可以通过消息队列进行通信,而不用关心彼此的实现细节。
3.横向扩展
通过对消费者的横向扩展,降低了消息队列阻塞的风险,以及单个消费者产生单点故障的可能性(当然消息队列本身也可以做成分布式集群)。

3.限流

秒杀系统里面多处采取了流控手段。

1、使用nginx+lua实现分布式限流。我们可以采用最基本的计数器算法。

--限流计数
function limit_url_check(key, s, m)
    local localKey = key;
    local yyy_limit = ngx.shared.url_limit
    --每分钟限制
    local key_m_limit = localKey .. os.date("%Y-%m-%d %H:%M", ngx.time())
    --每秒限制
    local key_s_limit = localKey .. os.date("%Y-%m-%d %H:%M:%S", ngx.time())
    local req_key_s, _ = yyy_limit:get(key_s_limit);
    local req_key_m, _ = yyy_limit:get(key_m_limit);

    --每秒处理
    if req_key_s then
        yyy_limit:incr(key_s_limit, 1)
        if req_key_s > s then
            return true
        end
    else
        yyy_limit:set(key_s_limit, 1, 60)
    end

    --每分钟处理
    if req_key_m then
        yyy_limit:incr(key_m_limit, 1)
        if req_key_m > m then
            return true
        end
    else
        yyy_limit:set(key_m_limit, 1, 85)
    end
    return false
end

2、应用层流控。JVM的负载应该在可控范围之内,对于JVM承载能力之外的请求,应该被合理管理。我们通过Filter过滤器,使用原子操作类AtomicInteger实现多维度单机流控。

在这里插入图片描述

限流的维度有很多种,如:单位时间内用户、uri、userAgent、ip等维度。限流的范围也可以多种,如:集群限流、单机限流。

4.降级

所谓“降级”,就是当系统容量达到一定程度时,限制或者关闭部分功能,从而将有限的资源供秒杀核心功能使用。如:商品详情页里,推荐相关联商品,直播关联展示等。这些我们都可以通过开关系统来实现降级。或者当大批量请求进来,已经超过承载能力,可以随机返回抢购失败等等。

5.超卖

超卖也就是如何设计活动库存的扣减。
在这里插入图片描述
步骤4中就是超卖的具体体现,如何解决呢?
在这里插入图片描述

下单扣减库存。也就是用户在提交订单的时候,就将库存扣减。但是如果在抢购开始时,非正常用户瞬时都提交订单,这样商品就立刻售罄,正常用户都无法购买。但是,到一定时间后,这些非正常用户又取消订单,这时商品又恢复购买。但是正常用户已经不关注此活动商品了。
付款扣减库存。也就是用户在付款的时候,才将库存扣减。这种会存在很多用户都能提交订单,但是付款时,提示库存不足。这种用户体验肯定是不能容忍的。
占用库存、扣减库存。用户在提交订单的时候,占用库存。在付款的时候,扣减库存。占用库存是有时长的,就像我们在抢火车票的时候,必须在15分钟内付款,如果规定时间内没有付款。订单将被取消,占用的库存就会释放。当然,用户在付款的时候,会先校验占用的释放存在,不存在再执行占用逻辑。
针对库存的扣减,我们可以采用悲观锁、乐观锁、Sync加锁。但是我们的背景是秒杀系统。这是一个高并发的系统,以上方式肯定是不行的。这里我们可以采用redis来实现。我们知道的,redis是有几种原子性操作的,比如:incrby、decrby。但是一旦缓存丢失需要考虑恢复方案。如果我们数据层是通过MQ异步持久化的话,消息消费完了才能重启redis初始化库存,否则也存在库存不一致的问题。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值