1 缓存与 NoSQL 在方案里各自回答什么
零售订单 API 经常同时承担商品展示、库存查询、创建订单和订单状态查询。它们看起来都在“读写数据”,承受的压力与允许的错误却不同:商品标题晚更新 30 秒通常可以解释,支付成功后订单仍显示未支付则需要处理,超卖更不能靠刷新页面解决。若把这些请求都送进同 1 个数据库,即使平均响应很快,大促期间少数热门商品也可能占满连接,让创建订单排队。
云上缓存主要把可重复读取的结果放到访问成本较低的位置,减少重复计算与后端访问。NoSQL 则是一组数据组织与访问方式,常见形态包括键值、文档和宽列。它可以承接某类业务数据的主要存储,也可以承接从订单事实生成的查询视图。缓存讲的是副本的角色,NoSQL 讲的是存储模型与接口,二者有交集,却不能互相替代定义。
本课沿用 1 条订单链路:用户打开商品页,提交订单,完成支付,再查询状态。关系数据库保存订单与支付相关的权威记录;缓存加速商品摘要和订单摘要;文档型存储承接按用户查询的订单展示视图。这里的安排只是便于讨论。若其他系统能够提供所需事务、条件写入和恢复能力,也可以承担权威数据,判断依据始终是业务不变量与可验证的服务保证。
方案里必须单独谈这层能力,是因为加速会引入新的状态:缓存可能没有值、有旧值、访问超时,或者整个分片不可用;查询视图可能还没收到刚提交的更新。每种状态都要对应明确行为。只写“接入缓存,提升性能”,等于把故障时谁保护数据库、谁保证订单正确、谁负责重建数据都留到了上线以后。

2 访问模式、数据模型与一致性边界
缓存是一份有用途的副本
缓存命中表示目标键当前存在且可使用,未命中表示需要按约定寻找其他来源。TTL 是条目的存活时长,到期通常只意味着缓存不再提供该值;具体删除时间取决于实现。TTL 不能证明值在存活期间始终正确,也不能替代订单关闭、退款等业务规则。将库存缓存 60 秒,不代表这 60 秒内可以凭缓存批准扣减。
还要区分过期、淘汰与持久化。过期依据时间,淘汰依据内存压力和策略,持久化试图在重启后恢复部分或全部数据。开启持久化的缓存依然可能因复制延迟、确认策略或备份间隔丢失最新写入。若某条数据不可丢,就必须说明写入何时被确认、确认覆盖哪些副本,以及故障恢复后能找回到什么位置,不能只看控制台上的“已开启”。
NoSQL 要从访问模式选择
键值模型适合“给出键,获取值”,例如通过订单号读取摘要。文档模型把订单收货地址、商品行和展示字段组织成文档,方便按文档整体读取;是否支持跨文档事务、索引和聚合,要逐项确认。宽列模型通常围绕分区键和排序键组织大量记录,适合提前知道查询入口的场景,但临时增加跨分区查询可能很昂贵。NoSQL 并不意味着没有结构,也不意味着天然支持任意规模。
先列出访问模式,再设计数据结构:按订单号查详情、按用户与时间查最近订单、按支付流水定位订单。每条路径都要回答键是什么、返回多少数据、是否需要排序、写入频率多高。为了查询而复制字段很常见,但要把更新来源、版本比较和重建办法一起设计。索引也会消耗空间与写入能力,不能把每个字段都建索引当作默认方案。
一致性、可用性和业务承诺
强一致读取与最终一致读取的差异,在订单链路里表现为“支付完成后立刻查询能看到什么”。最终一致可以满足许多列表与统计需求,但必须说明传播延迟目标和超时后的处理。读己之写表示用户能看到自己刚完成的写入,不自动等于所有用户同时看到相同结果。可用性则是请求能否按约定完成,成功返回旧数据也必须符合接口契约。
网络分区发生时,跨副本协调与继续服务之间存在约束,不能把 CAP 简化成日常任意选择 2 项,更不能由此断言所有 NoSQL 都弱一致。应检查具体产品在具体读写级别下的行为。云服务负责部分节点运维,应用仍负责数据模型、权限、超时、重试和容量。多可用区复制提升部分故障下的连续性,不能代替备份,也不能阻止错误删除传播。
3 旁路缓存、穿透击穿雪崩与回填竞态
旁路缓存的读取与更新
常见的 cache-aside,也叫旁路缓存:应用先读取缓存,命中就返回;未命中再读权威数据,并把结果回填缓存。回填失败通常不应把已经成功的数据库读取改成业务失败,但要记录失败并限制后续回源压力。缓存访问超时与键不存在必须分开统计,因为前者可能意味着共享依赖正在故障,继续逐请求回源会迅速放大影响。
更新时采用“提交权威数据,再删除缓存”能够减少直接维护多份值的复杂度,但它并不自动消除竞态。例如读取线程先读到旧数据,更新线程随后提交并删键,读取线程最后又把旧值填回。可结合短 TTL、版本比较、提交事件失效和定期校验约束旧值窗口。若目标是不允许读到旧状态,就应让该接口读取满足所需一致性的权威来源,或使用有明确保证的版本协议。
下面的动画跟踪同 1 个商品键的请求、回源和更新。留意“数据库已提交”和“缓存已更新”是不同的时间点。缓存中的版本号只有在写入端使用原子条件比较时才提供保护;先读版本、再普通写入仍然会发生竞争。

穿透、击穿与雪崩
缓存穿透是大量请求查询根本不存在的数据,导致每次都访问后端。可以先校验订单号格式、身份和访问范围,再为合法但不存在的结果设置较短的空值缓存。空值也会过时:创建成功后应失效相应空值,或确保其存活时间符合查询承诺。布隆过滤器可以排除部分不存在的键,但可能误判存在;数据同步不完整时还可能错误拒绝合法业务,必须设计更新与降级策略。
缓存击穿常指某个热门键失效后,大量并发请求同时回源。进程内请求合并可以让同进程的请求共享 1 次加载;跨进程互斥需要处理锁租期、持有者失败与等待上限,不能让等待队列无限增长。对允许短暂陈旧的数据,可以保留旧值并由后台刷新。旧值服务窗口必须有上限,库存扣减、支付确认等操作不能套用这条规则。
缓存雪崩往往涉及大量键集中失效或缓存服务整体不可用。TTL 加随机偏移可以分散到期时间,却不能解决整个集群失联。应用还需要回源并发上限、排队上限、超时与降级。预热则是提前加载经过评估的热门集合;全库预热既可能压垮数据库,也可能把冷数据挤进内存,降低真正热门数据的驻留比例。
分区、复制与热点
分区把数据分配到多个存储单元,复制让同份数据存在多个副本。分区键决定流量分布,而数据量均匀并不保证请求均匀。若把活动编号作为唯一分区键,大促可能让几乎所有写入集中在 1 个分区。为键添加桶号可以分散写入,但按活动查询时需要访问多个桶并合并结果,这增加请求数、排序工作和失败处理成本。
复制也有代价:同步确认通常增加写入等待,异步复制可能使副本读取短暂落后。切换主节点后,客户端可能遇到连接断开、请求结果未知和重新发现节点。此时“没收到成功”不能被当作“没有执行”。应用必须用幂等键、条件写入或业务唯一约束识别重试,尤其不能把自动重试直接放在余额扣减与库存减量之外。
用负载估算约束直觉
假设商品查询为 20000 次/秒,命中率为 95%,在忽略合并与重试的简化模型下,回源约为 1000 次/秒。若命中率降到 80%,回源变成 4000 次/秒。命中率只下降 15 个百分点,数据库负载却增加到 4 倍。它是否承受得住,要依据实测安全吞吐、连接数与查询成本判断,不能只看缓存平均延迟。
缓存容量也不能只把业务值长度相加。键、元数据、索引、内存碎片、副本和运维余量都会占用资源。TTL 更长可能减少回源,也会增加驻留数据并延长失效事件丢失后的旧值窗口;副本更多可能提高读取能力,却不一定缓解单个主分片的写热点。每个参数都要连接到业务约束与观测证据。
4 订单读路径:会话、库存展示与幂等键
把正确性留在可证明的位置
商品页上的“库存充足”是展示提示,提交订单时仍需执行权威库存检查。可采用带条件的原子更新,例如仅在可售量大于等于购买量时扣减,并检查实际影响结果。若库存位于独立服务或独立存储,订单与库存跨系统不能假设存在本地事务,要设计预占、确认、释放与补偿,并给每个状态转换提供幂等标识。
创建订单应由服务端识别业务幂等键。相同键与相同请求内容重试时,返回已有订单或处理状态;相同键对应不同内容时,应拒绝或明确报冲突。幂等记录、订单创建和库存动作之间如果存在故障窗口,就要依靠事务边界、状态机或恢复任务关闭窗口。只在缓存里存 10 分钟去重标记,无法证明 10 分钟后不会重复创建。
支付回调可能重复、乱序或延迟。更新应检查允许的状态转换与事件标识,不能让较旧事件覆盖已支付或已退款状态。缓存里的订单摘要可以带上业务版本和生成时间,便于判断延迟;客户端展示字段也应区分“支付处理中”与“未支付”。用户刚完成支付时,可在有限时间内读权威状态,或携带最低期望版本查询,超过等待预算就返回明确的处理中状态。
查询视图与事件传播
按用户查看最近订单可以使用独立查询视图,减少复杂关联查询。订单事务提交后再单独发送事件,存在“事务成功但事件没发出”的窗口。事务发件箱是一种常见办法:在同 1 个数据库事务中写订单和待发送事件,由转发器持续投递。消费者按事件标识去重,按业务版本处理乱序,失败进入可重放流程;消息确认成功也不代表所有视图已经更新。
查询视图必须能够由权威记录与保留的事件重建。删除字段、修改文档结构或更换分区键时,先考虑旧数据兼容、回填速率和切换条件。订单列表里的地址、电话等字段要按最小需要复制,缓存和日志也属于数据治理范围。缓存键应包含租户或权限相关边界,不能仅用订单号命中后直接返回给任意调用者。
连接管理同样是订单链路的一部分。应用实例扩容后,每实例连接池上限相加可能超过服务端容量。跨区访问会增加网络时间,短超时可能制造无效重试。应为整条请求设定截止时间,再分配缓存、数据库和下游预算;读取重试也要受剩余时间与总次数限制,避免嵌套重试把 1 次请求扩展成大量后端访问。
5 动手:Redis/条件写与 TTL 直觉
先在隔离测试环境建立 1 份配置记录:网络入口、认证方式、加密、分片与副本、最大连接数、内存或吞吐额度、备份周期、恢复目标和告警接收方。确认服务只能被授权的应用网络访问,应用使用独立身份。控制台显示实例健康,只说明部分基础设施检查通过;它没有替应用检查商品查询是否命中,也没有验证订单权限过滤是否正确。
下面用兼容常见键值接口的命令说明行为。示例中的商品摘要可被重新生成,价格是否可作为交易依据仍由业务服务确认。命令应通过已配置的加密连接与凭证管理执行,不把真实口令写进脚本或终端历史。
SET product:sku:482:summary '{"version":17,"title":"保温杯"}' EX 60
GET product:sku:482:summary
TTL product:sku:482:summary
DEL product:sku:482:summary
观察首次读取、重复读取、删除后再读取的差别。删除只移除副本,应用下一次可以重新回源。读取与 TTL 检查不是原子快照,执行期间键可能变化;不同实现对不存在键和永久键有不同的约定返回值,应查看所用接口说明。不要把测试环境的全库清空命令放进生产排查手册,也不要为了解决单键旧值而清空整个业务缓存。
再用与产品无关的伪请求理解条件写入。它表达“只有版本仍为 17 时,才写入版本 18”,要求存储端原子执行条件与修改。条件失败表示需要重新读取与决策,不能无条件覆盖。
UpdateItem(
key = {order_id: "O482"},
condition = "version = 17 AND status = 'PENDING'",
set = {status: "PAID", version: 18}
)
练习按用户与时间查最近 20 个订单,检查是否命中了预期索引或分区。查询少量结果也可能扫描大量数据,要同时记录读取单位、扫描条数、返回条数和延迟。分页优先使用服务提供的游标或明确的稳定排序键;并发新增订单时,单纯按页码偏移可能出现重复或遗漏。游标不能绕过授权检查,也不应暴露敏感内部字段。
最后演练 3 种异常:指定键不存在、缓存连接超时、条件写入结果未知。记录 API 返回、回源次数和日志中的请求标识。短暂切断测试客户端到缓存的连接时,验证回源并发受到限制;恢复连接后观察是否出现集中重建。备份恢复另做受控演练,检查恢复出的订单样本、数据时间点和依赖配置,不能以“备份任务成功”代替恢复成功。
6 观测:命中率、回源 QPS、热 key
观测应沿业务请求展开。入口记录接口、请求标识、耗时、结果和剩余时间预算;缓存记录操作类型、命中、未命中、超时、错误与回源原因;数据库记录查询类型、连接等待和执行时间;查询视图记录事件版本与消费延迟。日志使用脱敏后的键摘要,避免记录完整订单内容。高基数订单号适合受控日志或追踪,不宜直接作为监控指标标签。

命中率应按业务与操作分类,通常可定义为命中次数除以命中加未命中次数,同时把连接错误与超时独立展示。若把超时排除后只看命中率,缓存已经不可用时曲线仍可能很好看。还应区分正值命中与空值命中、主动回源与后台刷新,并观察请求合并后的真实回源并发。单个总体平均值容易掩盖热门 SKU 或某个分片的问题。
延迟至少看 p50、p95 和 p99,并同时查看请求量与错误率。缓存指标包括内存使用、淘汰速率、连接数、操作排队、热点分布与复制延迟;NoSQL 指标包括限流、分区吞吐、条件冲突、读取消耗和索引写入压力。业务指标则包括下单成功率、库存拒绝率、支付状态可见延迟。技术指标正常而业务状态落后,仍然是需要处理的故障。
告警必须连接动作。例如以经过压测确认的数据库安全回源额度为基准,当持续回源逼近额度且连接等待增长时触发保护;当视图延迟超过支付状态展示目标时,切换该接口读取路径或展示处理中。时间窗、阈值和解除条件需按流量波动制定,避免每次短暂抖动都呼叫值班人员。告警还要给出负责服务、相关面板、最近变更和处置入口。
追踪采样也要保留证据。常态可使用较低比例采样,异常、超时和结果未知的写入应按策略保留更多上下文。服务端执行时间短而客户端耗时长,可能指向连接池、网络或重试等待;读副本返回旧版本则应关联复制位置或版本记录。只有把时钟偏差与采集口径讲清楚,才能可靠地拼接跨服务时间线。
7 常见坑:缓存当真相、无界键空间、跨用户键
把缓存命中当作数据正确。 键存在只表示有副本。订单状态是否新、返回对象是否属于当前用户、文档结构是否与代码兼容,都需要单独保证。对缓存值增加模式版本可以帮助升级,但变更键前缀时也要评估冷启动流量,避免所有应用同时切换后集中回源。
给所有数据设置相同 TTL。 同批写入会导致同批过期,短 TTL 还可能让低访问频率的数据永远无法复用。应按数据更新率、允许陈旧时间和访问热度分类,对适用数据加入随机偏移。随机 TTL 只分散时间,不能解决旧值回填和失效消息丢失,仍要保留版本、补偿或校验机制。
以为加分片就能处理所有热点。 单键可能始终位于同 1 个分片。读热点可以考虑应用本地副本、请求合并或可接受的多副本读取;写热点则需要重新评估业务键和原子边界。把库存拆到多个桶后,如何判断总量、如何处理部分成功与退款归还,都是新增的正确性问题。
把分布式锁当作永久授权。 持有者暂停或网络阻塞后租期可能已经结束,恢复执行时另 1 个持有者正在工作。仅凭“曾拿到锁”不能安全修改外部资源。需要时使用递增的隔离令牌,并由被保护资源拒绝旧令牌;如果资源没有这种能力,应回到原子条件写入、数据库约束或适合该业务的串行处理。
重试没有上限且忽略幂等。 缓存失败、数据库失败和接口失败各重试 3 次,会出现调用倍增。应指定由哪层重试、哪些错误可重试、退避与随机抖动方式,以及总截止时间。写入超时后先用幂等标识查询结果,禁止未经判定再次执行非幂等扣减。拒绝请求有时是保护已接收订单的必要手段。
把托管服务理解成没有运维工作。 服务商可以修复节点,却不能自动修正不均匀分区、过度索引和错误权限。扩容可能需要时间,缩容或迁移也可能影响性能;计费可能同时涉及存储、请求、备份和跨区传输。容量评估要包含故障时减少副本或失去部分容量的情况,并通过演练确认客户端切换行为。
8 案例:大促命中率骤降与库存展示漂移
某零售系统在活动开始前记录到商品查询 20000 次/秒,缓存命中率约 97%,回源约 600 次/秒。数据库压测给出的当前配置安全读取额度为 1500 次/秒,并且必须为创建订单保留连接和计算资源。活动上线时批量刷新商品摘要,大量键具有相同到期时刻;热门商品失效后,回源迅速超过安全额度,订单写入开始等待连接。
处置先保护订单创建:限制商品查询回源并发与等待长度,对允许陈旧的商品文案短时提供旧值,对没有安全旧值的请求返回明确的暂不可用。库存和支付操作继续走各自的权威校验。接着停止导致同步失效的批量任务,按热度分批恢复缓存,给后续过期时间增加随机偏移,并对热门键启用有等待上限的请求合并。
事故期间有人建议“关闭缓存,全部查库”。按当时流量,这会把约 600 次/秒的正常回源放大到接近 20000 次/秒,因此不适合作为默认降级。缓存可选,并不代表后端具备全流量承接能力。若业务要求缓存故障后仍完整提供所有读取,就必须预先为后端准备对应容量,并通过受控故障演练证明。
恢复不能只看命中率回升。需要确认订单写入延迟、连接等待和错误率回到目标范围,缓存重建速率稳定,视图消费积压持续下降,再逐步放开限额。活动后还应重新评估热门键占比与冷启动过程,补充发布流程中的分批失效策略。该案例的根因是过期与回源行为集中叠加,不应仅归因于“机器不够大”。
9 排查案例:证据链
另 1 次故障表现为“支付页面成功,订单列表仍显示待支付”,持续约 40 秒后自行恢复。入口错误率很低,缓存命中率也正常。排查首先记录脱敏订单标识、用户标识、支付事件标识和请求时间,确认是同 1 笔交易,并检查接口究竟读取权威订单表、缓存摘要还是查询视图。避免一开始就删缓存,因为这会改变现场且可能掩盖真实路径。

追踪显示:支付回调在 12:00:01 完成,权威订单版本已经从 17 升到 18,事务发件箱也存在对应事件;列表请求在 12:00:04 返回版本 17。缓存记录显示其源头是查询视图,视图本身也只有版本 17,因此即使删除缓存,回填仍然得到旧状态。这个证据把问题范围从支付提交缩小到事件传播与视图更新。
继续检查事件投递、消费确认、重试和分区积压,发现某个消费分区被失败重试占据,支付事件在 12:00:39 才应用到视图。消费日志还表明较旧版本事件曾再次到达,但被条件更新拒绝,没有覆盖版本 18。由此可以区分传播延迟与乱序覆盖:它们都会导致展示异常,却需要不同修复。若日志缺少版本与事件标识,就很难得到这一判断。
即时措施是在支付完成后的有限时间内,让订单状态接口读取权威来源,并对列表保留更新中的提示,同时限制额外读取量。长期措施是处理阻塞事件、设置隔离重试与重放流程,并把视图延迟纳入业务告警。重放前确认消费者幂等且版本比较正确,恢复后抽样比对权威记录与视图,验证没有重复副作用。
证据链的终点应是可反驳的结论:权威写入何时成功、事件何时可投递、视图何时应用、缓存依据哪个版本返回、用户何时看到新状态。若缺少某段证据,就把它列为待补观测,不能用“最终一致所以正常”结束排查。最终一致描述机制,允许延迟多久、超时后怎么办,仍然由接口承诺和业务方案决定。
放进方案里
为零售订单 API 编写方案时,先列明权威数据与派生数据:订单、支付和库存状态由具备相应事务或条件更新能力的系统确认;商品摘要进入缓存,用户订单列表进入可重建的查询视图。每条读取路径写出访问键、返回上限、允许陈旧时间和授权位置,每条写入路径写出幂等标识、状态转换与结果未知后的查询办法。
缓存采用旁路读取,提交权威更新后触发失效,并通过版本规则、有限 TTL 与补偿任务约束旧值窗口。对商品文案允许短时旧值,对库存扣减和支付确认使用权威校验。事件使用事务发件箱可靠衔接,消费者执行去重与版本检查,查询视图具备重放、比对和重建流程。分区键按访问模式选择,并单独评估热门商品与大用户的流量集中。
容量部分填写峰值请求量、预期命中率、缓存不可用时的回源上限,以及数据库为订单写入保留的额度。超时、连接池、重试和排队预算必须覆盖应用扩容后的总量。对缓存超时、热门键失效和视图滞后分别定义限流、旧值服务、权威读取或明确失败的触发条件,恢复时分批放量,避免集中重建。
运维部分约定命中率口径、尾延迟、淘汰、热点、限流和视图延迟的告警与负责人。备份明确可接受的数据丢失范围和恢复时长,演练需验证实际恢复的数据。安全部分说明网络隔离、最小权限、凭证轮换与敏感字段保留。上线验收以压测和故障演练中的下单成功率、状态可见延迟及恢复结果为依据,同时记录成本估算和未满足的边界。
10 小结:走向 CDN 与 DNS(C18 前先完成监控/安全衔接)
- 能否指出每类订单数据的权威来源,并说明缓存或查询视图丢失后如何恢复?
- 商品展示、库存扣减与支付状态是否各有明确的一致性承诺?
- 是否处理缓存未命中、超时、旧值回填和大量键同时失效?
- 是否按访问模式设计分区与索引,并评估热门键和写入集中?
- 创建订单与支付回调是否具备幂等、条件更新和结果未知后的查询路径?
- 缓存故障时,回源是否受后端安全容量和整条请求截止时间限制?
- 能否用请求标识、事件标识和版本号串起完整排查证据?
- 是否验证了查询视图重建、备份恢复和分批恢复流量,而不仅是配置开关?
自测:商品查询峰值为 30000 次/秒,命中率由 98% 降到 90%,简化模型下回源会从 600 次/秒升至 3000 次/秒,即 5 倍。若后端安全额度只有 1200 次/秒,方案必须在回源到达该额度前实施保护;增加缓存 TTL 只能作为经过业务陈旧约束评估后的部分措施。
下一课转向 C17 云监控入门:把本章验收过的结论带进后续主题,写成可检查的方案约束。