秒杀系统的搭建

秒杀系统的搭建思路

在如今这个流量就是金钱的时代,秒杀活动无疑还是一个非常吸引流量的活动,各大电商平台都会推出类似双11这样的大促活动,那么如何保证这种高并发情况下的秒杀系统的正常运行,就变成了程序员需要重点考虑的一个问题了。

一、构建思路:

其实,针对秒杀活动的高并发,其实可以分为 大量的并发读请求和大量的并发写请求。我们也就是要针对这两类请求来进行针对性处理。

但是,这两类请求也有一些共同的处理思路,那就是4要1不要。

1.数据量要尽可能的小

不管是读请求还是写请求,其实都是针对数据来进行操作的。而数据的传输是会消耗时间的,不管是网络中的数据传输,还是我们服务器中的数据传输。而我们在服务器中对数据进行的处理是非常消耗CPU的。

而数据一般都是会与数据库进行打交道的,这又涉及到了序列化与反序列化,这也是一个非常消耗CPU的,所以,保证数据尽可能的小,数据格式尽可能的简单,与数据库打交道尽可能的少。

2.请求数量要尽可能的少

这里说的请求数量不是只 用户访问我们服务器的请求数量,而是用户一个访问请求,我们自己渲染页面所需要的请求数量,比如说访问页面的时候后端接口的调用,页面依赖的资源的加载,Ajax请求等。 而每一个请求无疑都是会消耗资源的,所以请求数量要保证尽可能的少。

3.请求路径要尽可能的短

这里说的路径是指的是 一个请求过来,我们处理请求到返回要经过的节点。

一般来说,一个请求进来,经过节点的时候都要创建一个连接,这无疑就降低了系统的可用性。比如每个节点不发生故障的概率是99.9%,那么经过5个节点之后,这个请求不发生故障的概率就会降为99.9%的5次方,约等于99.5%。 而且数据在不同节点的传输中也会影响效率(序列化与反序列化)。

因此要保证请求路径尽可能的短。

4.依赖要尽可能的少

这里说的依赖是说的强依赖,也就是说我们处理请求的这个服务强依赖于其他服务,这就造成了整个系统的可用性的降低。

我们可以根据业务逻辑来将不同的服务独立开来,并设置分级。上级服务尽量减少对下级服务的强依赖,这样可以防止重要的服务被相对而言不重要的服务拉胯。

5.不要有单点

这个就很好理解了,单点的弊端大家都清楚,如果真有一个服务是单点的,那么如果这个服务发生异常或者宕掉了,那么无疑会引发大范围的事故。

而且我们现在大部分都是 分布式系统了,也基本不存在单点这个问题了。

而这4要1不要也是要根据实际情况来进行取舍的,有时候会针对具体情况进行定制化。

二、数据

并发量大,瞬时流量大是秒杀的一个特点。那么瞬时的数据 读写操作也会相应的大幅度增加。

在处理数据的时候,我们要分为 读,写两个操作来进行不同的处理。

1.并发读

要解决并发读的问题,就需要先进行判断出来秒杀时或者说是高并发时,用户要读取的数据到底是什么,

也就是一个热点数据的发现行为。

我们可以通过多种方式来进行热点数据的发现。

①采用类似强制性手段,谁要进行秒杀活动,或者什么商品要参加秒杀,这些都需要卖家进行报名,这样我们就可以在业务层面首先确定哪些商品可能会成为热点数据。

②采用数据分析,提前分析出来哪些商品在这次活动中受欢迎的概率大一些,可以进行针对性的热点处理。

③采用动态热点分析,我们可以构建一个异步系统,在整个秒杀活动的链路上,提前进行热点数据的分析,比如说采集首页上面的搜索量 或者商品的热度值,可以传入到分析模块进行分析,分析出结果后通知我们下游服务,下游服务针对性的进行热点缓存。

通过发现热点数据,并将之放入到缓存当中,可以提高高并发情况下的读请求的效率。

针对热点数据,我们要分情况的对其进行 优化、限制、隔离。

优化的情况我们需要考虑这个热点数据的组成, 看看他是否实现了动静分离,如果有的话,可以将静态资源长期缓存,而动态资源我们可以设置一个较短的过期时间,因为我们知道,热度也是会不断变化的。这里可以使用LRU淘汰算法(淘汰掉热度最低的数据)来淘汰掉原来的数据。

限制其实也是对服务的一种保护,我们可以通过多种方式实现将热点数据单独的限制在一个处理队列里面,这样就可以防止 大量访问热点数据的请求过来访问热点,抢占了所有的资源,而导致访问其他数据的请求阻塞。

隔离其实就是将整个秒杀模块独立出来。包括服务及数据库。 将秒杀模块独立出来的话,相关的请求就会进入我们给他专门搭建的集群中,从而跟普通请求分离开来,而且数据库的隔离,也可以减少其他模块因为秒杀活动开始之后造成的数据库的访问压力。

目的都是为了防止因为较小的数据(请求) 影响全部的数据(请求)。

其实说到并发读,大多数人想的都是利用缓存来进行,但是其实还有一种思路就是 采用 服务器应用层的LocalCache(可以使用ConcurrentHashMap),可以用来存放一些经常被读取但不经常修改的数据。因为单台缓存机器最大也就是职称30W/S的请求,而直接使用Java 的LocalCache的话就直接看服务器的性能了。

2.并发写

而秒杀模块的并发写操作其实就是一个下订单(一般的话,秒杀的商品是不会放入购物车中)。这就涉及到了我们秒杀活动中不可避免的一个 库存的问题 。

什么时候锁库存,如何锁库存,如何防止超卖这也是秒杀的难点。

一般而言,现在就锁库存有三种方式:

①下订单锁库存
②付款之后锁库存
③预扣库存

①下订单锁库存:用户下了订单之后,就进行库存中减去订单中的商品数量。这种方式是实现锁库存的最简单的方法也是最安全的方法,一定不会发生超卖情况。但是,你要知道,并不是下了订单就要付款的,正常用户的话是会正常购买,但是如果有其他竞争对手进行恶意竞争,就可能会导致大量下订单又不付款的情况。当然针对这个情况我们也有相应的风控进行前序的风控判断,但是无疑也是增大了业务的复杂度。

②付款之后锁库存:用户下了订单之后,并不会去扣减库存,而是在用户支付成功之后再去扣减库存。这样可能出现的问题就是 用户在下了单之后 ,商品卖光了,那么用户就不能去继续进行支付了。甚至在高并发情况下,会出现商品超卖现象。

③预扣库存:这种的实现就会稍微麻烦一些,用户下单之后,我们会为用户创建一个过期时间相对较短的订单(根据不同的业务来),在订单未过期期间,我们为其保留一个库存,过期之后,我们将库存释放,其他用户可以继续购买。用户在下单之后去付款的时候,我们再次进行校验状态,包括库存数量,秒杀商品的状态以及限购量等,如果通过之后就可以付款成功。如果库存不足的话,就提示用户秒杀失败。

虽然第三种看起来很美好,但是实际上还是没有解决问题,因为你依然为用户保留了一个过期时间,如果存在恶意攻击的话,依然可以在这个过期时间内刷空你的库存。

那么实际应用中,那种方案更好呢?

在实际中,因为下单减库存比预扣库存以及付款之后锁库存的实现以及逻辑更加简单,所以采用这种方式的比较多。同时为了防止超卖,一种是在应用程序中通过事务来判断,即保证减后库存不能为负数,否则就回滚;另一种办法是直接设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时会直接执行 SQL 语句来报错;再有一种就是使用 CASE WHEN 判断语句,例如这样的 SQL 语句:

UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END

还有一种方法就是,库存量就是一个热点数据。我们要提前将之放入到缓存当中,进行缓存预热。

而针对这个库存量,其实读的时候是不需要强一致性的,只有写的时候是要求强一致性的。

我们可以将 库存的 写操作直接放在缓存中进行操作,当然这个缓存必须要支持持久化,我使用的是redis,我们可以采用redis 的list数据结构(队列),来左放右取,从而防止超卖。

当然这是一种非常简单的秒杀才会使用到的。如果你的业务逻辑很复杂,也就是扣除库存的逻辑很复杂的话,那我们就不能在缓存中进行处理了,而是要对数据库进行操作了。这个时候就需要上锁及事务啦。

因为前面讲过,我们可以讲秒杀系统单独提出来,然后数据库也是单独的,那么 就不存在因为秒杀的请求而阻塞其他正常请求数据库的问题了。但是 这还是没有解决Mysql 的行级锁的问题,这个时候我们就需要在 应用层面或者 数据库层面进行排队处理了。

①应用层面:我们可以以商品id为标识,进行分队列操作,这样就可以避免大量请求去访问同一行造成的请求阻塞,同时也可以控制链接到数据库的连接数,防止过度占用资源。

②数据库层面:应用层面只能保证单台服务器内的请求排队了,对于集群而言,效果不大。这时候就需要第三方补丁来帮忙实现 数据库层面的排队了。 推荐alisql。

三、优化

1.优化点

现在基本流程已经跑通了,那么我们该考虑一下如何进行优化 或者说 可以从哪些方面考虑优化秒杀这个模块

其实不同的设备对“性能”这个的定义也是不一样的,比如说CPU的性能主要看主频,磁盘主要看IOPS等等。

而针对我们秒杀模块而言,就是针对服务器,QPS(每秒访问数)和RT(响应时间)应该是最重的指标。\

一般情况而言,RT越短,QPS也就越高。而多线程的情况下就还需要考虑线程数了。

总QPS=(1000ms/RT)*线程数量

RT一般是由CPU执行时间和线程等待时间组成。经过大量实际的测试发现,减少线程等待时间对提升性能的影响并没有很大。

线程数量的话 也不是越多越好,因为线程越多,切换线程的成本就会越大,且线程本身也是会耗费一部分内存的。

有个经验公式就是如果是 IO密集型 线程数量 = CPU + 1

如果是CPU密集型 线程数量 = (2*CPU)+1

出去这个配置 还有一个最佳实践公式

线程数=[(线程等待时间+线程CPU时间)/线程时间]* CPU数量

但是具体的线程数还要根据具体的性能测试来确定。

2.发现瓶颈

秒杀模块,他的瓶颈更多的是发生在CPU上面的。我们可以使用JProfiler进行观察,查看整个请求中每个方法的CPU执行时间,就可以发现那个方法消耗的CPU时间最多,以便针对性的做出优化。

当然还有其他方面也会成为瓶颈,具体问题还要具体分析。

最简单的就是当你的QPS到达上限的时候,看看你的CPU 使用率是否达到了95%,如果没有的话,那就是其他地方存在瓶颈,比如说锁限制或者是IO限制。

3.如何优化

①减少编码,因为Java的编码运行是比较慢的,如果涉及到字符串的操作,都会进行将字符转为字节,这个时候就要查表,然后编码,大大影响性能。

所以减少编码,直接使用流进行传输数据,可以大大提升性能。

②减少序列化,同样,序列化也是非常消耗性能的。但是如果需要不同服务之间的调用的话,就要使用序列化。那么可以将两个原本部署在不同机器上的服务部署在一台tomcat中,就能避免序列化。

③极致优化,抛弃MVC框架,直接在一个业务层直接将业务处理完,省却处理逻辑。

④并发读优化,我们允许读的场景读到一些脏数据,然后写的时候能保证最终一致性即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值