Go 项目中 Redis 缓存的实用设计与实现(Cache-Aside 模式)

Go 项目中 Redis 缓存的实用设计与实现(Cache-Aside 模式)

在高并发消息投递或用户查询场景中,MySQL 往往会成为瓶颈 ------ 短时间内大量重复读请求会把数据库压垮。本文结合实际项目经验,用 Cache-Aside(旁路缓存) 模式,一步步教你在 Go 项目中落地 Redis 缓存,既解决性能问题,又保证系统容错能力。

核心模式:Cache-Aside 是什么?

Cache-Aside 是最常用的缓存策略,逻辑很简单:

  • 读路径:先查缓存,命中直接返回;未命中查数据库,结果写回缓存。
  • 写路径:先写数据库,成功后再更新 / 删除缓存(避免数据不一致)。

这种模式的好处是:Redis 挂了不影响主流程(自动降级到数据库),实现也不复杂。

Key 生成器:避免硬编码,统一规范

用函数生成 Key,既规范又不容易冲突。比如:

  • 收件人列表:deliver:recips:<消息ID>
  • 用户待投递列表:deliver:pending:user:<用户名>
go 复制代码
// 生成收件人列表缓存 Key
func KeyRecips(serverMsgID string) string {
    return fmt.Sprintf("deliver:recips:%s", serverMsgID)
}

// 生成用户待投递列表缓存 Key
func KeyPendingUser(username string) string {
    return fmt.Sprintf("deliver:pending:user:%s", username)
}

读路径

以 "查询消息收件人列表" 为例

步骤 1:先查缓存

如果 Redis 可用且 Key 有值,直接反序列化返回。

go 复制代码
func GetRecips(ctx context.Context, serverMsgID string) ([]string, error) {
    key := KeyRecips(serverMsgID)

    // 1. 尝试从缓存获取
    if rdb != nil {
        val, err := rdb.Get(ctx, key).Result()
        if err == nil && val != "" {
            // 缓存命中:反序列化 JSON
            var recips []string
            if json.Unmarshal([]byte(val), &recips) == nil {
                return recips, nil
            }
        }
    }
步骤 2:缓存未命中,查数据库

这里用 queryRecipsFromDB 代替具体 SQL 查询,重点看逻辑。

go 复制代码
    // 2. 缓存未命中:查询数据库
    recips, err := queryRecipsFromDB(serverMsgID) // 你的 DB 查询逻辑
    if err != nil {
        return nil, err
    }

步骤 3:结果写回缓存(带 TTL)

把数据库查到的结果序列化,写回 Redis,设置短 TTL(比如 5 秒,避免缓存太久不一致)。

go 复制代码
    // 3. 写回缓存(即使失败也不影响主流程)
    if rdb != nil {
        if b, err := json.Marshal(recips); err == nil {
            // 设置 TTL 为 5 秒(短期热点保护)
            _ = rdb.Set(ctx, key, b, 5*time.Second).Err()
        }
    }

    return recips, nil
}
序列化用 JSON 就行

缓存值用 JSON 序列化,优点是:

  • 跨语言友好(其他服务也能读);
  • 调试方便(Redis CLI 直接看内容)。
  • 性能足够(除非极致 QPS,否则 JSON 开销可忽略)。
TTL 怎么设?
  • 短期热点(秒级) :比如收件人列表、待投递列表,设 2-5 秒 ------ 既抗住瞬时并发,又避免数据不一致。
  • 长期低频(分钟级) :比如用户信息,设 30 分钟 ------ 降低用户表读压。
  • 建议:TTL 做成可配置(比如环境变量),方便根据命中率调整。

写路径:事务成功后再操作缓存

写操作的核心是:先确保数据库事务成功,再动缓存

以 "发送消息后更新收件人缓存、失效待投递列表" 为例:

步骤 1:数据库事务

先在事务里写入消息和收件人数据,确保原子性。

go 复制代码
func SendMessage(ctx context.Context, msg *Message, recips []string) error {
    // 1. 开启数据库事务
    tx := db.Begin()
    defer func() {
        if r := recover(); r != nil {
            tx.Rollback()
        }
    }()

    // 2. 事务内写入:插入消息、插入收件人列表
    if err := tx.Create(msg).Error; err != nil {
        tx.Rollback()
        return err
    }
    if err := insertRecips(tx, msg.ID, recips); err != nil { // 你的 DB 写入逻辑
        tx.Rollback()
        return err
    }

    // 3. 提交事务
    if err := tx.Commit().Error; err != nil {
        return err
    }
步骤 2:事务成功后,操作缓存
  • 更新:把刚写入的收件人列表直接塞进缓存(下次读直接命中)。
  • 失效:删除受影响的 "用户待投递列表" 等缓存(避免旧数据)。
go 复制代码
    // 4. 事务成功后:更新/失效缓存(失败不影响主流程)
    if rdb != nil {
        // a. 更新收件人缓存(TTL 5 秒)
        recipsKey := KeyRecips(msg.ID)
        if b, err := json.Marshal(recips); err == nil {
            _ = rdb.Set(ctx, recipsKey, b, 5*time.Second).Err()
        }

        // b. 失效相关缓存:比如每个收件人的待投递列表
        for _, user := range recips {
            pendingKey := KeyPendingUser(user)
            _ = rdb.Del(ctx, pendingKey).Err()
        }
        // 再失效全局待投递列表等...
    }

    return nil
}
相关推荐
我是一颗柠檬7 小时前
【Java后端技术亮点】热Key探测与本地缓存二级防护:Redis热点问题的终极解决方案
java·redis·后端·缓存·中间件
cfm_29148 小时前
Redis高并发缓存架构设计与性能优化实战
redis·缓存·性能优化
画江湖Test8 小时前
Redis 块的原理
数据库·redis·缓存·性能优化
海市公约8 小时前
Redis主从复制全量同步七步时序与命令传播机制详解
数据库·redis·缓存·主从复制·高可用架构·全量同步
小马爱打代码9 小时前
Redis 和 MySQL 双写一致性:延迟双删、读写锁、MQ、Canal 怎么选?
数据库·redis·mysql
我,也来自江湖10 小时前
Redis的持久化有哪些方式
数据库·redis·缓存
小小工匠10 小时前
Redis - 实现分页 + 多条件模糊查询:一套完整可落地的组合方案
数据库·redis·缓存·分页·模糊查询
阿演11 小时前
DataDjinn v0.1.6 更新:增加在线更新功能,Redis 数据源支持,表格预览和连接体验继续增强
数据库·redis·缓存·数据库连接工具
喵了几个咪11 小时前
AI重构软件开发范式:框架与脚手架为何仍是生产级开发的刚需?
vue.js·人工智能·react.js·重构·golang·ai编程
Trouvaille ~12 小时前
【Redis篇】Redis 渐进式遍历与数据库管理
数据库·redis·缓存·中间件·数据库管理·后端开发·scan