秒杀系统设计重点
秒杀系统设计重点
系统特点
1. 瞬时高并发
仅在秒杀时间点的前后几分钟,流量突增。
2. 读多写少
大量用户抢少量商品,大部分用户的访问不与修改库存的操作关联。
高性能
1. 动静分离
将动态页面改造成静态页面,可以将静态数据放在CDN中进行缓存。
2. 热点操作优化
刷新、下单、添加购物车一类的操作,是用户的行为,在秒杀时间点大量发生,可以做一些限制保护。如用户频繁刷新页面时提示、添加验证码。
3. 热点数据优化
3.1 热点识别
能够提前预测出的热点数据,如商家数据、活动促销等的数据,比较容易做到提前准备;
但也存在随着实际业务变化的突发热点,可能集中在某个平时比较冷门的商品上。需要动态的热点发现能力。
- 采集热点信息:nginx采集URL日志,提前识别潜在的热点实际;
- 热点发送:通过一定规则将热点数据推送给各个系统,每个系统决定不同的处理策略。
3.2 热点隔离
将识别到的热点请求和普通请求区分开。可以为秒杀系统部署单独的域名和集群(系统隔离),也可以为热点数据启用单独的缓存或数据库(数据隔离)。
3.3 热点优化
缓存和限流。使用redis作为缓存。
一致性
1. 库存问题
核心问题是避免超卖。将购买过程分为两步:下单和付款。
采用预扣库存的方式。下单后,先减库存,一段时间没有付款,库存自动释放。
- 避免恶意下单:用户打标、限制最大购买数;可以对用户或者ip限流,但都会存在一些问题:恶意请求可以使用多个用户,多个普通用户也可能共享IP。更好的方式是验证码。
- 避免超卖:如果是普通商品,允许一定量的超卖,可以通过补货来解决。但有些商品库存不允许为负,可以通过数据库事务来避免,出现负数回滚。
- 自动释放:采用延迟队列,下单后向延迟队列发送一条消息,消息超时后,消费者会读取消息并查询订单状态。
2. 高并发读
“分层校验”:读请求只做业务检查(如用户是否有秒杀资格、商品状态等),不做一致性校验,因为会影响性能。在写时才对库存做一致性检查。缓存中允许一定的脏数据,这样只会导致少量下单请求被认为有库存,在写数据时在保证最终一致性。
3. 高并发写
如果逻辑简单,不依赖事务,可以将商品信息直接放在Redis中操作。如果要放在DB中,可以使用分布式锁控制操作并发度。
-
添加锁
- setNx命令(set if not exist):和设置超时时间的操作是分开的,不保证原子性。
- set命令:同时设置[lockKey, requestId]的键值对,以及过期时间。
-
释放锁:判断lockKey是否存在,然后比较requestId是否相等,已避免释放其他请求添加的锁(自己的可能已经过期了)。采用lua脚本保证原子性。
-
自旋锁:用于避免大量请求同时竞争锁导致问题。获取锁失败时,阻塞一段时间,然后重复获取锁。
高可用
核心目标:控制对资源的瞬时消耗
1. 流量削峰
1.1 入口削峰
采用答题或验证码,在入口的层面拉长请求的峰值。
或者增加门槛,如用户等级,来限制用户数。
也可以分批秒杀,将购买周期拉长。
1.2 消息队列
将同步的直接调用转化为异步的间接推送。
- 消息丢失问题:消费者消费mq中的消息时,可能因为网络或其他问题造成消息丢失。解决办法是添加一张消息发送表。生产者向mq推送消息前,先写入表中。消费者消费完消息,再修改消息状态为已处理;增加一个job,查询消息发送表中待处理数据,重新推送进mq。
- 重复消费问题:因为增加了重试机制,可能导致重复消费。解决办法是增加一个消息处理表,消费者读取消息后,先检查是否存在表中,不存在才消费;消费消息后,将消息写入表中,要保证消费和写表是原子操作。
- 垃圾消息问题:存在某些消息无法被成功消费,导致job一直重试。解决办法是限制消息的最大重试次数。
2. 缓存击穿
采用缓存预热,实现将商品放在缓存中;采用分布式锁,如果缓存中不存在对应商品,先获取锁,然后再去数据库查询,查完将商品放入缓存并释放锁。
3. 缓存穿透
如果大量请求传入了不存在的商品id,每次都加锁会导致性能问题。可以在查询缓存前使用布隆过滤器(布隆过滤器通过多个哈希函数将元素映射到位数组中的多个位置,判断时检查这些位置是否全为1,以此实现高效的“可能存在”或“一定不存在”的查询),提前过滤掉不存在的商品。