缓存删除与刷新的数据竞争问题:如何避免脏缓存?
缓存并发更新脏数据问题的解决方案及缓存失效实践
问题本质
你碰到的是Cache-Aside(懒加载)模式下的典型缓存与数据库一致性并发冲突:GET请求缓存未命中后去查询DB,同时DELETE请求完成DB删除和缓存清理,但GET请求后续将已删除的旧数据写回缓存,导致后续请求拿到脏数据。
不止互斥锁:多种优化方案
1. 缓存版本号校验(推荐)
给Car结构体增加version字段,每次DB操作(新增/删除/更新)时同步更新版本号:
- 优化后的
getCar流程:getCar(id): car = getCarFromCache(id) if car == null { // 从DB获取数据及对应版本号 car, dbVersion = getCarAndVersionFromDB(id) if car != null { updateCacheWithCarAndVersion(id, car, dbVersion) } else { // 缓存null值并设置短过期时间,避免DB穿透 updateCacheWithNull(id, 60) return null } } else { // 校验缓存版本是否与DB一致 currentDbVersion = getCarVersionFromDB(id) if car.version < currentDbVersion || currentDbVersion == -1 { // -1标记已删除 deleteCarFromCache(id) return getCar(id) // 重新查询 } } return car deleteCar流程:删除DB数据时,将该ID的版本号标记为-1(或一个极大值),再删除缓存。后续GET请求校验版本时会识别数据已失效,不会写入脏缓存。
这种方式无需锁,仅通过版本号实现一致性,性能影响极小。
2. 延迟双删
调整deleteCar的执行逻辑,在首次删除缓存后,异步延迟一段时间再执行二次删除:
deleteCar(id): deleteCarFromDB(id) deleteCarFromCache(id) // 异步线程延迟1-5秒(根据业务DB查询耗时调整) asyncDelay(() => { deleteCarFromCache(id) })
原理:即便GET请求在首次缓存删除后写入旧数据,延迟删除会再次清理脏数据,后续请求会重新查询DB(此时已无数据)。
优点是实现简单,无锁开销;缺点是存在短暂的脏数据窗口,需合理设置延迟时间。
3. 细粒度互斥锁(单ID锁)
不用全局锁,仅针对单个Car ID加锁:
getCar流程优化:getCar(id): car = getCarFromCache(id) if car == null { lock(id) // 仅对当前ID加锁 try { // 二次检查缓存,避免其他线程已加载数据 car = getCarFromCache(id) if car == null { car = getCarFromDB(id) if car != null { // DB存在才更新缓存 updateCacheWithCar(id, car) } } } finally { unlock(id) } } return cardeleteCar流程同样对该ID加锁,确保DB删除与缓存删除的原子性。
这种方式仅同一ID的请求会互斥,不同ID请求互不影响,性能远优于全局锁。
4. 主动更新缓存(替代懒加载)
若业务删除操作频繁,可放弃懒加载,改为主动更新策略:
- 新增/更新Car时,先写DB再同步更新缓存;
- 删除Car时,先删DB再删缓存;
- GET请求直接读缓存,缓存不存在则返回数据不存在。
这种方式彻底避免懒加载的并发问题,但要求所有写操作都能正确触发缓存更新,适合写操作可控的场景。
缓存失效核心实践要点
- 缓存策略选型:
Cache-Aside(懒加载)适合读多写少场景;Write-Through(写DB同时写缓存)适合一致性要求高的场景;Write-Behind(先写缓存异步写DB)适合写频繁场景; - 一致性优先级:强一致场景优先选版本号或细粒度锁;允许短暂不一致场景可选延迟双删;
- 避免缓存穿透:已删除的ID可缓存
null值并设置短过期时间,防止大量请求直接打向DB; - 热点数据防护:热点ID可设置永不过期(结合主动更新)或加互斥锁,避免缓存失效后引发DB雪崩。
内容的提问来源于stack exchange,提问作者JackG
相关产品推荐
相关产品推荐

