Redis学习
Redis 最常见的模式 Cache Aside:
目前写go应用的用法如下:
在 service/product.go 实现了 Cache Aside 模式:
GET /api/products/:id
├─ Redis GET product:xxx → 命中 → 直接返回
└─ 未命中 → MySQL 查询 → Redis SET(30分钟过期) → 返回
PUT /api/products/:id → 更新 DB → Redis DEL product:xxx
DELETE /api/products/:id → 删除 DB → Redis DEL product:xxx
核心代码就几行
// 查缓存
val, err := common.RedisClient.Get(common.Ctx, cacheKey).Result()
if err == nil {
// 命中,反序列化返回
json.Unmarshal([]byte(val), &product)
}
(此处省略查DB的过程,就是查出product对象)
// 未命中,查 DB 后写入缓存
data, _ := json.Marshal(product)
common.RedisClient.Set(common.Ctx, cacheKey, data, 30*time.Minute)
知识点
- String 结构:用 JSON 序列化存复杂对象
- 过期时间:30 分钟自动淘汰,防止脏数据
- 缓存失效:写操作后主动删缓存,下次读时重新加载

(图片来源于互联网 侵删)
2.Redis命名约定 object-type🆔field
Redis 官方文档推荐的惯例,来源在这:
https://redis.io/docs/manual/keyspace/ (https://redis.io/docs/manual/keyspace/)
里面有句话:
Try to stick with a schema. For instance, "object-type🆔field" is a good idea.
官方推荐的格式就是 object-type🆔field,也就是 实体类型:ID:字段。
例如:
user:123:visits 用户 123 的访问次数
product:456:views 商品 456 的浏览量
product:456 商品 456 的详情(我们项目里)
3.缓存策略深入
先讲问题,再改代码。
1: 缓存穿透问题 + 空值缓存方案
2: 缓存击穿问题 + 本地锁保护
3: 缓存雪崩问题 + 随机 TTL
Q:我不知道为什么会导致这三个问题 你做了什么改动 为什么这么改 我看不懂呀
A:对不起,我跳太快了。我们把代码展开一行行对比讲。
改动前 GetProduct(Service层的一个函数)
func GetProduct(id string) (*model.Product, error) {
cacheKey := "product:" + id
// ① 查 Redis
val, err := common.RedisClient.Get(common.Ctx, cacheKey).Result()
if err == nil {
var product model.Product
if json.Unmarshal([]byte(val), &product) == nil {
return &product, nil // 有缓存 → 直接返回
}
}
// ② 没缓存 → 查 MySQL
product, err := dao.GetProduct(id)
if err != nil {
return nil, err // MySQL 也没有 → 返回错误
}
// ③ 查到后写入 Redis
data, _ := json.Marshal(product)
common.RedisClient.Set(common.Ctx, cacheKey, data, 30time.Minute)
return product, nil
}
这个代码有 3 个问题,逐个说:
问题 1:缓存穿透(查的商品根本不存在)
场景: 请求 GET /api/products/nonexist-1,Redis 没有 → 查 MySQL → 没有 → 返回错误
下次再来查 nonexist-1,Redis 仍然没有 → 又查 MySQL。恶意攻击的话,每秒请求 1000 个不存在的商品 ID,MySQL 就崩了。
我的改动:
product, err := dao.GetProduct(id)
if err != nil {
// ★ 不存在也缓存,存个 "null",60 秒过期
common.RedisClient.Set(common.Ctx, cacheKey, "null", 1time.Minute)
return nil, errors.New("商品不存在")
}
效果: 第一次查完发现不存在 → Redis 记下"这个商品不存在,别查 MySQL 了" → 后续请求看到 "null" 直接返回,不打 MySQL。
问题 2:缓存击穿(热点商品缓存刚好过期)
场景: 一个爆款商品,同时 1000 个人访问。Redis 刚好过期 → 1000 个请求同时打到 MySQL。
我的改动——加了双检锁:
lock := getProductLock(id) // ① 每个商品一个锁
lock.Lock()
defer lock.Unlock()
// ② 拿到锁后再查一次 Redis(别人可能已经写进去了)
val, err = common.RedisClient.Get(common.Ctx, cacheKey).Result()
if err == nil {
// 有缓存了 → 直接返回,不用查 MySQL
...
}
效果: 1000 个请求同时来,只有 1 个 能拿到锁去查 MySQL,其他 999 个等锁释放后,从 Redis 拿数据。
问题 3:缓存雪崩(大量 key 同时过期)
场景: 你存了 1 万个商品,全是 30time.Minute 到期。30 分钟后全过期 → 1 万个请求同时打 MySQL。
我的改动:
// 原来是固定 30 分钟
common.RedisClient.Set(common.Ctx, cacheKey, data, 30time.Minute)
// 改成 20~40 分钟随机
ttl := 20time.Minute + time.Duration(rand.Intn(20))time.Minute
效果: A 商品 22 分钟过期,B 商品 35 分钟过期,C 商品 27 分钟过期……不会扎堆。
===================================================
一行一行看最终代码
func GetProduct(id string) (*model.Product, error) {
cacheKey := "product:" + id
// ——— 第 1 步:查 Redis ———
val, err := common.RedisClient.Get(common.Ctx, cacheKey).Result()
if err == nil {
// ★ 缓存的是 "null" → 直接返回"商品不存在"
if val == "null" {
return nil, errors.New("商品不存在")
}
var product model.Product
if json.Unmarshal([]byte(val), &product) == nil {
return &product, nil
}
}
// ——— 第 2 步:拿本地锁(防击穿) ———
lock := getProductLock(id)
lock.Lock()
defer lock.Unlock()
// ★ 双检:拿到锁后再查一次 Redis,避免重复查 DB
val, err = common.RedisClient.Get(common.Ctx, cacheKey).Result()
if err == nil {
if val == "null" {
return nil, errors.New("商品不存在")
}
var product model.Product
if json.Unmarshal([]byte(val), &product) == nil {
return &product, nil
}
}
// ——— 第 3 步:查 MySQL ———
product, err := dao.GetProduct(id)
if err != nil {
// ★ 防穿透:不存在也缓存空值
common.RedisClient.Set(common.Ctx, cacheKey, "null", 1*time.Minute)
return nil, errors.New("商品不存在")
}
// ——— 第 4 步:写入 Redis(随机 TTL 防雪崩) ———
ttl := 20time.Minute + time.Duration(rand.Intn(20))time.Minute
data, _ := json.Marshal(product)
common.RedisClient.Set(common.Ctx, cacheKey, data, ttl)
return product, nil
}
浙公网安备 33010602011771号