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

【Redis】三种缓存问题(穿透、击穿、双删)的 Golang 实践

Redis 缓存三大问题(延迟双删、缓存击穿、缓存穿透)Golang 实践指南

本文通过实际业务场景和 Golang 代码示例,详细讲解 Redis 中延迟双删、缓存击穿、缓存穿透的解决方案,帮助 Golang 开发工程师快速理解并落地到业务中。

一、缓存穿透(Cache Penetration)

问题描述

查询不存在的数据(如 ID=-1 的用户、ID=9999 的商品),导致请求绕过缓存直接冲击数据库,可能造成数据库压力过大甚至宕机。

核心解决方案

  1. 缓存空值:对不存在的数据,在缓存中存储一个短期有效的空值(如 “nil”),避免后续无效请求直接访问数据库。

  2. 布隆过滤器:在缓存前拦截无效请求(适用于数据量极大的场景),本文以更易落地的「缓存空值」为例。

场景示例:用户信息查询

假设业务需要根据用户 ID 查询信息,恶意请求可能使用不存在的 ID 频繁查询,需通过缓存空值防护。

package mainimport ("context""fmt""time""github.com/go-redis/redis/v8"
)// 模拟数据库:仅存储 ID=1001 的用户type MockDB struct{}func (db *MockDB) GetUserByID(id int64) (map[string]interface{}, bool) {if id == 1001 {return map[string]interface{}{"id":   1001,"name": "张三","age":  28,}, true}return nil, false
}// 用户服务:集成缓存穿透防护type UserService struct {redisClient *redis.Clientdb          *MockDBctx         context.Context
}func NewUserService(redisClient *redis.Client) *UserService {return &UserService{redisClient: redisClient,db:          &MockDB{},ctx:         context.Background(),}}// GetUserWithPenetrationProtection:带缓存穿透防护的查询方法func (s *UserService) GetUserWithPenetrationProtection(id int64) (map[string]interface{}, error) {cacheKey := fmt.Sprintf("user:%d", id)// 1. 先查缓存(命中则直接返回,包括空值)cacheVal, err := s.redisClient.Get(s.ctx, cacheKey).Result()if err == nil && cacheVal != "" {if cacheVal == "nil" { // 命中空值缓存return nil, fmt.Errorf("用户不存在")}// 实际项目中需反序列化 JSON(此处简化)return map\[string]interface{}{"id": id, "data": cacheVal}, nil}// 2. 缓存未命中,查询数据库user, exists := s.db.GetUserByID(id)if !exists {// 3. 数据库无数据,缓存空值(短期有效,避免长期占用内存)if err := s.redisClient.Set(s.ctx, cacheKey, "nil", 5*time.Minute).Err(); err != nil {return nil, fmt.Errorf("缓存空值失败: %v", err)}return nil, fmt.Errorf("用户不存在")}// 4. 数据库有数据,写入缓存(设置合理过期时间)if err := s.redisClient.Set(s.ctx, cacheKey, "user_data", 30*time.Minute).Err(); err != nil {return nil, fmt.Errorf("写入缓存失败: %v", err)}return user, nil}func main() {// 初始化 Redis 客户端redisClient := redis.NewClient(\&redis.Options{Addr:     "localhost:6379",Password: "", // 无密码(生产环境需配置)DB:       0,  // 默认数据库})defer redisClient.Close()userService := NewUserService(redisClient)// 测试1:查询存在的用户(ID=1001)user, err := userService.GetUserWithPenetrationProtection(1001)if err != nil {fmt.Println("测试1失败:", err)} else {fmt.Println("测试1成功:", user)}// 测试2:首次查询不存在的用户(ID=9999)→ 查DB+缓存空值user, err = userService.GetUserWithPenetrationProtection(9999)if err != nil {fmt.Println("测试2失败:", err)} else {fmt.Println("测试2成功:", user)}// 测试3:再次查询不存在的用户(ID=9999)→ 命中空值缓存user, err = userService.GetUserWithPenetrationProtection(9999)if err != nil {fmt.Println("测试3失败:", err)} else {fmt.Println("测试3成功:", user)}}

代码关键说明

  • 空值缓存有效期:设置为 5 分钟(可根据业务调整),避免真实数据插入后缓存长期不一致。

  • 序列化处理:实际项目中需将用户数据序列化为 JSON 存储(示例中简化为字符串)。

  • 错误处理:缓存操作失败不阻断业务(如缓存空值失败仍返回「用户不存在」)。

二、缓存击穿(Cache Breakdown)

问题描述

热点 key(如热门商品、活动页面)过期瞬间,大量并发请求同时访问,导致所有请求绕过缓存直接冲击数据库,造成数据库压力骤增。

核心解决方案

  1. 互斥锁:保证只有一个请求能查询数据库并更新缓存,其他请求等待后重试(本文示例方案)。

  2. 热点 key 永不过期:业务层不设置缓存过期时间,通过后台定时任务更新缓存(适用于更新频率低的场景)。

场景示例:热门商品查询

假设某商品(ID=10086)是爆款,缓存过期时会有大量并发请求,需通过互斥锁控制并发。

package mainimport ("context""fmt""sync""time""github.com/go-redis/redis/v8")// 模拟商品数据库:仅存储热门商品 ID=10086type ProductDB struct{}func (db *ProductDB) GetProductByID(id int64) (map[string]interface{}, bool) {// 模拟数据库查询耗时(实际场景可能更久)time.Sleep(100 * time.Millisecond)if id == 10086 {return map[string]interface{}{"id":    10086,"name":  "热门手机","price": 3999,"stock": 10000,}, true}return nil, false}// 商品服务:集成缓存击穿防护(互斥锁)
type ProductService struct {redisClient *redis.Clientdb          *ProductDBctx         context.Context}func NewProductService(redisClient *redis.Client) *ProductService {return &ProductService{redisClient: redisClient,db:          &ProductDB{},ctx:         context.Background(),}
}// GetProductWithBreakdownProtection:带缓存击穿防护的查询方法
func (s *ProductService) GetProductWithBreakdownProtection(id int64) (map[string]interface{}, error) {cacheKey := fmt.Sprintf("product:%d", id)// 1. 先查缓存(命中则直接返回)cacheVal, err := s.redisClient.Get(s.ctx, cacheKey).Result()if err == nil && cacheVal != "" {return map[string]interface{}{"id": id, "data": cacheVal}, nil}// 2. 缓存未命中,尝试获取分布式锁(Redis SET NX)lockKey := fmt.Sprintf("lock:product:%d", id)lockVal := "1"// 锁有效期 3 秒(防止服务异常导致死锁)ok, err := s.redisClient.SetNX(s.ctx, lockKey, lockVal, 3*time.Second).Result()if err != nil {return nil, fmt.Errorf("获取锁失败: %v", err)}// 3. 未获取到锁:等待 100ms 后重试(简单退避策略)if !ok {time.Sleep(100 * time.Millisecond)return s.GetProductWithBreakdownProtection(id)}// 确保锁释放(无论后续逻辑是否成功)defer s.redisClient.Del(s.ctx, lockKey)// 4. 获取到锁:查询数据库product, exists := s.db.GetProductByID(id)if !exists {// 缓存空值(同时防护缓存穿透)s.redisClient.Set(s.ctx, cacheKey, "nil", 5*time.Minute)return nil, fmt.Errorf("商品不存在")}// 5. 写入缓存(热点 key 可延长过期时间,如 1 小时)if err := s.redisClient.Set(s.ctx, cacheKey, "product_data", 1*time.Hour).Err(); err != nil {return nil, fmt.Errorf("写入缓存失败: %v", err)}    return product, nil}func main() {// 初始化 Redis 客户端redisClient := redis.NewClient(&redis.Options{Addr:     "localhost:6379",Password: "",DB:       0,})defer redisClient.Close()productService := NewProductService(redisClient)// 模拟 100 个并发请求查询热点商品var wg sync.WaitGroupwg.Add(100)startTime := time.Now()for i := 0; i < 100; i++ {go func() {defer wg.Done()_, err := productService.GetProductWithBreakdownProtection(10086)if err != nil {fmt.Println("并发请求失败:", err)}}()}wg.Wait()fmt.Printf("100 个并发请求总耗时: %v\n", time.Since(startTime))// 预期结果:总耗时 ≈ 100ms(仅第一个请求查 DB,其余等待后命中缓存)}

代码关键说明

  • 分布式锁实现:使用 Redis 的 SetNX(Set if Not Exists)命令,确保同一时间只有一个请求获取锁。

  • 锁有效期:设置为 3 秒(需大于数据库查询耗时),防止服务崩溃导致锁无法释放。

  • 退避策略:未获取锁的请求等待 100ms 后重试,避免频繁自旋消耗 CPU。

  • 双重防护:同时缓存空值,兼顾缓存穿透问题。

三、延迟双删(Delayed Double Delete)

问题描述

更新数据库后直接删除缓存,可能存在并发一致性问题

  1. 线程 A 先更新数据库,再删除缓存;

  2. 线程 B 在 A 删除缓存前读取缓存(命中旧值),但在 A 更新数据库后写入缓存;

  3. 最终缓存存储旧值,导致「缓存脏数据」。

核心解决方案

延迟双删:更新数据库前后各删除一次缓存,第二次删除延迟一段时间(如 500ms),覆盖并发窗口期。

  • 第一次删除:清除旧缓存,避免更新期间读取旧值;

  • 第二次删除:解决「线程 B 读取旧 DB 数据后写入缓存」的问题。

场景示例:商品库存更新

假设需要更新商品库存(如下单减库存),需通过延迟双删保证缓存与数据库一致性。

package mainimport ("context""fmt""time""github.com/go-redis/redis/v8"
)// 模拟库存数据库:内存 map 存储(实际为 MySQL 等)type InventoryDB struct {stock map[int64]int // key: 商品ID,value: 库存}func NewInventoryDB() *InventoryDB {return &InventoryDB{stock: map[int64]int{10086: 10000, // 初始库存:10000},}}// UpdateStock:更新库存(模拟数据库事务)func (db *InventoryDB) UpdateStock(id int64, quantity int) (bool, error) {current, ok := db.stock[id]if !ok {return false, fmt.Errorf("商品不存在")}// 防止库存为负(如下单减库存时)if current+quantity < 0 {return false, fmt.Errorf("库存不足")}db.stock[id] += quantityreturn true, nil}// GetStock:查询库存func (db *InventoryDB) GetStock(id int64) (int, bool) {stock, ok := db.stock[id]return stock, ok
}// 库存服务:集成延迟双删type InventoryService struct {redisClient *redis.Clientdb          *InventoryDBctx         context.Context}func NewInventoryService(redisClient *redis.Client) *InventoryService {return &InventoryService{redisClient: redisClient,db:          NewInventoryDB(),ctx:         context.Background(),}}// UpdateStockWithDoubleDelete:带延迟双删的库存更新方法func (s *InventoryService) UpdateStockWithDoubleDelete(id int64, quantity int) error {cacheKey := fmt.Sprintf("stock:%d", id)// 1. 第一次删除缓存:清除旧值_ = s.redisClient.Del(s.ctx, cacheKey).Err() // 缓存删除失败不阻断业务// 2. 更新数据库(核心业务逻辑,需保证事务性)success, err := s.db.UpdateStock(id, quantity)if !success || err != nil {return fmt.Errorf("更新库存失败: %v", err)}// 3. 延迟一段时间后,第二次删除缓存(覆盖并发窗口期)// 延迟时间:根据业务响应时间调整(通常 100ms-1s)go func() {time.Sleep(500 \* time.Millisecond)_ = s.redisClient.Del(s.ctx, cacheKey).Err()}()return nil
}// GetStock:查询库存(先缓存后 DB)func (s *InventoryService) GetStock(id int64) (int, error) {cacheKey := fmt.Sprintf("stock:%d", id)// 1. 查缓存cacheVal, err := s.redisClient.Get(s.ctx, cacheKey).Int()if err == nil {return cacheVal, nil}// 2. 缓存未命中,查 DBstock, exists := s.db.GetStock(id)if !exists {return 0, fmt.Errorf("商品不存在")}// 3. 写入缓存(设置过期时间)_ = s.redisClient.Set(s.ctx, cacheKey, stock, 30*time.Minute).Err()return stock, nil}func main() {// 初始化 Redis 客户端redisClient := redis.NewClient(&redis.Options{Addr:     "localhost:6379",Password: "",DB:       0,})defer redisClient.Close()inventoryService := NewInventoryService(redisClient)// 测试1:初始查询库存stock, _ := inventoryService.GetStock(10086)fmt.Println("初始库存:", stock) // 预期:10000// 测试2:更新库存(减少 100)err := inventoryService.UpdateStockWithDoubleDelete(10086, -100)if err != nil {fmt.Println("更新失败:", err)} else {fmt.Println("库存更新成功(减少 100)")}// 测试3:查询更新后的库存stock, _ = inventoryService.GetStock(10086)fmt.Println("更新后库存:", stock) // 预期:9900}

代码关键说明

  • 延迟时间选择:500ms 需根据业务调整(如数据库写入耗时 200ms,并发请求响应耗时 300ms,则延迟 500ms 可覆盖窗口期)。

  • 异步删除:第二次删除通过 goroutine 异步执行,不阻塞主业务流程。

  • 容错性:缓存删除失败不阻断业务(如网络波动导致删除失败,后续查询会自动从 DB 更新缓存)。

四、总结与业务落地建议

问题类型核心痛点解决方案适用场景
缓存穿透无效请求冲击 DB缓存空值、布隆过滤器高频无效查询(如恶意请求)
缓存击穿热点 key 过期导致并发冲击 DB互斥锁、热点 key 永不过期热门商品、活动页面
延迟双删数据更新后缓存不一致两次删除 + 延迟库存更新、用户信息修改等

落地注意事项

  1. 依赖选择:示例使用 github.com/go-redis/redis/v8(Redis 客户端),生产环境需注意版本兼容性。

  2. 错误处理:缓存操作(如删除、写入)失败不阻断核心业务,避免缓存问题影响主流程。

  3. 过期时间:根据业务数据更新频率设置缓存过期时间(如高频更新数据设置 5-10 分钟,低频更新设置 1-24 小时)。

  4. 分布式锁优化:生产环境可使用 Redisson 等成熟框架,支持锁重入、自动续期等高级特性。

  5. 监控告警:对热点 key、缓存命中率、DB 压力等指标进行监控,及时发现异常。

http://www.dtcms.com/a/511092.html

相关文章:

  • 第1部分-并发编程基础与线程模型
  • 【含文档+PPT+源码】基于SSM的智能驾校预约管理系统
  • python股票交易数据管理系统 金融数据 分析可视化 Django框架 爬虫技术 大数据技术 Hadoop spark(源码)✅
  • 有哪些网站可以自己做加视频做一个购物网站
  • 佛山建站公司模板开发一个物流app需要多少钱
  • Java——使用Aspose实现docx模板填充与导出word和pdf
  • Cef笔记:VS2019编译cef_109.0.5414
  • 云上极速转码:阿里云ECS+T4 GPU打造高性能FFmpeg视频处理引擎(部署指南)
  • Mysql 数据库迁移
  • 自建webrtc低延时分布式街机游戏直播方案
  • PHP四川文旅服务网站-计算机毕业设计源码76050
  • 从“开源开放”走向“高效智能”:阿里云 EMR 年度重磅发布
  • AI服务器工作之ubuntu系统下的驱动安装
  • 将跨平台框架或游戏引擎开发的 Windows 应用上架 Microsoft Store
  • 网站建立需要什么永久网站建设教程
  • 安装nginx
  • Leetcode+Java+图论+最小生成树拓扑排序
  • Unity画线功能LineRenderer详解附有案例
  • 从零部署自维护版 Uptime Kuma:Node.js + PM2 + Nginx 全链路实战指南
  • 哪个网站可以做纸箱沈阳市住房和城乡建设部网站
  • 腾讯送命题:手写多头注意力机制。。。
  • 信息传递视角下开源AI智能名片链动2+1模式S2B2C商城小程序对零售企业的赋能研究
  • AWS IoT Core 自动化巡检工具开发与实战报告
  • 【开题答辩过程】以《重庆市社区养老服务小程序设计与实现》为例,不会开题答辩的可以进来看看
  • 思淘网站建设网站域名证书
  • Substance Designer的通道合并(Channel Packing)自动化工作流
  • 【数据分析】基于R语言的宏基因组微生物群落下游数据的统计分析与可视化教程
  • TDE透明加密技术原理深度解析与文件在线安全传输实践指南
  • AI协作天花板!CherryStudio让多模型协同像搭积木
  • flash网站开发用什么语言wordpress 静态文件大