一、缓存的核心定位与架构成本
缓存是数据交换的缓冲区,本质为高读写性能的临时数据存储介质。在计算机体系结构中,缓存广泛存在于浏览器端、应用服务器、数据库引擎、CPU寄存器及磁盘控制器中。其核心设计是以空间换时间,通过前置热点数据,阻断低频请求对底层存储的直接穿透。
缓存的核心作用
- 降低后端负载:将高频读请求拦截在内存层,避免数据库承担重复的磁盘I/O与计算开销,保护核心存储不被突发流量击垮。
- 提升读写效率,降低响应时间:内存访问延迟通常在纳秒至微秒级,而数据库涉及网络往返、锁竞争与磁盘寻道,延迟在毫秒级。
引入缓存的隐性成本
- 数据一致性成本:数据库是持久化的唯一真相源,缓存是其副本。双写场景下,网络抖动、事务回滚或进程宕机极易导致双端数据状态发散,系统需额外投入资源治理脏数据。
- 代码维护成本:业务逻辑需显式处理缓存命中、未命中回源、序列化反序列化、失效策略及降级兜底。链路延长导致代码分支增多,调试复杂度呈指数级上升。
- 运维成本:需建立完整的缓存集群监控、内存水位预警、热点Key探测、慢查询分析及故障切换预案。缓存节点扩缩容与数据迁移对运维团队提出更高要求。
二、基于Redis的懒加载缓存接入
下面我们给出一个示例对商品的查询我们加入缓存。在业务系统初期,为了快速缓解数据库的查询压力,开发者通常采用最基础的懒加载(Lazy Loading)模型。该模型的核心思想是"按需加载",即仅在数据首次被请求时触发回源逻辑,将查询结果暂存至Redis中,后续请求直接从缓存获取,避免重复访问数据库。
根据id查询商铺缓存的流程图
从客户端请求到缓存命中、未命中回源及数据回填。
#mermaid-svg-NdD1G4wJuhwqSKJg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NdD1G4wJuhwqSKJg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NdD1G4wJuhwqSKJg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NdD1G4wJuhwqSKJg .error-icon{fill:#552222;}#mermaid-svg-NdD1G4wJuhwqSKJg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NdD1G4wJuhwqSKJg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NdD1G4wJuhwqSKJg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NdD1G4wJuhwqSKJg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NdD1G4wJuhwqSKJg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NdD1G4wJuhwqSKJg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NdD1G4wJuhwqSKJg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NdD1G4wJuhwqSKJg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NdD1G4wJuhwqSKJg .marker.cross{stroke:#333333;}#mermaid-svg-NdD1G4wJuhwqSKJg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NdD1G4wJuhwqSKJg p{margin:0;}#mermaid-svg-NdD1G4wJuhwqSKJg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NdD1G4wJuhwqSKJg .cluster-label text{fill:#333;}#mermaid-svg-NdD1G4wJuhwqSKJg .cluster-label span{color:#333;}#mermaid-svg-NdD1G4wJuhwqSKJg .cluster-label span p{background-color:transparent;}#mermaid-svg-NdD1G4wJuhwqSKJg .label text,#mermaid-svg-NdD1G4wJuhwqSKJg span{fill:#333;color:#333;}#mermaid-svg-NdD1G4wJuhwqSKJg .node rect,#mermaid-svg-NdD1G4wJuhwqSKJg .node circle,#mermaid-svg-NdD1G4wJuhwqSKJg .node ellipse,#mermaid-svg-NdD1G4wJuhwqSKJg .node polygon,#mermaid-svg-NdD1G4wJuhwqSKJg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NdD1G4wJuhwqSKJg .rough-node .label text,#mermaid-svg-NdD1G4wJuhwqSKJg .node .label text,#mermaid-svg-NdD1G4wJuhwqSKJg .image-shape .label,#mermaid-svg-NdD1G4wJuhwqSKJg .icon-shape .label{text-anchor:middle;}#mermaid-svg-NdD1G4wJuhwqSKJg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NdD1G4wJuhwqSKJg .rough-node .label,#mermaid-svg-NdD1G4wJuhwqSKJg .node .label,#mermaid-svg-NdD1G4wJuhwqSKJg .image-shape .label,#mermaid-svg-NdD1G4wJuhwqSKJg .icon-shape .label{text-align:center;}#mermaid-svg-NdD1G4wJuhwqSKJg .node.clickable{cursor:pointer;}#mermaid-svg-NdD1G4wJuhwqSKJg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NdD1G4wJuhwqSKJg .arrowheadPath{fill:#333333;}#mermaid-svg-NdD1G4wJuhwqSKJg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NdD1G4wJuhwqSKJg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NdD1G4wJuhwqSKJg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NdD1G4wJuhwqSKJg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NdD1G4wJuhwqSKJg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NdD1G4wJuhwqSKJg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NdD1G4wJuhwqSKJg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NdD1G4wJuhwqSKJg .cluster text{fill:#333;}#mermaid-svg-NdD1G4wJuhwqSKJg .cluster span{color:#333;}#mermaid-svg-NdD1G4wJuhwqSKJg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NdD1G4wJuhwqSKJg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NdD1G4wJuhwqSKJg rect.text{fill:none;stroke-width:0;}#mermaid-svg-NdD1G4wJuhwqSKJg .icon-shape,#mermaid-svg-NdD1G4wJuhwqSKJg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NdD1G4wJuhwqSKJg .icon-shape p,#mermaid-svg-NdD1G4wJuhwqSKJg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NdD1G4wJuhwqSKJg .icon-shape .label rect,#mermaid-svg-NdD1G4wJuhwqSKJg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NdD1G4wJuhwqSKJg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NdD1G4wJuhwqSKJg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NdD1G4wJuhwqSKJg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 命中
未命中
不存在
存在
开始
提交商铺id
判断缓存
是否命中
返回商铺信息
结束
根据id查询数据库
判断商铺
是否存在
返回404
将商铺数据
写入Redis
返回商铺信息
基础接入代码实现
python
from fastapi import FastAPI, HTTPException
import redis.asyncio as aioredis
import json
app = FastAPI()
# 初始化Redis异步客户端,连接本地默认端口
redis_client = aioredis.Redis(host="127.0.0.1", port=6379, db=0, decode_responses=True)
async def query_shop_from_db(shop_id: int) -> dict | None:
# 模拟ORM查询:SELECT * FROM shop WHERE id = ?
mock_db = {1: {"id": 1, "name": "星巴克旗舰店", "type": 1}}
return mock_db.get(shop_id)
@app.get("/shops/{shop_id}")
async def get_shop(shop_id: int):
cache_key = f"shop:{shop_id}"
# 1. 优先查询Redis缓存
cached_data = await redis_client.get(cache_key)
if cached_data:
return json.loads(cached_data)
# 2. 缓存未命中,转向数据库查询
shop = await query_shop_from_db(shop_id)
if not shop:
raise HTTPException(status_code=404, detail="Shop not found")
# 3. 将数据库查询结果直接写入Redis
await redis_client.set(cache_key, json.dumps(shop))
return shop
潜在问题与架构隐患分析
尽管该实现快速完成了缓存接入,并有效降低了数据库的重复读取压力,但从工程架构视角审视,此方案存在致命的局限性。代码中执行redis_client.set时未设置任何过期时间或失效机制,这意味着缓存数据在Redis中为永久存储。一旦后端数据库中的商铺信息发生变更(如名称修改、营业状态调整、价格更新),缓存层无法感知该变化,仍将持续向客户端返回历史数据。此时,数据库与缓存之间的数据一致性被彻底打破,且该不一致状态将永久持续,直至运维人员手动执行清理命令或缓存节点重启。
单纯的基础懒加载仅解决了"读性能"与"数据库减负"问题,却引入了"写一致性"的新挑战。在真实生产环境中,数据变更是常态,缓存若无法与底层数据源保持同步,将直接导致业务逻辑错乱、用户体验受损甚至资损风险。系统必须建立一套明确的缓存失效、更新与兜底机制,才能在享受缓存高性能的同时,保障数据状态的可控与收敛。
三、缓存更新策略的演进与工程选型
缓存与数据库的同步机制直接决定系统的数据一致性水位与架构复杂度。工程实践中主要存在三种策略,需根据业务容忍度与资源预算进行选型。
内存淘汰机制
依赖Redis配置的maxmemory与淘汰算法(如allkeys-lru、volatile-lfu)。当实例内存触及阈值时,Redis自动踢出部分Key。下次查询时触发懒加载重建缓存。该策略无需业务代码干预,维护成本为零,但一致性极差,数据存活时间完全不可控。适用于店铺分类列表、首页广告位、静态字典等低频修改、读多写少、允许短暂不一致的场景。
超时剔除(TTL)
业务在写入缓存时通过EXPIRE或SETEX命令设置固定存活时间。到期后Redis执行惰性删除或定期扫描清理。该策略将一致性控制为时间窗口内的可预期偏差,维护成本极低。适用于绝大多数常规业务,通常作为主动更新机制的兜底防线,防止因主动更新失败或异常导致缓存永久脏化。
主动更新
业务逻辑在修改数据库的同时,显式执行缓存的增、删、改操作。该策略由应用层强管控双端状态,一致性表现最优,但需处理并发竞争、事务边界与失败重试,开发与联调成本显著上升。适用于店铺详情、库存扣减、用户核心资料、订单状态等对数据时效性要求极高的核心链路。
三大缓存更新策略对比
下表三种策略的对比总结。
| 策略 | 说明 | 一致性 | 维护成本 | 业务场景 |
|---|---|---|---|---|
| 内存淘汰 | 不用自己维护,利用Redis内存淘汰机制,内存不足时自动淘汰部分数据。下次查询时更新缓存。 | 差 | 无 | 低一致性需求(如店铺类型查询) |
| 超时剔除 | 给缓存数据添加TTL时间,到期后自动删除缓存。下次查询时更新缓存。 | 一般 | 低 | 常规业务兜底,配合主动更新使用 |
| 主动更新 | 编写业务逻辑,在修改数据库的同时,更新缓存。 | 好 | 高 | 高一致性需求(如店铺详情查询) |
工程选型准则
低一致性需求:直接采用内存淘汰或固定TTL 。
高一致性需求:必须采用主动更新为主、超时剔除为兜底的组合策略。主动更新保障数据变更后的快速收敛,TTL提供异常场景下的自动自愈能力,两者结合可在复杂分布式环境中实现一致性与可用性的最佳平衡。
四、主动更新模式下的架构设计
主动更新并非单一实现方式,其底层架构设计直接影响系统性能、一致性与扩展能力。主流存在三种设计模式。
Cache Aside Pattern(旁路缓存)
由缓存调用者(业务应用)直接管理数据库与缓存的双向交互。读操作遵循懒加载,写操作在更新数据库后显式失效缓存。该模式架构扁平,无需引入额外中间件,业务层可精准控制读写路径与重试逻辑。因其实现成本可控、性能损耗低,成为90%以上企业微服务架构的默认选型。
Read/Write Through Pattern(穿透缓存)
缓存层与数据库层整合为统一服务,对外暴露标准API。业务调用者仅与缓存服务交互,无需感知底层数据库存在。读未命中时,缓存服务内部自动回源并回填;写操作由缓存服务同步持久化至数据库。该模式将一致性治理完全下沉至基础设施层,业务开发极简。但缓存服务成为全链路单点瓶颈,同步写库会显著拉低写性能。
Write Behind Caching Pattern(异步回写)
业务所有CRUD操作仅针对缓存执行。后台由独立线程或消息队列按时间窗口或数据量阈值,批量将内存变更异步刷入数据库。该模式将写性能压榨至极致,适用于短视频点赞计数、直播间在线人数、日志流水等允许最终一致性、追求高吞吐的场景。致命风险在于缓存宕机或异步线程阻塞将导致数据永久丢失。
主动更新模式结构对比
| 模式 | 数据流向控制 | 缓存与DB交互者 | 适用架构 |
|---|---|---|---|
| Cache Aside | 业务层控制 | 业务应用 | 绝大多数分布式系统 |
| Read/Write Through | 缓存层控制 | 缓存服务代理 | 自研缓存中间件/网关 |
| Write Behind | 异步线程控制 | 缓存 + 异步队列 | 高并发计数/日志类场景 |
五、Cache Aside 模式下的并发冲突
在Cache Aside模式中,写操作的执行细节直接决定系统在并发环境下的数据一致性表现。需重点攻克三个核心问题。
问题一:每次数据库变更后,是"删除缓存"还是"更新缓存"?
- 更新缓存 :每次数据库变更后,业务代码重新计算新值并执行
SET覆盖旧缓存。该方案存在严重缺陷。其一,无效写放大 :若数据被频繁修改但无人读取,大量CPU与网络带宽消耗在无意义的缓存覆写上。其二,并发覆盖风险 :多线程并发修改不同字段时,后执行的SET会直接覆盖先执行的变更,导致部分字段丢失。 - 删除缓存 :数据库变更后仅执行
DEL操作,使缓存失效。下次读请求未命中时自动回源加载最新数据。该方案本质是延迟更新策略,彻底规避无效写与字段覆盖问题,将一致性压力转移至读路径,大幅降低架构复杂度与并发冲突概率。工业界统一采用删除缓存作为标准实践。
问题二:"先删除缓存,再更新数据库"还是"先更新数据库,再删除缓存"?
缓存与数据库的操作顺序不同,在并发交错下将产生截然不同的数据状态。
先删除缓存,再更新数据库
此路径存在严重的逻辑漏洞,会导致永久不一致。
数据库 Redis缓存 线程2(读请求) 线程1(写请求) 数据库 Redis缓存 线程2(读请求) 线程1(写请求) #mermaid-svg-Kw9vnC3i0aVCK6Rk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Kw9vnC3i0aVCK6Rk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .error-icon{fill:#552222;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .marker.cross{stroke:#333333;}#mermaid-svg-Kw9vnC3i0aVCK6Rk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Kw9vnC3i0aVCK6Rk p{margin:0;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Kw9vnC3i0aVCK6Rk text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Kw9vnC3i0aVCK6Rk .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Kw9vnC3i0aVCK6Rk #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .sequenceNumber{fill:white;}#mermaid-svg-Kw9vnC3i0aVCK6Rk #sequencenumber{fill:#333;}#mermaid-svg-Kw9vnC3i0aVCK6Rk #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .messageText{fill:#333;stroke:none;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .labelText,#mermaid-svg-Kw9vnC3i0aVCK6Rk .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .loopText,#mermaid-svg-Kw9vnC3i0aVCK6Rk .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Kw9vnC3i0aVCK6Rk .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .noteText,#mermaid-svg-Kw9vnC3i0aVCK6Rk .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .actorPopupMenu{position:absolute;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Kw9vnC3i0aVCK6Rk .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Kw9vnC3i0aVCK6Rk .actor-man circle,#mermaid-svg-Kw9vnC3i0aVCK6Rk line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Kw9vnC3i0aVCK6Rk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 缓存进入真空期 T2持锁等待序列化,上下文切换 灾难:DB=20,Cache=10(永久不一致) 1. 删除缓存(key=shop:1)12. 查询缓存(未命中)23. 查询数据库(获取旧值 v=10)34. 更新数据库(v=20)45. 将旧值 v=10 写入缓存5
解析:T1执行删除后,缓存清空。T2立即进入读链路,查询缓存未命中后转向数据库获取旧值。在T2进行数据序列化或网络传输期间,T1完成数据库更新。T2恢复执行后,将已过时的旧值写回缓存。此时数据库为新值,缓存为旧值,且无任何机制触发缓存再次失效。该不一致状态将永久持续,直至人工干预或依赖外部TTL清理。高并发场景下复现概率极高。
先更新数据库,再删除缓存
虽然存在极小概率窗口,但具备自愈能力。
数据库 Redis缓存 线程2(写请求) 线程1(读请求) 数据库 Redis缓存 线程2(写请求) 线程1(读请求) #mermaid-svg-SsudKzexFfJustYn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SsudKzexFfJustYn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SsudKzexFfJustYn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SsudKzexFfJustYn .error-icon{fill:#552222;}#mermaid-svg-SsudKzexFfJustYn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SsudKzexFfJustYn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SsudKzexFfJustYn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SsudKzexFfJustYn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SsudKzexFfJustYn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SsudKzexFfJustYn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SsudKzexFfJustYn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SsudKzexFfJustYn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SsudKzexFfJustYn .marker.cross{stroke:#333333;}#mermaid-svg-SsudKzexFfJustYn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SsudKzexFfJustYn p{margin:0;}#mermaid-svg-SsudKzexFfJustYn .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-SsudKzexFfJustYn text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-SsudKzexFfJustYn .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-SsudKzexFfJustYn .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-SsudKzexFfJustYn .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-SsudKzexFfJustYn .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-SsudKzexFfJustYn #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-SsudKzexFfJustYn .sequenceNumber{fill:white;}#mermaid-svg-SsudKzexFfJustYn #sequencenumber{fill:#333;}#mermaid-svg-SsudKzexFfJustYn #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-SsudKzexFfJustYn .messageText{fill:#333;stroke:none;}#mermaid-svg-SsudKzexFfJustYn .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-SsudKzexFfJustYn .labelText,#mermaid-svg-SsudKzexFfJustYn .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-SsudKzexFfJustYn .loopText,#mermaid-svg-SsudKzexFfJustYn .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-SsudKzexFfJustYn .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-SsudKzexFfJustYn .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-SsudKzexFfJustYn .noteText,#mermaid-svg-SsudKzexFfJustYn .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-SsudKzexFfJustYn .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-SsudKzexFfJustYn .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-SsudKzexFfJustYn .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-SsudKzexFfJustYn .actorPopupMenu{position:absolute;}#mermaid-svg-SsudKzexFfJustYn .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-SsudKzexFfJustYn .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-SsudKzexFfJustYn .actor-man circle,#mermaid-svg-SsudKzexFfJustYn line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-SsudKzexFfJustYn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} T1完成回源,准备执行写缓存前发生调度延迟 若缓存本为空,删除操作幂等无影响 短暂不一致:DB=20,Cache=10 1. 查询缓存(未命中)12. 查询数据库(获取旧值 v=10)23. 更新数据库(v=20)34. 删除缓存(key=shop:1)45. 将旧值 v=10 写入缓存5
逐帧解析 :T1完成数据库回源获取旧值,在准备写入缓存的瞬间发生线程调度暂停。T2趁机介入,完成数据库更新并删除缓存。T1恢复后,将旧值写入Redis。此时确实出现短暂的数据不一致。但该方案被业界广泛采纳的核心原因在于:第一,T1从查库结束到写缓存完成的窗口通常在0.1至0.5毫秒,T2的事务提交+网络RTT+Redis ACK需2至5毫秒,两者精确重叠的概率低于万分之一。第二,即使发生重叠,脏数据也仅存在于极短时间窗口内。第三,结合TTL兜底机制,该脏数据将在设定周期后自动过期,下次查询自然加载新值,系统具备自愈能力。
问题三:如何保障双端操作的原子性?
单体架构下,可通过本地事务将数据库更新与缓存删除包裹在同一执行上下文中,利用ACID特性保证同成功或同失败。
分布式架构下,本地事务无法跨越Redis节点。生产环境通常放弃强一致性事务,转而采用最终一致性方案。主流做法包括:引入Canal监听MySQL Binlog,解析数据变更后投递至消息队列,由消费者异步执行缓存删除;或采用可靠消息表机制,写库成功后记录补偿任务,定时任务扫描重试直至缓存删除成功。
六、最终方案
综合上述理论推演与工程约束,缓存更新的最佳实践已形成标准化规范。
读操作严格遵循命中直接返回 -> 未命中查库 -> 写入缓存并设定超时时间。
#mermaid-svg-3EgzFNroHGEv88Dw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3EgzFNroHGEv88Dw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3EgzFNroHGEv88Dw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3EgzFNroHGEv88Dw .error-icon{fill:#552222;}#mermaid-svg-3EgzFNroHGEv88Dw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3EgzFNroHGEv88Dw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3EgzFNroHGEv88Dw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3EgzFNroHGEv88Dw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3EgzFNroHGEv88Dw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3EgzFNroHGEv88Dw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3EgzFNroHGEv88Dw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3EgzFNroHGEv88Dw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3EgzFNroHGEv88Dw .marker.cross{stroke:#333333;}#mermaid-svg-3EgzFNroHGEv88Dw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3EgzFNroHGEv88Dw p{margin:0;}#mermaid-svg-3EgzFNroHGEv88Dw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3EgzFNroHGEv88Dw .cluster-label text{fill:#333;}#mermaid-svg-3EgzFNroHGEv88Dw .cluster-label span{color:#333;}#mermaid-svg-3EgzFNroHGEv88Dw .cluster-label span p{background-color:transparent;}#mermaid-svg-3EgzFNroHGEv88Dw .label text,#mermaid-svg-3EgzFNroHGEv88Dw span{fill:#333;color:#333;}#mermaid-svg-3EgzFNroHGEv88Dw .node rect,#mermaid-svg-3EgzFNroHGEv88Dw .node circle,#mermaid-svg-3EgzFNroHGEv88Dw .node ellipse,#mermaid-svg-3EgzFNroHGEv88Dw .node polygon,#mermaid-svg-3EgzFNroHGEv88Dw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3EgzFNroHGEv88Dw .rough-node .label text,#mermaid-svg-3EgzFNroHGEv88Dw .node .label text,#mermaid-svg-3EgzFNroHGEv88Dw .image-shape .label,#mermaid-svg-3EgzFNroHGEv88Dw .icon-shape .label{text-anchor:middle;}#mermaid-svg-3EgzFNroHGEv88Dw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3EgzFNroHGEv88Dw .rough-node .label,#mermaid-svg-3EgzFNroHGEv88Dw .node .label,#mermaid-svg-3EgzFNroHGEv88Dw .image-shape .label,#mermaid-svg-3EgzFNroHGEv88Dw .icon-shape .label{text-align:center;}#mermaid-svg-3EgzFNroHGEv88Dw .node.clickable{cursor:pointer;}#mermaid-svg-3EgzFNroHGEv88Dw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3EgzFNroHGEv88Dw .arrowheadPath{fill:#333333;}#mermaid-svg-3EgzFNroHGEv88Dw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3EgzFNroHGEv88Dw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3EgzFNroHGEv88Dw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3EgzFNroHGEv88Dw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3EgzFNroHGEv88Dw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3EgzFNroHGEv88Dw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3EgzFNroHGEv88Dw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3EgzFNroHGEv88Dw .cluster text{fill:#333;}#mermaid-svg-3EgzFNroHGEv88Dw .cluster span{color:#333;}#mermaid-svg-3EgzFNroHGEv88Dw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3EgzFNroHGEv88Dw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3EgzFNroHGEv88Dw rect.text{fill:none;stroke-width:0;}#mermaid-svg-3EgzFNroHGEv88Dw .icon-shape,#mermaid-svg-3EgzFNroHGEv88Dw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3EgzFNroHGEv88Dw .icon-shape p,#mermaid-svg-3EgzFNroHGEv88Dw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3EgzFNroHGEv88Dw .icon-shape .label rect,#mermaid-svg-3EgzFNroHGEv88Dw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3EgzFNroHGEv88Dw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3EgzFNroHGEv88Dw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3EgzFNroHGEv88Dw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 命中
未命中
不存在
存在
开始
客户端请求查询商铺
查询Redis缓存
缓存是否命中?
反序列化JSON
返回数据
查询数据库
数据是否存在?
返回404
将数据写入Redis
**关键:设定TTL过期时间**
返回数据
写操作严格遵循先更新数据库 -> 再删除缓存,并通过事务或异步补偿保障操作原子性。
#mermaid-svg-vILFHKGtxIXCvNKJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vILFHKGtxIXCvNKJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vILFHKGtxIXCvNKJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vILFHKGtxIXCvNKJ .error-icon{fill:#552222;}#mermaid-svg-vILFHKGtxIXCvNKJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vILFHKGtxIXCvNKJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vILFHKGtxIXCvNKJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vILFHKGtxIXCvNKJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vILFHKGtxIXCvNKJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vILFHKGtxIXCvNKJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vILFHKGtxIXCvNKJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vILFHKGtxIXCvNKJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vILFHKGtxIXCvNKJ .marker.cross{stroke:#333333;}#mermaid-svg-vILFHKGtxIXCvNKJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vILFHKGtxIXCvNKJ p{margin:0;}#mermaid-svg-vILFHKGtxIXCvNKJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vILFHKGtxIXCvNKJ .cluster-label text{fill:#333;}#mermaid-svg-vILFHKGtxIXCvNKJ .cluster-label span{color:#333;}#mermaid-svg-vILFHKGtxIXCvNKJ .cluster-label span p{background-color:transparent;}#mermaid-svg-vILFHKGtxIXCvNKJ .label text,#mermaid-svg-vILFHKGtxIXCvNKJ span{fill:#333;color:#333;}#mermaid-svg-vILFHKGtxIXCvNKJ .node rect,#mermaid-svg-vILFHKGtxIXCvNKJ .node circle,#mermaid-svg-vILFHKGtxIXCvNKJ .node ellipse,#mermaid-svg-vILFHKGtxIXCvNKJ .node polygon,#mermaid-svg-vILFHKGtxIXCvNKJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vILFHKGtxIXCvNKJ .rough-node .label text,#mermaid-svg-vILFHKGtxIXCvNKJ .node .label text,#mermaid-svg-vILFHKGtxIXCvNKJ .image-shape .label,#mermaid-svg-vILFHKGtxIXCvNKJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-vILFHKGtxIXCvNKJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vILFHKGtxIXCvNKJ .rough-node .label,#mermaid-svg-vILFHKGtxIXCvNKJ .node .label,#mermaid-svg-vILFHKGtxIXCvNKJ .image-shape .label,#mermaid-svg-vILFHKGtxIXCvNKJ .icon-shape .label{text-align:center;}#mermaid-svg-vILFHKGtxIXCvNKJ .node.clickable{cursor:pointer;}#mermaid-svg-vILFHKGtxIXCvNKJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vILFHKGtxIXCvNKJ .arrowheadPath{fill:#333333;}#mermaid-svg-vILFHKGtxIXCvNKJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vILFHKGtxIXCvNKJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vILFHKGtxIXCvNKJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vILFHKGtxIXCvNKJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vILFHKGtxIXCvNKJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vILFHKGtxIXCvNKJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vILFHKGtxIXCvNKJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vILFHKGtxIXCvNKJ .cluster text{fill:#333;}#mermaid-svg-vILFHKGtxIXCvNKJ .cluster span{color:#333;}#mermaid-svg-vILFHKGtxIXCvNKJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vILFHKGtxIXCvNKJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vILFHKGtxIXCvNKJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-vILFHKGtxIXCvNKJ .icon-shape,#mermaid-svg-vILFHKGtxIXCvNKJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vILFHKGtxIXCvNKJ .icon-shape p,#mermaid-svg-vILFHKGtxIXCvNKJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vILFHKGtxIXCvNKJ .icon-shape .label rect,#mermaid-svg-vILFHKGtxIXCvNKJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vILFHKGtxIXCvNKJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vILFHKGtxIXCvNKJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vILFHKGtxIXCvNKJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 失败
成功
成功
失败
开始
客户端提交修改请求
更新数据库
数据库更新是否成功?
事务回滚
返回500错误
删除Redis缓存
删除是否成功?
返回成功
记录异常日志
触发异步补偿/MQ重试
缓存更新策略总结
下表总结给出的最佳实践方案,区分了高低一致性需求的应对策略以及读写操作的具体步骤。
| 需求类型 | 策略选择 | 读操作规范 | 写操作规范 |
|---|---|---|---|
| 低一致性 | 内存淘汰机制 | 命中返回,未命中回源 | 无需显式更新,依赖淘汰 |
| 高一致性 | 主动更新 + TTL兜底 | 1. 命中直接返回 2. 未命中查库并写缓存 3. 设定超时时间 | 1. 先写数据库 2. 再删除缓存 3. 确保操作原子性 |
代码实现
python
from fastapi import FastAPI, HTTPException
import redis.asyncio as aioredis
import json
import logging
app = FastAPI()
redis_client = aioredis.Redis(host="127.0.0.1", port=6379, db=0, decode_responses=True)
logger = logging.getLogger(__name__)
# 模拟数据库操作层
class ShopRepository:
async def update(self, shop_id: int, data: dict) -> bool:
# 实际项目替换为: session.execute(update(Shop).where(Shop.id==shop_id).values(data))
return True
async def get(self, shop_id: int) -> dict | None:
mock_db = {1: {"id": 1, "name": "星巴克旗舰店", "type": 1}}
return mock_db.get(shop_id)
repo = ShopRepository()
@app.get("/shops/{shop_id}")
async def get_shop_with_ttl(shop_id: int):
"""
读操作最佳实践:缓存未命中时回源写库,并强制设置TTL作为一致性兜底。
"""
cache_key = f"shop:{shop_id}"
cached_data = await redis_client.get(cache_key)
if cached_data:
return json.loads(cached_data)
shop = await repo.get(shop_id)
if not shop:
raise HTTPException(status_code=404, detail="Shop not found")
# 写入缓存并设置TTL。此处3600秒为示例,生产需根据业务更新频率评估。
# TTL是主动更新失败时的最后一道防线,确保脏数据不会永久驻留。
await redis_client.setex(cache_key, 3600, json.dumps(shop))
return shop
@app.put("/shops/{shop_id}")
async def update_shop_with_active_delete(shop_id: int, shop_data: dict):
"""
写操作最佳实践:先修改数据库,再删除缓存。确保操作顺序与并发安全。
生产环境建议将redis.delete纳入异步补偿或Binlog监听链路,避免阻塞主事务。
"""
cache_key = f"shop:{shop_id}"
# 1. 先更新数据库(应包裹在ORM事务或分布式事务上下文中)
success = await repo.update(shop_id, shop_data)
if not success:
raise HTTPException(status_code=500, detail="Database update failed")
# 2. 再删除缓存(Cache Aside 核心原则:失效优于更新)
try:
await redis_client.delete(cache_key)
except Exception as e:
# 删除失败不应回滚数据库事务,但需记录日志并触发异步重试补偿
logger.error(f"Cache delete failed for {cache_key}, triggering async compensation: {e}")
# 实际项目可投递MQ或写入延迟任务表
return {"status": "success", "message": "DB updated & cache invalidated"}