1. 初期:单库单表扛业务,MySQL基础优化

我们当时先上线了一个基础版:商品信息、用户信息、订单都放在一个MySQL实例里,几张表。一开始流量不大,一切正常。但很快,随着商品种类增多,订单表数据量突破百万级,查询开始变慢。

问题1:查询慢,尤其是订单列表按用户ID和时间排序。

我们分析慢查询日志,发现一个典型查询:

1
SELECT * FROM order WHERE user_id = 123 ORDER BY create_time DESC LIMIT 10;

explain发现全表扫描,因为user_id和create_time没联合索引。于是我们加了联合索引(user_id, create_time),查询瞬间变快。这里涉及B+树索引结构:联合索引先按user_id排序,再按create_time排序,所以能快速定位到该用户的记录,并利用索引的有序性直接拿到前10条,避免filesort。同时,因为只查10条,我们用到了索引下推(ICP),MySQL在存储引擎层就能根据索引过滤掉不符合user_id的记录,减少回表。

问题2:订单状态更新频繁,行锁竞争激烈。

秒杀时大量用户抢单,同一商品的库存更新成了热点。我们用了行锁,但发现并发更新同一行时,锁等待严重。于是我们调整了事务隔离级别为读已提交(RC),减少间隙锁,并且把库存字段单独放一张表,减少锁冲突。同时,我们优化了更新语句,只更新必要的字段,缩短事务时间。

问题3:数据量继续增长,单表突破千万。

我们开始考虑分库分表。当时用了ShardingSphere,按user_id取模分表,把订单表水平拆分成64张表。分表后,查询条件必须带上user_id才能路由到具体表,否则需要扫描所有表,这又引出了查询路由的设计。我们在中间件层做了SQL解析和改写,对于不带分片键的查询,我们通过异构索引(比如额外维护一个user_id和订单号的映射表)来解决。


2. 缓存引入:Redis扛住读压力

分表后,数据库压力暂时缓解,但秒杀时商品详情页的QPS高达几十万,直接查库肯定扛不住。我们引入了Redis做缓存。

问题4:缓存击穿、穿透、雪崩。

  • 商品详情:我们用Redis的String结构存储商品信息,key是商品ID,value是JSON序列化后的商品详情。过期时间设置随机值,防止大量缓存同时失效导致雪崩。
  • 热点商品:秒杀开始瞬间,某个商品被疯狂访问。如果缓存刚好过期,大量请求同时穿透到DB,就是缓存击穿。我们用了互斥锁:当缓存失效时,只允许一个线程去查库并重建缓存,其他线程等待或返回旧数据。这里我们用Redis的SETNX实现分布式锁,并设置合理的超时时间防止死锁。
  • 恶意请求:如果攻击者频繁请求不存在的商品ID,缓存查不到,每次都穿透到DB,这就是缓存穿透。我们引入了布隆过滤器,在请求到达前先判断商品ID是否可能存在,过滤掉99%的非法请求。布隆过滤器基于bitmap,我们直接用了Redis的Bitmaps实现,占内存极小。

问题5:排行榜需求。

运营要实时展示商品销量排行榜。如果用数据库order by count(*) group by,压力太大。我们改用Redis的Sorted Set,每次下单后,用ZINCRBY给对应商品增加销量,排行榜直接取ZREVRANGE。Sorted Set底层是跳跃表+哈希表,插入和排序效率都是O(logN),完美满足高并发实时排行。

问题6:分布式锁的细节。

秒杀扣库存时,我们用了Redis分布式锁来保证同一时刻只有一个线程扣减。但分布式锁有几个坑:

  • 锁超时:如果业务执行时间超过锁过期时间,锁自动释放,其他线程就会进入,导致并发问题。我们采用续期机制,用Redisson的看门狗原理,在锁快过期时自动续期。
  • 主从切换:Redis主从异步复制,如果master刚加锁就宕机,slave还没同步,锁就丢了。我们用了Redlock算法,但后来考虑到性能,我们在业务层面做了降级,允许极少情况下的超卖,再通过异步对账修复。

3. 异步化:消息队列削峰填谷

秒杀的核心是“削峰”。直接同步扣库存,数据库和Redis压力都大,而且一旦失败整个流程回滚,用户体验差。我们引入了RocketMQ做异步处理。

问题7:订单创建流程长,需要解耦。

用户秒杀成功后,我们只做两件事:在Redis中预扣库存,并发送一条“订单创建消息”到MQ。然后立即返回“秒杀成功”,后续的订单详情写入、积分赠送、短信通知等都通过消费消息异步处理。

  • 消息可靠性:RocketMQ的同步刷盘同步复制保证了消息不丢。但刷盘性能有损耗,我们权衡后用了异步刷盘+主从同步,保证极端情况下数据不丢。
  • 消息顺序:秒杀订单需要按顺序处理吗?我们要求同一用户的订单要顺序处理,防止重复下单。RocketMQ支持队列有序,我们把同一用户的订单发到同一个队列,消费者单线程消费该队列。
  • 消息重复:网络抖动可能导致MQ重复投递。我们要求消费者做幂等,比如用订单号作为唯一键去重,或者利用Redis的SETNX记录已处理消息ID。
  • 分布式事务:预扣库存和发送消息需要保证原子性。我们用RocketMQ事务消息:先发送半消息,然后执行本地事务(预扣库存),如果本地事务成功,提交消息;如果失败,回滚消息。这样即使本地事务完成后宕机,MQ也能通过回查机制确认最终状态。

4. 微服务架构:注册中心、配置中心

随着业务复杂,我们拆分了多个微服务:商品服务、订单服务、库存服务、用户服务。服务间调用频繁,需要服务发现和配置管理。

问题8:服务地址动态变化,如何管理?

我们用Nacos作为注册中心和配置中心。服务启动时向Nacos注册自己的IP和端口,消费者通过服务名调用。Nacos支持健康检查,定期探测服务状态,剔除异常节点。同时,配置中心管理所有服务的配置,比如线程池大小、开关,支持动态刷新,不用重启服务。

问题9:分布式链路追踪。

微服务调用链长,排查问题困难。我们集成了SkyWalking,基于字节码注入,自动追踪每个请求的调用链路,记录每个节点的耗时、异常。这帮助我们快速定位瓶颈,比如发现某个SQL慢是因为没加索引,或者Redis连接池满了。


5. 高可用:限流、降级、熔断

秒杀场景流量突发,必须做防护。

问题10:如何防止系统被冲垮?

  • 限流:网关层用Sentinel做限流,基于QPS或并发线程数。对于热点商品接口,配置令牌桶算法,每秒只放行一定请求。Sentinel的滑动窗口统计精准,而且支持动态规则推送。
  • 降级:当依赖的Redis或数据库响应变慢时,自动降级,返回默认值或提示“稍后重试”。比如商品详情缓存挂了,直接读DB,但加一个本地缓存做二级降级。
  • 熔断:用Hystrix或Sentinel的熔断器,当错误率超过阈值,自动熔断,快速失败,防止雪崩。

6. 数据一致性:最终保证

异步处理虽然提升了性能,但带来了数据不一致的风险。比如订单创建成功,但后续积分服务失败,导致用户积分没加上。

问题11:分布式事务的最终一致性。

我们采用了TCC模式(Try-Confirm-Cancel)来保证跨服务的最终一致。比如扣库存服务,Try阶段预扣,Confirm阶段实际扣减,Cancel阶段回滚。我们用Seata管理分布式事务,通过全局事务ID串联各个分支,保证要么都成功,要么都回滚。但TCC对业务侵入大,我们只用在核心链路,非核心链路则用本地消息表+定时任务补偿。


总结

从最初的一个MySQL单库,到分库分表+读写分离,再到引入Redis扛缓存,用消息队列异步解耦,用注册中心管理微服务,用Sentinel做高可用防护,最后用分布式事务保证最终一致——每一步都是业务驱动,每个技术点都踩过坑,也理解了背后的原理。

比如MySQL索引,不仅要知道B+树,还要知道索引下推、覆盖索引、自适应哈希;Redis不仅要会用,还得懂内存淘汰策略、持久化对性能的影响、集群模式下的数据分布;消息队列要清楚刷盘机制、重试策略、消息幂等;分布式锁要理解时钟漂移、续期、红锁的代价。