秒杀,你看这篇就够了

264 阅读6分钟

秒杀场景分析

秒杀是指用户在极短的时间内抢购限量商品的活动,通常伴随着高并发、短时请求激增的情况。这种场景下,系统需要具备很强的性能和可扩展性。

秒杀的问题:

  1. 高并发问题:短时间内大量用户请求,可能导致服务器过载,数据库连接耗尽。
  2. 超卖问题:当并发量过高,可能出现商品库存被多次扣减,产生超卖的现象。
  3. 下单与支付不一致:下单成功但未及时支付,导致库存被锁定,其他用户无法购买。
  4. 限流与公平性问题:秒杀活动可能导致系统崩溃或用户体验不佳,需要合理限流,避免个别用户占用过多资源。
  5. 恶意用户:可能出现脚本刷单、爬虫等,导致正常用户无法参与。

解决和改进措施:

  1. 限流:通过Nginx限流、Redis计数器、令牌桶等限流策略,避免短时间内过多请求打到后端。
  2. 异步处理:下单请求可通过消息队列(如RabbitMQ、Kafka)异步处理,避免直接操作数据库,减轻数据库压力。
  3. 乐观锁与库存扣减:使用乐观锁或者分布式锁机制防止超卖,或者基于Redis的原子性操作实现扣减库存。
  4. 缓存预热与降级:活动前将秒杀相关信息缓存到Redis等缓存系统,活动开始时直接从缓存读取,减少对数据库的压力;如果后端负载过高,可以直接返回“售罄”状态。
  5. 风控系统:识别恶意行为(如刷单、爬虫),并对IP、账号进行封禁或限制。

2.3 下单过程中失败的处理方式

秒杀过程中,订单操作涉及两步:

  1. 扣减库存
  2. 创建订单

如果任何一步失败,都需要回滚处理,确保数据一致性。主要有以下几种方式:

  1. 事务管理:通过数据库事务机制(如MySQL事务或Spring事务管理),将库存扣减和订单创建放在一个事务中,一旦任意步骤失败,会自动回滚。
  2. 补偿机制:在事务外加一层补偿机制,如果库存扣减成功但订单创建失败,可以通过定时任务或消息队列,重新尝试创建订单或将扣减的库存恢复。
  3. 幂等处理:确保订单创建操作是幂等的,避免重复创建订单。

2.4 压力测试与QPS

基于数据库和Spring事务的解决方案在高并发场景下可能会面临瓶颈,因此需要进行压力测试:

  1. 压力测试:可以使用工具(如JMeter、wrk)模拟秒杀场景的高并发请求,检测系统在一定QPS下的性能表现。
  2. QPS指标:具体QPS性能取决于多方面因素,包括数据库性能、缓存的使用、负载均衡策略等。基于Spring事务和数据库的纯解决方案,如果不使用缓存,通常支持数百到上千的QPS,但需要根据具体的应用架构进行优化。

2.5 防止用户下多单的解决方案

除了分布式锁外,还可以采取以下措施防止用户下多单:

  1. 唯一性约束:在订单表中,通过对用户ID和商品ID建立唯一性约束,避免用户重复提交订单。
  2. 限购策略:在业务层面实现限购逻辑,在秒杀开始前将限购信息缓存至Redis中,用户提交订单时,首先检查是否已超过限购数量。
  3. 防重令牌:为每个用户生成唯一秒杀令牌,用户请求时必须携带令牌,确保每个令牌只能使用一次,避免重复下单。
  4. 前端校验:通过前端限制用户的点击频率,防止用户频繁发起下单请求。

3. 场景拷打

3.1 订单查询与存储

3.1.1 用户端分页查询订单

用户分页查询可以通过MySQL的LIMITOFFSET进行分页。为了提高效率,可以结合缓存策略,将常用查询结果缓存到Redis中,并根据用户的查询条件动态更新。

3.1.2 商家端查询订单

商家查询订单的需求类似,但由于数据量更大,可能需要更多的优化手段,如SQL调优、适当的索引等。

3.1.3 海量数据存储问题

随着订单数据的增长,单表可能出现性能瓶颈,海量数据存储可以采用以下方式解决:

  1. 水平分表:将订单表按用户或订单的某一特征进行拆分,减小单表的数据量。
  2. 垂直分库:将不同类型的数据存储在不同的数据库中,比如用户相关的数据存储在一个数据库,订单相关的数据存储在另一个数据库。
  3. 归档冷数据:定期将历史订单数据归档,减少热数据表的容量。

3.2 水平分库分表的拆分维度

可以按照订单的某些维度(如userIdorderId)进行水平拆分:

  • userId拆分:将同一用户的订单存放在同一张表中,查询效率高,但商家查询时可能跨库。
  • orderId拆分:使用订单ID进行哈希分片,均匀分散数据。

3.3 用户查询订单的路由问题

如果按照orderId哈希分片,用户想要查询自己的订单,需要先通过userId或其他索引找到对应的订单ID,之后再根据订单ID定位到具体的分表。

3.4 商家查询订单的路由问题

商家查询订单时,如果是按userId哈希分片,则商家查询商品订单的效率会下降。可能的解决方案是:

  1. 联合索引:在订单表中对userIdproductId创建联合索引,优化查询。
  2. 二级索引表:为商家维护一个独立的索引表,专门用于商家查询商品订单,索引表只存储订单的基本信息和分片的路由信息。

3.5 商品ID + 用户ID哈希分片的路由问题

如果userIdproductId组合后进行哈希分片,查询时userId的哈希和productId + userId的哈希可能不一致,导致无法路由到正确的表。为解决此问题:

  1. 两级哈希:可以先根据userId哈希路由到某个分库,再根据productId哈希路由到具体的分表。
  2. 维护映射关系:通过Redis或者数据库中维护userIdproductId的映射关系,查询时可以快速找到对应的分表。

3.6 使用两个哈希函数的方案

使用两个哈希函数,分别对userIdproductId进行哈希运算,按照结果组合进行分库分表。这样可以实现较均匀的数据分布,查询时先根据userId路由到对应库,然后再根据productId找到具体表。这种方式需要额外维护userIdproductId的映射关系,确保路由正确。