秒杀系统的特点
- 高性能:秒杀涉及到大量的并发读和并发写,因此支持高并发访问这点非常的关键。
- 一致性:秒杀是有限数量的商品在同一时刻被很多倍的请求同时来减库存,在大并发更新的过程中都要保证数据的准确性。
- 高可用:秒杀时会在瞬间涌入大量的流量,为了避免系统宕机,保证高可用,需要做好流量限制。
秒杀系统的优化思路
后端优化
- 限流:秒杀系统往往是多于商品数量数倍的请求来抢购,我们可以屏蔽掉无用的流量,允许少部分流量走后端。
- 削峰:秒杀请求在时间上高度集中于某一个时间点,随时的流量很容易压垮系统,因此需要对流量进行削峰处理,缓冲瞬时流量,尽量让服务器平缓的处理这些请求。
- 异步:将同步请求转化为异步请求,来提高并发量。
- 利用缓存:创建订单时,每次都需要先查询判断库存,只有少部分成功的请求才会创建订单,因此可以将商品信息放在缓存中,减少数据库查询。
- 负载均衡:利用Nginx进行负载均衡,减轻单个服务器的压力。
前端优化
- 限流:前端答题或验证码来分散用户的请求。
- 禁止重复提交:限定每个用户发起一次秒杀之后,需要等待才可以发起另一次请求,从而减少重复的用户请求。
- 本地标记:当用户成功秒杀到商品后,禁止用户再次提交请求。
- 动静分离:将前端静态数据直接缓存到离用户最近的地方,比如用户浏览器、CDN或者服务器的缓存中。
防作弊优化
- 隐藏秒杀接口:为了避免在秒杀开始之前被用户发现刷接口,因此需要用户在没到秒杀开始不能获取秒杀接口,只有秒杀开始了,才返回秒杀地址url和验证MD5,用户拿到这两个数据才可以进行秒杀。
- 对僵尸账号限制:对于一些僵尸账号,可以检测账号的活跃度或者等级信息来进行限制。当然也可以通过用户画像限制僵尸号。
如何做好限流
在应对秒杀,大促销等高性能压力的场景时,为了保证系统的平稳运行,必须针对超过预期的流浪,通过预先设定的限流规则选择性的对某些请求进行限流“熔断”。
下面就介绍一下,我了解到的一些限流算法。
计数器算法
通过一个计数器counter来统计一段时间内请求的数量,并且在指定的时间之后重置计数器。该算法实现简单,但是会出现临界问题。
比如,我们的限流规则是每秒不超过100次请求。假如,第一个1s的时间窗口内,请求集中到最后的10ms内,在第二个1s的时间窗口内,100次请求都集中在最开始的10ms内,那们实际上在短短的20ms内就集中的200次请求,那么很能就会压垮系统。
计数器限流算法的Redis Lua实现:
1 |
|
滑动窗口算法
滑动窗口算法是计数器算法的一种改进,将原来的一个时间窗口划分为多个时间窗口,并且不断向右滑动该窗口。流量经过滑动时间窗口整形之后,可以保证任意时间窗口内,都不会超过最大允许的限流值,从而流量曲线回更加平滑,可以部分解决上面提到的临界突发流量问题。
但是基于时间窗口的限流算法,只能在选定的时间粒度上限流,对选定时间粒度内的更加细粒度的访问频率不做限制。
令牌桶法
令牌桶法的工作流程如下;
- 如果在t秒内限制请求的数量为n,那么每过t/n秒向桶内添加一个token。
- 如果令牌桶内的token的数量超过限制b,那么多于的token会被抛弃。
- 每次请求进入之时,需要先尝试从令牌桶中拿token,只有拿到了token才会处理接口请求,否则进行限流处理。
基于Redis Lua的令牌桶限流算法实现:
1 | -- 令牌的唯一标识 |
漏桶法
相比于令牌桶算法,漏桶法对于去令牌的频率也有限制,要按照t/n的固定速率来取令牌。
限流规则的合理性
限流规则包含三个部分:时间粒度,接口粒度,最大限流值。时间粒度的选择尤其重要。比如我们可以选择1秒钟不超过1000次,也可以选择10毫秒不超过10次。虽然看起来一致,但是实际的效果却大不系统。比如1秒钟不超过1000次,但是很可能1000次请求就集中在几毫秒内。但如果选择10毫秒不超过10次,会导致误杀许多不应该限流的请求。因此时间粒度的选择要根据实际情况,灵活调整。
如何利用好缓存
缓存可以解决哪些问题
提升性能
在绝大多数的应用中,查询数据库都是非常耗时的。而很多时候其实都是读多写少的,这个时候正确的使用缓存可以极大的提升系统的性能。缓解数据库压力
当用户请求增多的时候增多时,数据库的压力将大大增加,通过缓存能够大大降低数据库的压力。
因此我们可以看出,缓存使用于那些读多写少的情况。还使用于一些对性能要求高的场景,比如秒杀。
缓存的三种模式
Cache Aside 更新模式
这种工作模式是比较常见的工作模式了。
其具体的流程是:
- 失效:应用程序先从cache中取数据,如果没有拿到,则从数据库中取数据,成功后放到缓存中。
- 命中:应用程序从cache中取数据,取到后返回。
- 更新:先把数据存到数据库中,成功后再让缓存失效。

注意点
- 先更新数据库,再更新缓存可能会出现并发写操作导致脏数据。这种方法其实是实际应用中推荐的,但是理论上仍然存在问题。假如,有两个线程再同时并发的进行更新,先更新数据库的反而后更新缓存,那么就可能导致缓存中的数据是脏数据。
- 先更新缓存,再更新数据库,这个逻辑是错误的,因为并发的读和写可能导致脏数据,假如,有两个线程同时并发的进行更新,一个线程删除了缓存,此时第二个线程来读取缓存,没有命中,然后从数据库中取出老数据,并更新回缓存。这个时候第一个线程也把数据库更新了。这个时候缓存中数据就是过期的就数据了。
Read/Write Through更新模式
在Read/Write Through更新模式中,应用程序只需要维护缓存,数据库的维护由缓存来代理。

这种模式相较于 Cache Aside模式,缓存的维护工作不再由调用方负责了,而是由缓存服务自己来加载。
Write Behind Caching 更新模式
Write Behind Caching更新模式就是在更新数据时,只更新缓存,不更新数据库,缓存会异步的批量的更新数据库。这样的好处在于可以合并多次更新,是直接的内存操作。但是问题在于,数据不再是强一致的,而且可能丢失。
