百万QPS下的缓存设计(数据复制与一致性)

本文探讨了在系统QPS达到百万级别的场景下,如何通过缓存数据复制解决高可用性和线性扩展的问题。介绍了Master-Slave复制提高系统可用性以及L1缓存分担带宽压力的方法。

一旦一个系统的QPS达到百万,常规的分布式缓存也将遇到瓶颈,IO瓶颈,带宽瓶颈,可用性问题等,本篇将和大家探讨如何通过缓存数据复制解决系统的高可用及线性扩展问题。

一、百万QPS下对系统架构的需求

一方面是系统可用性的需求,互联网架构中分布式缓存的使用,极大的提高了系统的性能和吞吐,但同时使得系统的可用性极大的依赖于分布式缓存的可用性。

如果缓存数据只有一份,必然会有单点风险,即,这个缓存实例挂掉后,大量请求直接穿透到DB,在整体QPS达到百万的场景,将直接导致系统crash。

一种解决的方案就是增加一个具有相同数据的缓存实例,当某一个缓存实例出现问题时,可以有备份缓存实例顶上去,提高系统整体可用性。


另一方面系统可扩展性的需求,互联网的用户增长曲线非常陡峭,体现在系统压力上包括存储容量和请求并发量两个方面,对于存储容量问题,由于我们讨论的是缓存,只保留最热的数据就好了。对于请求并发量问题,上篇也讨论过通过将数据拆分到多个实例的方式来解决,但如果存在multi_get的场景,实例不能过多(一般5-8个)。那么问题来了,在既有multi_get场景不能部署更多实例,而请求量(包括Qps和带宽)又超过整体缓存并发容量的情况下,如何解决呢?

这种情况下,缓存数据复制就派上了用场。


二 什么是缓存数据复制

一般情况下的分布式缓存都会部署多个实例,这是把一份完整数据拆分到多个实例中,把具有完整一份缓存数据的多个实例我们称作一组。对于中小系统服务,一般有一组就可以了。但对于有特别需要(比如:上节中提到的可用性和扩展性的需要),就需要有多组。而由一组到多组缓存的建立和维护过程,就是我们所说的缓存数据复制。


三 如何进行缓存数据复制

根据不同的目的或需要,需要多组缓存配合协作的方式不同,有不同的缓存复制策略。

基于可用性的Master-Slave复制(如图)

就是正常前端请求首先请求到Master缓存上,如果Master缓存有则返回,没有则请求Slave缓存,如果Slave缓存中有则返回,并把这个请求的key-value回种到Master缓存中;如果Slave缓存中也没有,则请求DB,并把这个请求的key-value同时回种到Master/Slave缓存中。这种场景下,当任何一个缓存实例出现问题(crash或被flush),都会有相应的Slave缓存实例做备份,不会出现突然的大量请求穿透到DB。大大的提高系统的可用性。

然而,大家可能也注意到了,这种可用性的提升,需要增加一倍的缓存空间。于是,勤俭持家的同学们又设计出一种变种策略,互为Master-Slave复制。这是在多机房场景下,即保障系统可用性又不额外增加成本的方式。(如图)

这种情况下,A机房的Master缓存将作为B机房的Slave缓存,而B机房的Master缓存将作为A机房的Slave缓存。正常情况下,两个机房都采用Master-Slave方式,应该需要4组缓存,但采用了互为Master-Slave模式后,只需要2组,成本并没有增加。

基于线性扩展的缓存复制

在单组缓存实例个数固定的场景下,单组缓存能承受的请求峰值总是有瓶颈的。

对于Qps,假定单缓存实例安全Qps为6W/s,一组部署5个实例,单组的Qps理论峰值6W/s*5=30W/s,在请求流量与hash策略完全匹配的情况下可以达到30W/s,实际上经常会出现由于木桶效应导致实际可承受峰值小于理论峰值。不管怎样,一旦Qps需要超过这个情况,系统将无法再扩展支撑。

对于带宽,在千兆网卡的实例情况,理论带宽1Gbit/s=128MB/s,而实际上带宽超过80M就可能出现丢包等网络偶发不稳定情况。对于个别单次请求返回数据较大的情况,可能还没达到Qps的峰值,带宽就成为瓶颈了。在微博的feed场景下,就出现了这一情况。对于这类场景,除了架构上降低单次请求返回大小,就是通过缓存复制的方式,扩展出可线性扩展的L1缓存。(如图)

这样,前端请求首先会随机请求到一组L1缓存,如果这个L1缓存命中则返回,否则再请求到主缓存,如果命中,返回,同时将key-value回种到这个L1缓存中。如果主缓存中没有中,则穿透到DB,返回并同时回种到主缓存及刚才那个L1缓存。L1缓存可以有多组,很好的分担了带宽的压力,并且可以线性扩展。达到百万QPS,那都不是事儿!


四 数据一致性之CAP

讨论分布式系统,不得不说CAP理论:Consistency(一致性),Avaliability(可用性),Partition tolerance(分区容忍性)三个特性再分布式存储系统中,最多只能实现上面的两点。在互联网分布式系统中,我们首先不能容忍的是当有一台或几台实例挂掉后致使整个系统不可用,也就是分区容忍性必须得到满足,而相比一致性和可用性,我们会在更多的场景下选择可用性。对于一致性,在很多场景下只要达到最终一致性就可以了。下面我们就以我们探讨的分布式缓存系统为例,探讨下为什么一致性是可以妥协的。

一致性也称作原子性或事务性,表示所有操作在同一时间不可分割,对于我们分布式缓存的场景,数据的一致性要求数据写入DB和缓存(可能有多组)的操作必须在一个事务中,显然这个要求的实现成本是比较高的。但我们实际的场景中,并不真的需要绝对一致性(原子性)。只要让用户感觉一致就可以了。

一个经典的场景就是发微博流程,我们知道DB的读写性能相比缓存来讲要慢很多,所以为了提高微博发表的容量和速度,以及承受更大的发表峰值,微博平台采取了异步处理的方式。就是当用户发表微博时,首先写入主缓存和处理队列,队列写成功,后续的处理(比如写DB)就可以执行,如果缓存成功了,这条微博就可以被其他用户读取到,就可以认为用户发表成功了。至于后面的存储到DB及容错问题,就交给后面的处理程序搞定了。整个这一切,对用户是透明的,用户的感觉就是他的微博发表成功了,其他用户也能够正常看到。这就是典型的缓存和DB异步处理的场景。它并不要求这两个操作必须原子,打破了一致性的特性。

但是,这种不一致只允许在很短的一段时间内存在,最终需要保障缓存和DB达到最终的一致。因为一旦缓存过期,请求还是要穿透到DB,如果那时DB仍没有存储成功,将会面临用户的投诉。所以,这个异步需要较好的容错策略保障最终的数据一致性。所以,除了针对DB写入的容错和重试策略外,最开始写入的缓存也会是一个短期缓存,保证万一出现DB写入问题,这种不一致能够很快消除,达到数据的最终一致。

所以,在大数据的互联网系统场景中,都会优先保证Avaliability,Partition tolerance,而对于Consistency,只要保证最终一致性即可。

<think>嗯,用户想了解如何保证缓存数据库的数据一致性,这是一个分布式系统中很经典的问题。用户可能是后端开发工程师或者系统架构师,正在设计一个需要缓存的系统,遇到了数据一致性的困扰。从问题表述看,用户对技术概念有基本理解,但需要系统性的解决方案。 我之前提供的引用资料很全面,覆盖了四种同步策略(Cache-Aside、Read-Through/Write-Through、Write-Behind、Refresh-Ahead)、操作顺序的选择(先删缓存还是先更新数据库)、以及延时双删等关键技术点。用户应该需要更聚焦的实践指导,而不是理论罗列。 用户可能最关心的是实际场景下的选择依据。比如电商系统中商品库存这种高频更新数据,和用户资料这种低频更新数据,处理策略就完全不同。金融场景需要强一致性,而资讯类应用可以接受短暂不一致。 从技术实现角度,需要重点考虑几个维度:一致性强度要求(强一致还是最终一致)、系统吞吐量需求、是否允许数据丢失、团队运维能力等。比如延时双删虽然能解决部分并发问题,但实现复杂且依赖消息队列;而简单的TTL过期策略虽然容易实现,可能造成业务逻辑问题。 用户没有说明具体业务场景,所以我的回答需要覆盖通用方案。可以先用流程图展示核心策略对比,然后重点展开最常用的Cache-Aside模式,因为90%的场景都适用。对于关键细节如“延迟时间设置少毫秒合适”“重试机制如何设计”这些工程师实际编码时会卡住的问题,要给出具体参数参考。 特别要注意避免过度设计。有些文章会推荐Write-Behind这样复杂的模式,但对数应用来说完全是杀鸡用牛刀。应该强调“根据业务需求选择最简单有效的方案”这个原则。 最后需要提醒监控的重要性。很团队只关注方案实现,没建立缓存命中率、数据库延迟等监控项,等线上出问题时才发现不一致。可以建议在回答末尾补充监控指标清单。</think>保证缓存(如Redis)数据库的数据一致性分布式系统的核心挑战,需结合业务场景选择合适的策略。以下是关键方法及适用场景: --- ### **一、核心策略对比** | **策略** | **操作顺序** | **优点** | **缺点** | **适用场景** | |-------------------|-------------------------------|------------------------------|------------------------------|----------------------------| | **Cache-Aside** | 读:缓存数据库→回填<br>写:更新数据库→删缓存 | 高缓存命中率,实现简单 | 写后短暂不一致 | 通用场景(用户信息、商品详情) | | **Write-Through** | 写:同时更新缓存+数据库 | 强一致性 | 写性能低,缓存利用率低 | 配置类数据 | | **Write-Behind** | 写:更新缓存→异步更新数据库 | 高写入性能 | 可能丢数据,实现复杂 | 高频写(点赞数、日志) | | **Refresh-Ahead** | 缓存过期前主动刷新 | 避免缓存击穿 | 预测不准可能浪费资源 | 热点数据预热 | --- ### **二、解决并发不一致的实践方案** 1. **延迟双删(针对Cache-Aside)** ```java public void updateData(Data data) { // 1. 先删除缓存 redis.delete(key); // 2. 更新数据库 db.update(data); // 3. 延迟后再删一次(覆盖并发读导致的脏数据) Thread.sleep(100); redis.delete(key); } ``` - **为何有效**:覆盖「读请求在写操作中加载旧数据缓存」的场景 - **延迟时间**:通常为业务读请求耗时 + 缓冲时间(如100-500ms) - **读写分离场景**:需等待主从同步完成后再删缓存(通过监控复制延迟确定等待时间) 2. **异步补偿机制** - 用消息队列(如Kafka)记录删缓存失败的操作,异步重试 - 示例流程: ```mermaid graph LR A[删除缓存失败] --> B[写入消息队列] B --> C{消费者重试删除} C -->|成功| D[确认消息] C -->|失败| E[延迟重试] ``` --- ### **三、强一致性场景方案** 1. **分布式锁** - 写操作时加锁 → 更新DB → 删缓存 → 释放锁 - 缺点:性能下降,复杂度高(仅限金融/订单等敏感数据) 2. **数据库事务日志监听** - 通过MySQL Binlog或Canal监听数据变更 → 删除/更新缓存 - 优点:彻底解耦,保证顺序性 - 缺点:架构复杂,延迟较高 --- ### **四、通用设计原则** 1. **设置合理TTL** - 所有缓存必须设置过期时间(兜底最终一致性) - 根据数据变更频率调整:高频数据TTL短(如30s),低频可长(如24h) 2. **避免Value** - Value更新易导致不一致,可拆分为小字段独立缓存 3. **降级方案** - 缓存故障时直连数据库,避免雪崩(但需牺牲性能) --- ### **五、监控告警** | **指标** | **目标值** | **作用** | |--------------------|------------------|------------------------------| | 缓存命中率 | >90% | 评估缓存有效性 | | 数据库读QPS | 突增时告警 | 发现缓存穿透/击穿 | | 消息队列积压 | <1000条 | 监控异步补偿健康度 | | 主从复制延迟 | <200ms | 确保延迟双删有效性 | > **关键结论**: > - **强一致性**:优先选「Cache-Aside + 延迟双删 + 事务日志监听」 > - **最终一致性**:用「Cache-Aside + TTL + 异步补偿」降低成本 > - **高频写场景**:Write-Behind模式(接受短暂不一致) > - *无银弹方案,需根据业务容忍度权衡性能一致性*[^1][^3][^4] --- **
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值