服务器开发技术、方法与实用解决方案
秒杀活动开始前,大量用户不断刷新活动页面,会使读请求量飙升;秒杀活动开始后,大量用户瞬时涌入抢购有限商品,会形成写请求“洪峰”
为了满足高并发读请求所需容量,可采用资源扩展策略和数据缓存策略,限流策略作为兜底保护。数据缓存通常由本地缓存、分布式缓存和CDN共同组成缓存方案。其中,商品信息一般采用二级缓存,图片信息一般采用三级缓存
对于写请求(下单请求)可通过限流前置拦截绝大部分流量,如客户端限流,直接返回失败提示信息,从而减轻商品库存处理环节的压力
高可用实际包括网络高可用、服务高可用及存储高可用
网络系统包括防火墙、路由器、交换机等网络设备;服务器系统主要指提供服务的应用集群;存储系统指存储设施
冗余备份是最常见的高可用方案,当服务或应用因意外终止时,通过故障转移机制快速启用冗余或备用的服务器、系统、硬件或网络接替工作
秒杀活动的参与者有两个:买家和卖家。买家操作包括查询库存、扣减库存、落订单记录、支付。卖家操作包括查询库存、编辑库存等
编辑库存时,实际库存和卖家所见库存可能不一致,如采用了热点散列等技术,统计全局剩余库存时有时延导致库存不一致,修改库存时导致库存被错误覆写导致超卖
扣减库存时,一些秒杀系统会采用缓存来实现,该方式可能会导致超卖:数据库和缓存不一致、缓存本身的可见性都可能导致超卖
此外还可能存在少卖的问题:1.幂等控制失败、重复扣减库存;2.买家下单后,放弃付款或付款超时,导致库存占用却未交易成功
避免黄牛采用技术手段作弊,除了采用“答题”,“防链接暴露”等基础手段外,大型电商平台还会采用更为有效的风控校验手段,如人机识别、用户画像、关系网络(人际网络、媒介网络)等
扣减库存主要包括两步:
为了保证数据一致性,两个步骤须放在同一个事务中
从用户动线来看,购买一件商品需要经过浏览详情、加购、结算、提交订单、确认支付等环节,这些环节基本都需要查询对应商品的库存,如果已无库存或库存不足应提醒用户,阻断流程。为了保证并发性能,查询库存时不会直接访问数据库,而是查询缓存
缓存库存的更新可以通过数据库binlog同步到缓存
库存运作核心链路:
所有商品的库存存放于同一数据库的同一表中,来自应用系统的所有库存扣减请求最终都路由到同一数据库实例。该方式性能并发量较小
对数据进行水平拆分后,将不同商品的库存扣减请求路由到不同数据库,降低单机负载并显著提升并发能力。
采用商品库存id作为分表键,可以保证对同一商品库存的扣减操作和插入流水操作在同一事务中实现
如果针对某一商品,其库存数据只对应数据库的一条行记录,分库分表依旧无法解决问题。为了解决库存扣减热点问题,一般有两条技术路线:内核优化和热点散列
内核优化
优化数据库内核,提升行更新操作的性能。如“水车”模型:在应用层做轻量化改造,对热点行SQL打上热点标签,当这类SQL进入内核后,在内存中维护一个hash表,将主键(如商品Id)hash到统一地方作请求合并,经过一段时间后(默认100us)后统一提交,从而将串行处理优化为批处理,避免每个热点请求都去扫描和更新Btree
热点散列
在大规模秒杀活动中,针对单一商品的库存扣减峰值可以达到几万甚至几十万,单凭优化数据库内核依旧很难满足需求。针对这种情况,最有效的方案是采用热点散列,即分布式库存扣减,将同一商品的库存提前分配至多个桶中,根据路由规则将库存扣减请求路由至不同的桶,从而将集中于单实例的请求分散
“分桶”的一种技术实现是采用Redis。在缓存中扣减库存以提升系统的吞吐量;缓存扣减成功后异步向数据库写入库存扣减流水并更新库存;此外,还需要通过定时任务等机制实现缓存与数据库总量同步
该方案存在的问题:
由于库存具有全局性,买家、卖家对商品库存的写操作需做到彼此可见才能有效避免商品超卖,因此库存系统一般采用中心写、单元读。随着业务的发展,“中心写”模式逐渐成为整个库存系统的容量瓶颈,由此产生了库存单元化架构。
如果不采用中心写,可能在两个单元上分别进行扣减库存,导致超卖
单元封闭,即保证每一次库存操作都在所属单元内完成。
如A、B两个单元,A处理UID尾号在00~10之间的用户,B处理尾号在11~20之间的用户,A、B互为备份。如果B发生故障,则需将B单元的流量切换到A。但切流是有前提的,需要将B单元写入的数据全部同步到A单元,否则B单元的用户被路由到A单元后将无法看到自己的最新数据
在故障切流的过程中,为了不产生脏数据,且减小对用户体验的影响,目前已形成一套标准的解决方案——禁止写+禁止更新
在用户路由切换过程中禁止写入操作,在数据同步过程中禁止更新操作,保证对历史单据的更新操作必须基于最新的镜像数据,但是对插入操作放开,因为插入操作意味着首次下单
为了实现购买数量上限应为所有单元可售卖库存之和,需要具备全局库存管理能力。在实际应用中,一般采用异步汇总的方式实现,本质上是中心写、单元读
在使用单元化库存架构时,可能会出现全局库存还有剩余,但是部分单元已售光的问题。为解决这一问题,可以发起库存调拨,从中心到单元做一次即使虚拟出库和入库操作;当中心无库存时,需要将所有单元的库存回收