当前位置: 首页 > news >正文

系统设计(2)—Redis—消息队列—数据库—熔限降

Redis 缓存设计

在高并发系统中,缓存是提升性能、减轻后端负载的杀手锏。Redis 作为内存级的高性能缓存数据库,被广泛应用于各类系统设计中。利用 Redis,将热点数据存储在内存中,可以加速读写并大幅降低对后端关系型数据库的直接访问频率。例如,用户会话、商品详情、配置项、排行榜等数据都适合放入缓存,以毫秒级速度读取。

常见应用场景

  • 数据缓存: 最典型的是将数据库查询结果缓存到 Redis。例如商品详情页访问量大且商品信息不频繁变动,可在用户第一次访问商品时将商品数据写入 Redis,后续请求直接从 Redis 获取。缓存有效降低数据库压力。

  • session 会话缓存: 网站的登录会话、用户令牌常保存在 Redis 中,这样各应用服务器都能共享访问,并且Redis内存读写快,验证用户登录态时延低。即使单台应用宕机,session不丢失,只要Redis高可用,就不影响用户继续访问。

  • 计数器与排行榜: Redis 的原子自增操作使其成为实现浏览计数、点赞数的利器。在直播、热点新闻场景下,使用Redis累加观看人数、点赞数,可以每隔一段时间再同步到数据库,既保证实时性又减轻数据库写入频率。Redis的有序集合数据结构也适合做排行榜(比如按积分排序列出Top N),可定期持久化。

  • 分布式锁: 在微服务环境下需要跨节点的互斥锁时,可利用Redis的SETNX和过期机制实现简单的分布式锁。虽然Redis锁不是百分百严谨(需要考虑持锁节点宕机等情况),但在秒杀抢购、库存扣减等场景经常使用。进一步还有RedLock算法通过多主节点提高安全性。

  • 队列和消息: Redis 列表结构可用作简易队列,生产者LPUSH插入,消费者BRPOP阻塞式拿出元素。在实时性要求不高又不想引入重型消息队列的场景(如简单的任务队列)下,可以用Redis充当缓冲。但需要注意Redis并非专业MQ,无法保证严格可靠投递。

缓存模式

         常见有缓存旁路模式 (Cache Aside):应用读取数据时先查缓存,未命中再查数据库并将结果写入缓存(又称lazy load);更新数据时先更新数据库,再删除缓存(或更新缓存)。这模式实现简单且应用广泛。但要小心在高并发写场景下保持缓存与数据库的一致性。

        另一种**读穿写模式 (Read-Through/Write-Through)**是应用只和缓存交互,由缓存代理决定何时取DB,甚至对更新也直接写缓存并由缓存同步数据库。这需要配合如Redis-MySQL双写一致性的组件,复杂度高,在面试中一般讨论Cache-Aside模式即可。

高并发场景下策略

  • 电商秒杀: Redis 在秒杀系统中发挥核心作用。库存预减是常用策略:活动开始前将商品库存缓存到Redis,用户下单请求先尝试对Redis库存执行原子递减,若结果>=0则认为抢购成功,再异步写入实际订单数据库,否则库存不足直接拒绝​。这种方式利用Redis单线程原子性避免超卖,同时极大减少数据库事务竞争。不过需要处理Redis和数据库的不一致风险(最终以数据库库存为准,可在订单写库前再次校验库存防止少减)。另外,秒杀的订单写入往往通过消息队列异步串行化处理,Redis在这起到削峰缓冲作用:所有请求快速读写缓存,成功的再排队慢慢落库。

  • SaaS CRM: CRM系统的缓存应用侧重于多租户隔离数据实时性。可以为每个租户建立独立的缓存命名空间(如不同前缀)防止键冲突。缓存内容包括一些基础数据(如行业分类列表、热门销售线索等)供频繁查询。由于CRM系统数据变化相对频繁(业务人员不断更新客户信息),缓存的实时性要求高,可能需要设置较短的过期时间或者在数据库更新时主动刷新相关缓存,以确保用户看到的数据是最新的。

  • 支付系统: 支付系统对一致性要求高,因此缓存多用于非强一致场景。例如缓存一些相对静态的配置(银行列表、费率信息)和用户资料,加快查询,但对于交易余额这类强一致数据一般直接查询数据库以避免资金错误。不过支付流程中也会用Redis来防重和限流:比如记录近期支付请求的流水号,防止同一流水请求被处理两次;对某用户支付尝试次数进行计数,一旦超过阈值暂时锁定,防止恶意刷支付接口。Redis的快速读写非常适合这些操作。

缓存雪崩、击穿、穿透及对策

  • 缓存穿透: 指大量请求查询不存在的数据,由于缓存永远不会命中(不存在的 key 没缓存),每次都打到数据库。例如攻击者不断请求一个不存在的ID。解决方案是对这类请求缓存一个空结果(比如缓存一个特殊值表示空,设置短TTL),或利用布隆过滤器在查询前快速判断请求的key是否存在于数据集合中。

  • 缓存击穿(热点Key失效): 指某个热点数据在缓存过期的一瞬间,有大量并发请求同时读这个key,因为缓存失效都去查数据库,可能压垮DB。解决办法是在缓存失效更新时加互斥锁或信号量——第一个请求去查DB并重建缓存,其他请求等待缓存重建完再读缓存​。或者给热点数据设置“不主动过期”,由后台定时刷新缓存,这样不会有过期瞬间。还可随机化不同key的过期时间,避免同一时刻大量key同时失效。

  • 缓存雪崩:大量缓存集中失效或缓存服务器宕机导致请求全部打到数据库,引发DB崩溃。应对措施:缓存失效时间尽量加随机散布,避免同一时间大量数据过期;部署缓存高可用集群(Redis 主从+哨兵或集群模式),避免单点故障;在流量高峰期通过降级措施保护数据库,例如暂时返回默认值或限流部分请求​。确保缓存层本身高可用是重中之重,如部署Redis哨兵实现自动故障转移,并在应用层做好容错(Redis不可用时及时熔断该查询逻辑)。

在实际生产中,会综合多种方法来预防三大问题,比如缓存预热,参数校验,缓存空值,设置随机过期时间,布隆过滤器判空,热key永不过期,singleFlight归并请求,以及限流等技术。

Redis集群与扩展

当缓存数据量和并发超出单机Redis承载时,可采用Redis 集群或分片模式扩展容量。Redis Cluster模式将数据分布到16384个哈希槽,由多台节点分担,全局可管理数十TB内存。比如京东金融的大规模缓存系统R2M,管理的总内存容量超过60TB,包含近600个 Redis 集群、9200多个 Redis 实例​。可见大型互联网公司会部署庞大的Redis集群来应对海量缓存需求。使用集群需要注意键分布:比如多个相关键要存储在同一槽位时,可使用Hash Tag将它们放到同一分片,否则涉及多个键的操作(如Lua脚本、事务)会受限于跨分片问题。另一种扩展方式是客户端分片(如使用一致性哈希将不同key路由到不同Redis实例)或代理分片(如Twemproxy、Codis)实现,对应用透明。无论哪种方式,都要考虑数据重分布带来的开销,当增加或减少节点时尽量平滑迁移最少数据

持久化与容灾

Redis默认是内存存储,为了防止数据丢失,可开启AOF或RDB持久化,但这在主要将Redis作为缓存的场景下通常不启用全部数据持久化,而是接受缓存数据在重启丢失(因为数据库还保存了权威数据)。不过对于某些需要缓存又不想损失的数据(如缓存的会话、订单未支付列表),可考虑开启部分持久化或热备节点来防数据丢失。Redis 哨兵模式或Cluster模式可提供快速故障转移能力,一旦主节点失效从节点顶上继续服务,让应用几乎无感知。但需要注意主从切换期间数据一致性问题——可能新主缺失最后几毫秒数据,因此关键的数据(如分布式锁)要有额外保障或在发生切换时全局通知应用层处理。

常见坑点

  • 缓存一致性: 当数据库更新时,相关缓存若未及时更新/删除,会导致读到脏数据。需要严格遵循先更新DB再删除缓存的顺序。如果删除缓存失败要有补偿机制(比如发送消息通知再删一次)。对于非常敏感的数据,宁可缩短缓存TTL甚至每次查数据库,以确保一致。

  • 内存淘汰策略: Redis内存满后按策略淘汰键。如果不合理设置,可能出现缓存抖动。常用LRU或LFU淘汰最近最少用的数据,避免热点数据被误删。监控Redis内存使用,防止因为没有设置maxmemory而撑爆导致进程异常。

  • 命名空间与键设计: 在多业务共用一个Redis时,键要加业务前缀区分,防止不同模块键冲突导致数据错乱。键名不宜过长但要可读,必要时可以用hash结构存储以减少大量小键。

  • 滥用Redis: 要认识到Redis再快也是内存和网络操作,计算密集型的事情不要放在Redis做。例如不要频繁用Redis做大集合的交集并集计算,那更适合离线计算后缓存结果。还有些新手会把Redis当成数据库用,存大量冷数据,这没有意义且浪费内存,应当只缓存热数据,冷数据依然存在DB,访问时再缓存。

实践案例

几乎所有互联网公司都深度使用了Redis。

京东构建了大规模的分布式缓存平台 R2M 来支撑其电商和金融业务,高峰期依靠60TB级别的Redis集群保障读写高吞吐​。

微博利用Redis缓存热点话题和用户信息流,保证在热点事件发生时,用户首页依然能快速加载而不压垮后端数据库。

美团在其餐饮系统中使用Redis存储商家菜单和订单快照,配合过期时间自动删除过期菜单缓存,在午高峰时段顶住了订单查询的高并发。

对于秒杀系统,阿里的实践是库存预热到Redis并在下单时Lua脚本保证库存扣减的原子性,再通过消息队列异步写数据库,从而支撑起双11的海量抢购。

可以说Redis已成为高并发架构的标配组件,其灵活的数据结构和优秀的性能为系统提供了强有力的支撑。

消息队列

消息队列(MQ)是在分布式系统中实现异步解耦流量削峰的关键组件。通过MQ,系统可以将一些不需要同步完成的工作以消息形式发送到队列,另一个进程异步消费处理,从而降低主流程的响应延迟,并平滑高峰压力。典型的消息队列产品有 RabbitMQ、Kafka、RocketMQ、ActiveMQ 等,它们各有特点:RabbitMQ偏重可靠投递和丰富功能,Kafka追求高吞吐和分区顺序,RocketMQ则在高可靠和高性能间折中并天生支持分布式事务消息。

应用场景

  • 削峰填谷: 在高并发场景下,瞬时涌入的请求可能压垮系统。MQ可以作为缓冲区,把突发的大量请求存入队列中,让后端工作线程逐条处理,从而将高峰流量削平到后端可承受的水平。典型如秒杀下单:用户请求先迅速写入消息队列返回排队成功,再由订单服务逐个消费下单,避免数据库同时写入过多造成崩溃。

  • 异步处理: 对于一些不需要同步完成的操作,使用MQ可以大大提高主流程响应速度。比如用户注册后要发送欢迎邮件,这种耗时操作放到消息队列,由专门的邮件服务异步发送,注册接口就能快速返回不让用户等待。再如CRM系统中批量导入客户、生成报表等耗时任务,都可通过MQ异步执行,处理完再通过通知或回调告知用户。

  • 系统解耦: MQ可以解耦组件,让发送方和接收方无需直接调用彼此接口。例如电商下单成功后,需要更新库存、通知物流、发送短信、推送优惠券等,多种后续操作可以各自订阅订单完成的消息来执行,彼此解耦合。这样订单服务不需要知道短信服务的存在,只要往MQ发消息,各消费者自己处理即可。这种发布/订阅模式降低了模块之间的依赖。

  • 分布式事务(最终一致性): 在跨服务的数据操作中,用MQ来实现最终一致性是一种常见方案。例如支付成功后,需要同时更新订单状态和用户积分。如果直接远程调用更新,有失败一致性风险。改为支付服务发送一条“支付成功”消息,订单服务消费后更新订单状态,积分服务消费后加积分。如果其中一个服务暂时不可用,消息会留在队列稍后重试处理,最终达到一致状态。这种称为事务消息可靠消息最终一致,RocketMQ 就支持这类场景。

高并发及可靠性考虑

设计MQ方案要考虑吞吐可靠的平衡。像Kafka、RocketMQ能够在高峰场景下提供极高的吞吐,在阿里双11秒杀等业务中,消息队列中间件在流量削峰、解耦上发挥了不可替代的作用。为保障消息不丢失,可以开启消息持久化(落盘)和复制机制(多副本存储)。例如Kafka的每条消息可以写入多个副本,只要有一个副本存活消息就不丢,但这会稍微降低写入性能。在需要严格可靠的场景(如金融交易),通常使用确认机制:消息由消费者处理完需要明确ACK应答,若超时未ACK则MQ重新投递(或放入死信队列)。同时消费者处理消息时要幂等:防止重复消费造成数据错误,这通常通过为消息附加唯一ID,消费者端记录已处理过的ID来防重。

场景实践

电商秒杀中,为了应对海量订单,MQ承担了“削峰”的重任。所有用户秒杀请求首先进入消息队列,后端订单服务按照自己的处理速度消费队列,哪怕同时有几十万请求,也不会同时压垮数据库,而是排队逐个写入。这样用户体验是提交后进入“队列中”的状态,然后稍后得到结果通知。

SaaS CRM中,MQ可用于集成不同系统模块,例如客户信息变更时,通过消息通知相关的销售统计服务更新报表;或者CRM与外部系统(如钉钉、邮件)集成,通过消息触发外部API调用,防止主流程阻塞。

支付系统更加大量地依赖MQ保障可靠性和异步性——支付结果通知、对账流水都是通过消息异步分发,各业务模块订阅对应的支付消息做后续处理(发货、发送电子发票等)。支付场景还常用事务消息:在扣款完成后,支付服务发送一条消息到MQ并预先锁定,待订单服务确认写入订单成功后,再确认消息投递,这种两阶段提交确保了支付扣款和订单更新要么都成功要么都不发生。

常见坑点

  • 消息堆积: 如果消费者处理能力跟不上生产速度,队列会越积越长,延迟变高甚至内存溢出。要监控队列长度,必要时临时扩容消费者实例消费积压消息。另外最好为关键队列设置报警阈值,一旦积压严重可考虑降级策略(比如丢弃不那么重要的消息或者通知用户稍后再试)。还有一种方法是新建一个有个更多分区的topic,把请求切过去,等老topic处理完了再过来。

  • 消费顺序: 某些场景要求严格的消息顺序(如同一用户的操作顺序)。分布式队列通常只能保证单分区内有序,但不能保证全局顺序。解决方法是按照业务标识分区:比如Kafka可以按订单ID哈希分配分区,这样同一订单的所有消息进入同一分区,那个分区由单线程消费,保证顺序。或在应用层为消息加序号并在消费端自行校验顺序。

  • 重复与幂等: 网络抖动或消费者处理超时可能导致消息被MQ投递多次,必须确保处理逻辑是幂等的。例如给用户充值的消息,如果消费端没有防护,重复消费会导致用户余额多加。可以在处理前先检查这个消息ID是否已经处理过(记录在数据库或缓存中),若是则跳过。或者一些MQ(如Kafka)和数据库支持联合实现事务消费,要充分利用这些特性。

  • 错误处理: 当消费者无法处理某条消息(如格式错误或业务校验失败)时,不能无限制重试,否则会阻塞后续消息。应将问题消息投入死信队列(DLQ),以便后续人工排查,同时确保主队列畅通。系统应对DLQ有监控,发现死信及时处理。

  • 分布式追踪: 当系统通过消息流转,排查问题较直接调用更复杂。最好在消息中附带trace ID,在日志中链路串联起来,出问题时能通过trace追踪消息的流转路径。

实践案例

在大型互联网场景下,消息队列无处不在:

阿里巴巴自研的 RocketMQ 就是在双11海量订单、支付场景中诞生,承担着订单创建、库存扣减、交易状态通知等关键任务,实现了“削峰填谷”和异步解耦,保障了核心系统的稳定。

美团点评的业务也大量使用Kafka进行日志和用户行为的实时收集,然后通过流处理框架做实时分析。

腾讯微信的后台用消息队列来异步分发用户消息和朋友圈推送,从而将消息发送和存储解耦,提高了系统并发处理能力。

在金融领域,银行支付清算系统使用高可靠消息队列来在不同核心系统之间传递交易信息,确保哪怕某模块暂时不可用,消息也不会丢,稍后补上处理。

这些案例充分说明了消息队列在高并发和分布式系统中的价值:它既是“缓冲阀”,也是“连接器”,让系统既稳又灵。

数据库设计与优化

数据库是系统的数据底座,在架构设计中至关重要。对于关系型数据库 (RDBMS),如 MySQL、PostgreSQL 等,如何在高并发场景下保证数据一致性、性能和扩展性是架构师面临的主要挑战。以下从库表设计、分库分表、索引优化等方面讨论数据库的系统设计要点。

表设计与范式反范式

合理的表结构是基础。面试中常问是否遵循范式设计。一般来说核心业务数据采用第三范式设计,避免冗余保证一致性。然而在高读场景,有时会适当反范式提高查询性能,例如冗余存储一些统计字段,避免频繁JOIN。需要把握平衡:冗余带来一致性维护成本,应在可接受范围内使用。针对读多写少的业务,可以建立汇总表或冗余列,加快查询。针对写多的业务,则尽量保持表结构简洁、避免不必要的关系约束,以减小每次写操作的负担。

索引优化

数据库索引直接影响查询性能。设计时应根据常用查询模式创建合适的索引组合(单列、复合索引)。避免全表扫描是目标,一般在WHERE条件、JOIN和ORDER/GROUP字段上建立索引。需要注意索引并非越多越好,过多索引会降低写性能和增加存储占用。要特别小心索引失效的场景,例如LIKE前缀模糊查询、函数转换列值等,可能导致索引无法使用。对于高并发写入的系统,要评估每个索引的必要性。常见优化手段包括覆盖索引(只用索引列满足查询需求,避免回表)和分区表(对于超大表按时间或范围分区,加快特定范围查询)。

读写分离

当读请求远多于写请求时,可以采用主从复制的方式将数据库读压力分散到多个从库。应用将更新操作发送主库,查询操作优先发往从库。这样通过增加只读从库可以线性扩展读取能力。不过要注意主从同步的延时问题——从库数据可能稍有滞后,关键读取(比如刚提交的订单立即查询)仍需走主库或者采用强制刷新的机制。

业界常配置主库处理关键强一致读和所有写,其它一般查询走从库,以换取性能和一致性的平衡。此外,需要有从库故障切换机制,保证主库故障时能快速提拔某从库为主,或者使用支持多主的数据库方案。

分库分表

当单个数据库性能和容量达到极限,就需要水平拆分 (Sharding)。分库分表有多种策略,常见包括:

  • 按业务垂直拆分: 将不同业务模块的数据放入不同库。例如用户、商品、订单分别在独立的数据库或实例中。这减少了单库的数据量和耦合度,各库可以独立优化扩展。大部分系统演进的第一步都是垂直拆分,把单体数据库按领域拆开。

  • 按范围或哈希水平拆分: 针对某些数据量巨大的表,再在业务内进行水平拆分。比如订单表记录特别庞大,就可以根据某个键将表拆成多个。方案包括按用户ID哈希按订单创建时间分区买家/卖家双写等。实际案例:淘宝的订单库采用买家库和卖家库分离,一个订单会在买家库和卖家库各存一份,实现买家按用户ID、卖家按店铺ID拆分存储​。这种冗余存储使得无论从买家角度查订单历史还是卖家角度查销量,都能在各自独立的库上高效完成​。一般场景下,更常用的是按用户ID或订单ID做哈希分片,比如将订单ID按照某算法映射到不同库不同表。许多电商选择user_id作为sharding键,将同一用户的订单都存放在同一个库表上,查询单用户订单非常高效。

  • 冷热数据分离: 一些系统会把历史冷数据近期热数据分开存储。例如按年份拆分订单表,把今年的订单放在当前库,往年的归档到历史库或Nosql。这种方式用户查询近期订单走小表快,偶尔查历史订单可以接受稍慢。实现手段如定期把老数据转移到归档库,应用查询时根据时间决定查主库还是历史库​。

采用分库分表后,需要解决分片路由的问题,即应用层如何知道某个请求应访问哪个库表。通常会有路由层或中间件(如MyCAT、ShardingSphere)根据请求参数计算路由。应用也可以内置规则(如订单号中编码分片信息)。例如某订单号包含了分库分表的ID,通过解析就能定位库和表​。路由方案必须足够快且配置清晰,否则维护成本很高。

NoSQL 与新型数据库

除了关系型数据库,一些场景下会引入NoSQL或新型分布式数据库。例如需要存储超大规模的时序日志、历史行为记录,用HBase、Cassandra等宽表数据库可能更合适。或需要全文检索功能,则可以将数据同步到ElasticSearch供检索。

高并发场景实践

  • 电商秒杀/大促: 数据库是订单写入的最终落地点。为了承受瞬时万笔订单写入,常见做法是提前拆分订单库,甚至为秒杀活动单独设置订单库集群,将秒杀订单与普通订单分离。下单时只涉及较小的秒杀订单库,避免了对全局订单库的冲击。另外利用消息队列串行化写库,也减少了数据库并发事务冲突。库存扣减通常在缓存完成,数据库侧最终一致性调整库存,可能通过存储过程或乐观锁实现防超卖。像阿里双11,订单数据库经历了多年演进,采用了OceanBase这类分布式数据库来横向扩展,同时引入严格的压测和预案确保稳定。

  • SaaS CRM 系统: 多租户CRM在数据库设计上通常有两种模式:一库多租户一租户一库。一库多租户即所有客户的数据存在同一个schema中,通过tenant_id区分,优势是容易增加新租户,劣势是数据量巨大时单库压力大且不同租户间可能有影响。为避免“大客户拖慢系统”,很多SaaS选择拆库:小租户共享库,大客户独立库。比如免费版用户集中在几个共享数据库实例,而付费大客户各自一套数据库。这需要在应用层有路由逻辑按租户选择数据源。此外,为了扩展性,CRM也会针对业务模块分库,如将日志、审计等大数据量模块单独用MongoDB或ES存储。这样既保证关键交易数据的DB性能,又让查询分析类的需求由专门引擎处理。

  • 支付系统: 支付和财务场景下数据库设计强调强一致和高可用。一般采用商业数据库(如Oracle、DB2)或金融级MySQL架构,开启主备双写甚至三地五中心容灾,确保数据不丢。另外设计上尽量避免跨库事务,比如账户系统、清算系统分库,但通过异步对账来核对。支付流水号通常作为数据库主键,具有全局唯一性(可能使用雪花算法生成分布式ID)以便追踪。分表在支付中也很常见,比如按日期+账户将交易记录拆表,方便日终批处理和查询。由于支付涉及金额,通常不会做过多冗余数据,宁可牺牲一点性能确保一致性。不过也有优化:比如对账结果或日报数据会冗余存储为报表库,让查询报表不影响核心库性能。

数据库优化与运维

除了设计,数据库性能优化还包括一些运行时调整:

  • SQL优化: 定期审视慢查询日志,分析执行计划。添加必要的索引或优化SQL写法(避免SELECT *,拆分复杂查询等)。对于ORM框架产生的低效查询也要注意,例如N+1查询问题可以通过JOIN或批量查询解决。

  • 连接池: 使用数据库连接池,设置合理的最大连接数。过多连接会加重DB上下文切换,过少又可能不够用。高并发应用一般限制每个服务实例的连接数,乘以实例数不超过DB可承受范围。采用读写分离后,要分别设置主库和从库的池。

  • 预编译与批处理: 频繁执行的SQL尽量使用预编译语句复用执行计划。批量插入或更新多条数据时,使用批处理操作一次性提交,减少往返。合理使用事务批量提交,以减少每次事务提交的fsync等待。

  • 硬件与分区: 大量事实证明,给数据库加内存用SSD存储是最直接有效的性能提升手段。尽量让活跃数据集部分放在内存buffer pool里。如果单机仍不够,可以考虑表分区,将大表I/O分散到多个磁盘文件甚至多个节点上。还可以利用数据库集群/中间件(如Vitess、Citus)做分布式SQL处理,但这对应用透明度有挑战,通常是在明确需要并有专业DBA支持时采用。

  • 容灾与备份: 高并发不代表可以忽略备份,相反数据越多备份越关键。要设计备份策略(全量+增量)并定期演练恢复。还要考虑单机故障时快速切换(主从切换或使用分布式协议实现无缝迁移)。比如MySQL主从启用semi-sync半同步模式,确保主库提交的事务至少传到一个从库,主库故障后一个从库可以最新数据顶上。

常见坑点

  • 跨分片查询困难: 一旦进行分库分表,有些原先简单的查询(如全库排序、聚合)就变复杂,需要在应用层汇总多个库结果。这经常被忽视,等需求来了才发现难以实现。解决方案可能要引入分布式查询组件或者额外维护全局表。例如电商为了统计全站订单排名,不可能实时扫所有分片的订单表,所以会维护一个专门的统计库。

  • 分片键选择不当: 如果选择的shard key数据分布不均,会导致部分库压力远高于其他库(热点分片)。比如按用户ID尾号分片,如果某些大客户订单特别多,会让那一片成为瓶颈。因此shard key要选择能够平摊负载查询常用的字段,并定期评估分布。如果业务增长导致分片压力失衡,要做好再拆分或迁移方案(如双写新分片,老数据逐步挪过去)。

  • 事务边界变化: 分库后无法使用数据库本身的跨库事务,有些逻辑需要迁移到应用层。例如下单扣库存原本是同库事务,拆到订单库和库存库后要通过两阶段提交或补偿来保证一致。这增加了实现复杂度,如果设计不周全会埋下数据不一致隐患。

  • 过度依赖缓存掩盖DB问题: 有时因为用了缓存,数据库瓶颈没暴露,但一旦缓存失效或故障,数据库瞬间扛不住。不要忽略对数据库的优化和容量规划,缓存再好也需要DB撑住底层。建议定期模拟“缓存失效”场景进行压测,确保数据库有基本的应急处理能力(例如限流或者快速扩容)。

  • 锁和阻塞: 在高并发写场景下,要避免长事务和表级锁。例如避免大批量更新操作一下锁住全表,可以分批或使用更细粒度条件。监控数据库的锁等待和死锁情况,发现问题及时优化SQL或加合理的索引(死锁往往因索引不当导致锁顺序不一致)。

实践案例:

淘宝在早期架构中就对订单库进行了买家/卖家分库设计,每个订单写入两个分库,冗余存储换取了查询效率​。随着规模扩大,又对订单库按用户哈希进一步分片,同时维护关系索引表来定位订单所在库。

微信的用户数据据报道分布在上百个MySQL实例中,通过用户ID路由查询,每台数据库负载均衡且可水平扩展。另外微信好友关系等采用了分布式存储方案(自己开发的TenDB和混合架构),保证了在10亿级用户规模下数据库依然高效运转。

美团的酒店业务曾分享过分库分表实践:起初一张订单表撑不住时,先按订单创建时间分成当前月表和历史表,后来用户量更大,又按照用户ID哈希拆库,同时引入中间件代理简化路由逻辑。

支付宝为了保证金融级一致性,没有大规模使用分片,而是采用OceanBase这样的分布式强一致数据库来支撑交易,但也投入巨量硬件资源和优化手段。这些案例体现出数据库架构需要根据业务性质选择不同策略:电商等可以通过分库分表加中间件方案来扩展,而金融等选择更强一致的新型数据库,各有权衡。

熔断、限流与降级

在分布式高并发系统中,熔断限流降级是保障系统稳定性的三大“保护伞”机制,属于服务治理的重要组成部分。它们分别对应:出现故障时及时熔断阻断错误传播、在高流量时进行限流保护系统负载、在非常情况下降级非核心功能保证核心功能可用。合理运用这些策略可以避免局部故障放大成全局崩溃,提升系统在极端条件下的健壮性。

限流

限流是指限制系统的并发请求量或处理速率,防止瞬间过高的流量压垮系统。常用的限流算法包括固定窗口计数滑动窗口令牌桶漏桶等:

  • 计数器法(固定窗口): 按时间窗口计数请求数量,例如每秒不超过N次。一旦计数达到上限,对超出的请求直接拒绝或排队等待到下个时间窗。这实现简单但会有临界突发问题(窗口边界处可能短时高于阈值)。

  • 滑动窗口计数: 改进固定窗口,将统计窗口细分为更小格并滑动计算,使限流更平滑。比如将1秒分成100个10ms子窗口滑动,避免临界点聚集大量请求。

  • 漏桶算法 (Leaky Bucket): 将请求放入一个容量固定的“桶”中,桶以恒定速度漏出请求进行处理,超过桶容量的请求要么丢弃要么排队等待。漏桶的特点是输出速率恒定,能平滑地处理请求,消除流量突刺。例如允许请求以恒定速度流出系统,即使短时间进来很多也不会一下子全部处理,而是排队按固定速率出桶​。漏桶实现了严格的流量整形。

  • 令牌桶算法 (Token Bucket): 系统以恒定速率产生令牌放入桶中,每个请求处理前必须拿到令牌,拿不到就被拒绝或等待。桶有最大令牌数限制,多余令牌溢出。令牌桶允许一定程度的突发流量(当桶中有累积令牌时可一次消费多个请求),但长时间平均速率不会超过产生令牌的速率​。多数场景令牌桶更灵活,支持突发请求且易于动态调整速率。

实际限流实现可以在网关层、服务层或客户端。网关限流往往对外部请求根据IP或API做全局限制;服务内部也可对调用量大的接口设置阈值。对于分布式部署,需要考虑全局限流(总请求多少)还是单节点限流(每节点多少)。如果要求全局阈值,需要有集中协调(比如Redis原子计数或专门的限流组件)。

限流的动作一般是拒绝请求或者排队等待处理。拒绝时应返回友好提示(如503错误或自定义错误码),避免用户无响应感知。在秒杀场景,会使用限流使得只有获得“抢购资格”的请求进入后端处理,其余直接告诉用户繁忙稍后重试。

熔断

熔断 (Circuit Breaker) 来自电路概念:当下游某个服务发生故障或者响应极慢时,上游服务在调用多次失败后,开启熔断开关,暂停对该服务的调用,一段时间内直接快速失败或使用备用方案,等下游恢复后再关闭熔断恢复调用。其目的是防止故障蔓延,保护系统的整体可用性。

典型的熔断策略由 Netflix 的 Hystrix 等库推广开来,通常包含三个状态:Closed(闭合)Open(断开)Half-Open(半开)。在Closed状态下正常调用并统计错误率;一旦连续错误数或错误率超过设定阈值,切换到Open熔断状态,此时对该服务的调用不再真正发送,而是直接失败快速返回(或Fallback)。经过一段时间冷却后,进入Half-Open状态,允许少量请求去试探下游是否恢复:如果成功率高则认为恢复正常,关闭熔断;若仍有错误则重新Open继续熔断休眠。通过这样的机制,避免了下游服务不堪重负时上游无限制地重试造成雪崩。熔断一般结合降级处理,即当断路打开时,上游服务需要提供Fallback逻辑,比如返回一个默认值、缓存值或友好提示给用户,而不是直接报错。

熔断触发的条件可以依据错误比例、连续超时次数等。需要选取合理的阈值。如果阈值太低,可能因偶尔几个超时就熔断,导致不必要的服务中断;太高则熔断介入不及时。面试中可以提Hystrix的默认值(例如10秒内20次请求中超过50%失败才熔断)作为参考。熔断还可应用于资源池:比如线程池队列满了也可熔断新的请求进入等。

应用场景

熔断适用于层级调用较深或者外部依赖的场景。例如服务A调用服务B,服务B又依赖数据库。如果数据库连接池耗尽,服务B会超时,这时A如果不停地等待重试,也会拖垮自己,进而请求积压影响其他功能。设置熔断后,A在检测到B连续超时后,会暂时停止调用B并直接走fallback逻辑(比如返回“服务暂不可用”),这样数据库问题不会拖垮A。等过了熔断窗口再去试,发现数据库好了再恢复调用。再如第三方支付接口经常遇到网络或对方系统问题,调用方可以在失败累积到一定程度后熔断,期间直接返回支付系统繁忙的提示给用户,避免继续耗费资源等待无效的响应。

降级

降级 (FallBack/Graceful Degradation) 是指在系统压力过大或部分功能失效的情况下,有选择地牺牲非核心功能,确保核心功能可用或降低整体负载。降级往往是人为预先规划的策略,当触发条件满足时执行。

降级有多种形式:

  • 功能关闭: 直接关闭某些可有可无的功能模块。例如电商大促时关闭推荐系统、评论区等,以节省服务器资源,把资源让给下单交易等核心功能。又如高峰期暂停一些批量统计任务,等过后再补上。

  • 降质服务: 提供简化版本的服务响应。比如当搜索服务压力过大时,只返回最基本的搜索结果而省略高级筛选和排序;地图服务降级时可能只给文字地址不加载地图图层。

  • 读取缓存或默认值: 当实时服务不可用时,降级为提供缓存的数据或静态页面。比如某商品详情加载实时评价失败时,可以降级使用最近缓存的一些评价数据或直接显示“暂无新评价”。再如支付系统的积分服务超时,前端降级为不等待积分结果,直接完成支付流程,积分稍后补计。

  • 限制用户操作: 在极端情况下,可能对外只开放部分用户权限。比如某次12306春运抢票系统压力极高时,暂时关闭了退票改签等次要功能,只允许购票操作,度过高峰再恢复。

降级策略通常需要在架构设计时就规划好“哪部分可以不要”。关键是确保业务核心路径不受影响。对于用户体验而言,降级应该是次优而不失礼的方案——不完美但能接受。比如网页某些区域显示“数据加载中”或提供简单版本,而不是整个页面崩溃刷不出来。

触发条件: 降级可以由系统监控指标触发(如CPU超过阈值、线程池排队过长、依赖服务不可用),也可以运维人工干预(在后台控制台一键关闭某功能)。一般会在监控到响应时间剧增或错误率上升时,逐步启动预定的降级措施。降级结束也需要有条件,比如流量恢复正常或故障修复后,手动或自动恢复全部功能。

场景实践

双11大促这类极端场景,各公司都有丰富的降级措施。例如淘宝会提前制定“三级降级方案”:第一阶段关闭部分个性化推荐和大图加载,第二阶段如订单峰值过高则简化订单确认流程(可能不支持优惠券实时计算等),第三阶段如果数据库压力极大甚至可以启用读缓存模式(只允许下单,不支持订单查询实时刷新)等等。这些措施按需执行,以保证用户至少能完成下单付款这个核心目标。

视频网站在弹幕服务器故障时,会降级停用弹幕一段时间,让视频播放不受影响。

地图导航如果实时路况获取失败,可以降级只提供静态地图而无实时路况。

对于支付系统,降级空间较小,因为核心就是付款,但也有:比如支付短信网关故障时,可以降级改用电话语音验证,或者临时取消某些银行的优惠核销功能,确保支付主流程不断。

常见坑点

  • 降级预案未测试: 如果平时不演练降级策略,到紧急关头可能发现降级开关有bug或者降级逻辑不正确。因此降级方案需要经过平时小流量验证,确保真降下去系统能按预期工作。

  • 恢复策略: 降级容易恢复难,尤其是涉及数据差异的降级(例如降级期间数据没记录完整)。要设计好恢复后的补偿措施,比如降级期间积压的数据如何补写回去,功能恢复时是否会有突发流量冲击。恢复宜逐步进行,避免一下子全开反而引发新一轮流量高峰。

  • 过度降级: 如果把核心功能也降了就违背初衷了。所以降级粒度要把握,不能一出问题就“一刀切”关停大块系统。应该按重要性从外围次要功能开始逐级降。

  • 用户通知: 降级会影响用户体验,要考虑用户的感受。适当的时候应在界面给予提示说明,如“当前评论功能维护中”或“推荐系统繁忙暂未加载”,避免用户误以为出bug。同时这样也减少重复请求(比如用户不停刷新尝试),反而对系统有利。

实践案例

Netflix作为微服务熔断降级理念的倡导者,其OSS工具包(Hystrix等)实现了完善的熔断和降级机制,确保当某些推荐服务出问题时,用户仍能正常观看影片,只是少了一些个性化推荐。

国内的支付宝/蚂蚁系统在双11期间也会预先规划多级降级策略,如当订单峰值超过某阈值时,非支付相关的营销活动服务会自动挂起,保障支付链路顺畅。

美团曾分享过外卖下单系统的弹性保障:高峰期如果配送ETA计算服务响应慢,就降级采用平均送达时间代替,保证用户能下单付款,不会因为附属信息卡住。

又如滴滴在春运时期会关闭顺风车等次要业务,把资源留给快车/出租车主业。

可以看到,降级是一把双刃剑,用得好能救急,用不好可能伤害体验甚至业务,设计时需要谨慎权衡并充分准备。

相关文章:

  • babel核心知识点
  • 理想星环OS选择NuttX作为MCU侧OS的核心原因分析​
  • 在 Linux 上安装 PNPM 的教程
  • 20250426在ubuntu20.04.2系统上打包NanoPi NEO开发板的FriendlyCore系统刷机eMMC的固件
  • Windows---注册表
  • Redis | Redis集群模式技术原理介绍
  • 如何选择游戏支付平台呢?
  • 基于OpenMV+STM32+OLED与YOLOv11+PaddleOCR的嵌入式车牌识别系统开发笔记
  • LINUX426 文件权限rwx、ugo、高级权限(冒险、强制、粘滞位)、chmod修改权限 umask默认权限 软件包
  • C++---类和对象(二)
  • WPF 上位机开发模板
  • 动态规划求解leetcode300.最长递增子序列(LIS)详解
  • NdrpEmbeddedPointerUnmarshall函数分析之第二次循环处理第二部分DomainSid
  • 三维重建(二十)——思路整理与第一步的进行
  • MongoDB 入门使用教程
  • 算法习题-力扣446周赛题解
  • 关于调度策略的系统性解析与物流机器人应用实践
  • 机器学习基础理论 - 频率派 vs 贝叶斯派
  • 在 Ubuntu24.04 LTS 上 Docker 部署英文版 n8n 和 部署中文版 n8n-i18n-chinese
  • 新增Webhook通知功能,文档目录树展示性能优化,zyplayer-doc 2.5.1 发布啦!
  • 广州一季度GDP为7532.51亿元,同比增长3%
  • 六部门:进一步优化离境退税政策扩大入境消费
  • 酒店保洁员调包住客港币,海南官方通报:成立调查组赴属地调查
  • 永辉超市一季度净利降近八成,未来12个月至18个月是改革成果集中释放期
  • 铜钴巨头洛阳钼业一季度净利润同比大增九成,最新宣布首度进军黄金矿产
  • 男子称喝中药治肺结节三个月后反变大增多,自贡卫健委回应