https://kaiwu.lagou.com/course/courseInfo.htm?courseId=1394 山海 贝壳资深Java开发工程师,业务负责人
大家好,我是山海。一年一度的“双11”过去,各位电商行业的技术同学肯定已经做好技术复盘,准备迎接“双12”了吧!
因为购物节的用户流量太大,年底这两个月,对于很多技术同学来说是一年中最忙的时间段,加班加点没日没夜。我记得 2020 年天猫“双11”的数据大屏显示零点成交量高达 58.3 万单/秒。这个真的非常高了!
不只是“双11”会出现流量峰值,在平常的商品交易过程中,商家为了在刺激用户购买的同时也控制住成本,总是限制一定数量的商品进行降价,或者在规定的一段时间内让商品的价格降低。因此,大部分商家都会采用秒杀的方式,让消费者在短时间内抢购来完成交易,也会出现流量高峰。
那能撑住这种流量高峰的并发系统是如何实现的呢?对于并发有兴趣的同学来说,哪些地方是容易踩坑的呢?今天我从自己的经验出发,结合秒杀系统的设计案例,来做个分享。
秒杀业务的特点
在正式开始前,我要坦白地说,其实做好一个秒杀系统是非常有难度的,因为这要求我们把系统的每一层架构都做到极致。而且你仅仅做好系统架构的优化还是远远不够的,秒杀系统对于部署的环境、服务器的硬件都有非常高的要求,甚至你的 Java 虚拟机、垃圾回收器都要优化到极高的程度。这些并不是某一个模块或者某一个开发同学就能很好支撑的。
所以对于做业务系统的开发同学,我会从系统使用和优化的角度来讲秒杀,以便使大家对于高并发能有更好的理解和应用。
首先,我们要了解一下需要秒杀的业务。这里插一句,业务思维对于开发来说是非常重要的,无论我们做什么开发工作,都要从具体的业务入手,所以我们要想清楚,一个业务中哪些需求是可以舍弃的,哪些是要必须支持的,哪些可以靠后完成,哪些要立刻完成。
而秒杀业务主要有以下几个特点。
第一点:瞬时访问量特别高。
第二点:并非每个访问量都是有效的。
第一点,大家应该都能达成共识。对于第二点,可能有一些同学不认可或者不理解。为什么我说不是每个访问量都有效呢?因为有一些访问是破坏规则的,它可能是程序模拟的,这种访问就认为是无效的。而且,在实际业务中,有一些访问虽然的确是真正的购物请求,但是“秒杀”本身就有所限制,不可能每个人都能买到,所以我们可以设定一些规则,在前期过滤掉一些访问。这个地方我们后面细说。
第三点:秒杀结果并不一定要即时通知到用户。根据我的经验,在秒杀活动中,晚几分钟甚至十几分钟通知到用户是否成功秒杀到商品,也是能被用户接受的,特别是在活动界面有友好交互的情况下。
第四点:对于秒杀的数量,一定要准确控制。多卖或者少卖都会带来损失,这个时候如何准确地在高访问的情况下完成秒杀数量归零,是非常考验系统设计的技巧和经验的。
第五点:对于支付流程的完成,只要能保持最终一致性就可以,不需要立刻全流程完成,所以这个也是可以用一些设计技巧来实现。
简单说完业务,我们再来了解一下,通常做一个秒杀系统涉及到哪些关键层级,对于每个层级我们又可以做什么样的优化。
分发层(分流)
先来看分流层。分流层往往是解决并发问题的万能手段,我们来说一个极端的场景:假设有一亿并发量,如果我们能分发到一亿个服务器上,那对于每个服务器来说其实也只承担了一个访问量,岂不是非常轻松?当然,这只是一个假设而已,在实际工作中我们如何做好分流,又怎么去更好地支撑并发量,还不造成资源浪费呢?
这你就要听我说道说道了。
首先,你当然不能盲目做,得先做一个流量预估,这一点很自然就会想到。你可以根据平时的用户访问量或者结合过往的秒杀数据,来计算一些系数,预估出这次要秒杀的峰值。
其次,你要注意静动分离。什么叫静动分离?就是指静态资源与动态资源分开存储、分开请求,这样我们就能提前解决掉静态数据的压力,不至于在高并发时给后台增加太大压力。而说到静态资源,就少不了我们的 CDN。
最后,就是请求分流。我们可以根据某个维度,把流量分发到不同的服务器上,让这些服务器承担不同的压力。比如:我们用 IP 来分出区域,让不同区域的请求,落到不同的服务器上,这样就能有效避免所有区域的服务都由于压力大而挂掉。当然,这里有个常见疑问,这样做会不会导致一个服务器挂了,整个区域的服务都挂了?这种担心很有道理,所以我们通常采用 Hash 分流,也可以采用一致性哈希,让每个请求保持高随机性的同时还能分布均匀。
到现在,我们聊的还仅仅是技术上的分流,其实我们在开发上具体实现的时候,还可以做业务上的分流,这点可能你不一定想得到吧!我来用秒杀举个例子。
假如在一次活动中对某个商品上 1000 件秒杀,那我们就可以根据不同的条件将秒杀量分成多份。比如你可以按照省份,给北京 200 件、上海 200 件、深圳 100 件……这样,在你加锁的时候,可以每个省份用到一把锁,而不是全国共用一把锁。这样就能使流量压力大大缓解,锁获取等待时长也会变短,最终在保证精准判断剩余数量的同时,又能支撑更高并发。还有一点,这样做不至于出现所有的商品被其中一个城市全都秒杀到的情况,这一点对于秒杀活动的举办目的来说比较有意义。
网关层(削峰)
再说网关层,这一层会过滤掉绝大部分的请求,只保留少量运气好的请求会到达真正的业务层。比如还是上面那 1000 件秒杀商品,我们只让前 2000 个请求打到后面的业务服务器上,后面再有请求过来我们直接返回“秒杀失败”就可以了。这对用户来说,就是常说的“拼网速”了。当然,你也不一定必须排除掉后来的,也可以使用一些随机策略或者一些判断策略。
你可能会问:为什么让 2000 个请求到业务服务器,1000 件商品秒杀有这个必要吗?这其实是我的经验所得,因为这个过程是一个傻瓜式过滤,可能有一些请求是用爬虫手动请求的,或者用什么工具来模拟的,这样会使秒杀成功率远远高于正常操作。所以,这样的活动通常会有安全部门进行判断,然后过滤掉一部分数据。后续还有可能通过一些随机算法排除掉一部分,这样最终只有一部分访问量能到达业务服务器,所以我们要预留一定的访问量出来。
当然,想要削峰还有一个很常用的手段,这也是一个当我们自己作为用户时会非常讨厌的手段,就是验证码。其实在很多时候,程序本身还是很难判断一个操作是否是真实用户进行的,所以只能让用户来添加验证码或者答题,在最关键的秒杀前操作一下,以确认是否是有效用户。这个手段确实能帮助我们去掉大量的无效请求。而这个验证或者答题的服务一定是单独部署的,仅仅用来完成验证码或者算题的操作,速度会是非常快的。
处理层
处理层的设计和优化往往是业务开发的重头戏,也是最考验我们能力的地方。虽然经过前面的层层过滤,我们会把绝大部分的访问量挡在外面,但如果遇上“双11”这样的多种产品同时进行秒杀的场景,业务层的并发量还是非常惊人的。而且这里会涉及到不同服务之间的调用、不同存储之间的一致,所以整个系统设计是非常难的,那我们应该如何做呢?提醒你以下几点。
- 一些常用的、而且是不大的数据,一定要放在本地缓存里,比如城市名称、编码,或者一些类别。这里一定要注意,不能是特别大量的数据,否则服务器可能会产生内存溢出。但如果你不放数据,又会导致这些常用数据被频繁加载、销毁。所以,对于这部分数据要预先做一个考量和梳理。
- 把一些需要分布式读取的数据放在共享内存里,如 Redis,而这类数据里最重要的就是商品数量,因为它是需要多台服务器加锁,需要更新,而且需要被大量同时读取的,所以这些数据一定要放在共享内存里,并且一定要最小粒度加锁。
- 用异步来处理一些比较重的逻辑,避免一些不必要的串行时间。但这里最好使用线程池处理,一方面避免线程耗尽;另一方面也减少线程过多用不尽的情况。在业务层的请求逻辑最好是能做到异步处理,而不是让访问请求一直保持着,避免链接过大,服务崩溃。
- 使用队列来完成最终的一致性,而不是实时的一致性。在最终处理完成后,通知浏览器,让用户知道是否秒杀成功,以及其他信息。
存储层
存储其实也是支持高并发的关键一环。现在很多互联网公司的服务器都使用固态硬盘,这会让程序在不变的情况下,运行性能得到大幅度的提升。Redis、MySQL 等常用的存储都支持分布式部署,打破了内存大小与硬盘大小的限制。而且现在存储基本都是部署在云平台上,支持动态扩容与缩容。这些特性很好地支撑了秒杀这种场景的实现。
也由于现在能支持大容量存储,很多时候我们可以用空间换时间,也就是说,在许多情况下,我们可以用内存与硬盘同时存储,通过冗余一些数据存储来提高效率,这样虽然空间上有一定的浪费,但是可以大大节约时间成本。而有的时候由于系统过于庞大,我们不得不远程获取一些固定的信息,如人员信息等,为了减少这种开销,我们更愿意把一些不容易变更的数据存储本地,用空间本地代替远程,一方面可以提高效率,另一方面还可以降低远程调用的风险。
总结
到这里,我们把秒杀系统的关键层次说完了,不知道你对于秒杀还有什么困惑?
其实总结看来,秒杀的基本解决思路和我们经历过的其他高并发场景的思路非常类似,但它最大的特点就是瞬时访问量要比正常值要高出特别多,而与此同时,也由于它的特性,我们可以舍弃一部分访问量,让这个特别大的请求能得以实现。
那从秒杀到高并发,你主要记住分流和削峰、延迟返回、以空间换时间 4 个常用方法,如果你能把这 4 种方法灵活运用于各种场景,无论工作中遇到多大的访问量,其实都可以迎刃而解。重要的是,你需要结合具体的业务场景来进行考量,看这个场景对于一致性、时效性有什么要求。
有过秒杀经历的同学也可以把自己工作中遇到的场景发在评论区,大家一起讨论看看。