Redis 操作与缓存策略
核心概念
Redis 是高性能内存键值存储,在 Web 架构中承担缓存、分布式锁、限流、消息队列等多重角色。Go 生态中主流的 Redis 客户端是 github.com/redis/go-redis/v9。
go-redis 基本用法
go
import "github.com/redis/go-redis/v9"
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
PoolSize: 20,
})
// 所有操作都接受 context.Context
ctx := context.Background()
rdb.Set(ctx, "key", "value", 10*time.Minute)
val, err := rdb.Get(ctx, "key").Result()
常用数据结构
| Redis 类型 | Go 方法 | 典型场景 |
|---|---|---|
| String | Set/Get/Incr |
缓存、计数器、分布式锁 |
| Hash | HSet/HGet/HGetAll |
对象缓存 |
| List | LPush/RPop |
消息队列 |
| Set | SAdd/SMembers |
标签、去重 |
| ZSet | ZAdd/ZRange |
排行榜 |
三大经典缓存模式
1. Cache-Aside(旁路缓存)
最常用的缓存模式:读时先查缓存,未命中则查 DB 并回写缓存。
markdown
读请求 → 查缓存 → 命中?→ 返回
↓ 未命中
查数据库 → 写缓存 → 返回
go
func GetUser(rdb *redis.Client, db *sql.DB, id int64) (*User, error) {
ctx := context.Background()
key := fmt.Sprintf("user:%d", id)
// 1. 查缓存
if data, err := rdb.Get(ctx, key).Bytes(); err == nil {
var user User
json.Unmarshal(data, &user)
return &user, nil // 缓存命中
}
// 2. 缓存未命中,查数据库
user, err := getUserFromDB(db, id)
if err != nil {
return nil, err
}
// 3. 回写缓存(设置过期时间防永久脏数据)
if data, _ := json.Marshal(user); data != nil {
rdb.Set(ctx, key, data, 5*time.Minute)
}
return user, nil
}
写操作策略:先更新 DB,再删除缓存(而非更新缓存)。
go
func UpdateUser(rdb *redis.Client, db *sql.DB, user *User) error {
// 1. 先更新数据库
if err := updateUserInDB(db, user); err != nil {
return err
}
// 2. 再删除缓存(下次读时会重新加载)
rdb.Del(ctx, fmt.Sprintf("user:%d", user.ID))
return nil
}
为什么是"删除"而不是"更新"缓存?
- 避免并发写导致缓存与 DB 不一致
- 避免频繁更新但很少读取的缓存浪费资源
- 删除是幂等操作,更安全
2. Write-Through(直写)
写请求同时写缓存和 DB,由缓存层代理 DB 操作。应用程序只面对缓存,缓存层负责同步到 DB。一致性最强但写入延迟较高。
3. Write-Behind(异步写回)
写请求只写缓存,异步批量刷新到 DB。写入性能最高,但可能丢数据。
三大缓存问题与对策
缓存穿透
现象:查询一个根本不存在的数据,缓存永远不命中,每次都打到 DB。恶意攻击常见手段。
对策一:缓存空值
go
user, err := getUserFromDB(db, id)
if err == sql.ErrNoRows {
// 缓存空值,短过期时间
rdb.Set(ctx, key, "", 30*time.Second)
return nil, err
}
对策二:布隆过滤器------在缓存前加一层布隆过滤器,不存在的 key 直接拦截。
缓存击穿
现象:一个热点 key 过期瞬间,大量并发请求同时打到 DB。
对策:singleflight 或互斥锁------只让一个请求查 DB,其他请求等待。
go
import "golang.org/x/sync/singleflight"
var sf singleflight.Group
func GetUserWithSF(rdb *redis.Client, db *sql.DB, id int64) (*User, error) {
key := fmt.Sprintf("user:%d", id)
// 先查缓存
if data, _ := rdb.Get(ctx, key).Bytes(); data != nil {
return decodeUser(data)
}
// singleflight: 相同 key 的并发请求只执行一次
val, err, _ := sf.Do(key, func() (interface{}, error) {
user, err := getUserFromDB(db, id)
if err == nil {
rdb.Set(ctx, key, encodeUser(user), 5*time.Minute)
}
return user, err
})
return val.(*User), err
}
缓存雪崩
现象:大量缓存 key 同时过期,DB 瞬间压力暴增。
对策:过期时间加随机抖动
go
ttl := 5*time.Minute + time.Duration(rand.Intn(60))*time.Second
rdb.Set(ctx, key, data, ttl)
完整练习代码
以下代码使用 Go 内置的 map + sync.RWMutex 模拟 Redis 行为,无需安装 Redis 服务器即可运行,完整演示 Cache-Aside、缓存穿透/击穿/雪崩对策。
go
// redis_cache_strategy.go
package main
import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"sync"
"time"
)
// ============================================
// 模拟 Redis 客户端(内存实现,替代真实 go-redis)
// ============================================
// MockRedis 内存模拟 Redis 客户端
type MockRedis struct {
mu sync.RWMutex
store map[string]mockEntry
}
type mockEntry struct {
value []byte
expiresAt time.Time
}
func NewMockRedis() *MockRedis {
rdb := &MockRedis{store: make(map[string]mockEntry)}
// 后台清理过期 key
go func() {
for range time.Tick(time.Second) {
rdb.cleanup()
}
}()
return rdb
}
func (r *MockRedis) Set(key string, val interface{}, ttl time.Duration) {
data, _ := json.Marshal(val)
r.mu.Lock()
r.store[key] = mockEntry{
value: data,
expiresAt: time.Now().Add(ttl),
}
r.mu.Unlock()
}
func (r *MockRedis) Get(key string) ([]byte, bool) {
r.mu.RLock()
entry, ok := r.store[key]
r.mu.RUnlock()
if !ok || time.Now().After(entry.expiresAt) {
return nil, false
}
return entry.value, true
}
func (r *MockRedis) Del(key string) {
r.mu.Lock()
delete(r.store, key)
r.mu.Unlock()
}
func (r *MockRedis) cleanup() {
r.mu.Lock()
for k, v := range r.store {
if time.Now().After(v.expiresAt) {
delete(r.store, k)
}
}
r.mu.Unlock()
}
// ============================================
// 模拟数据库
// ============================================
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
// MockDB 模拟数据库
type MockDB struct {
users map[int]*User
// 查询计数器(用于验证缓存效果)
QueryCount int
}
func NewMockDB() *MockDB {
return &MockDB{
users: map[int]*User{
1: {ID: 1, Name: "张三", Age: 28},
2: {ID: 2, Name: "李四", Age: 32},
3: {ID: 3, Name: "王五", Age: 25},
},
}
}
func (db *MockDB) FindUser(id int) (*User, error) {
db.QueryCount++
time.Sleep(10 * time.Millisecond) // 模拟 DB 查询延迟
user, ok := db.users[id]
if !ok {
return nil, errors.New("user not found")
}
// 返回副本避免外部修改
copy := *user
return ©, nil
}
// ============================================
// Cache-Aside 模式实现
// ============================================
// CacheAsideService 旁路缓存服务
type CacheAsideService struct {
rdb *MockRedis
db *MockDB
}
func NewCacheAsideService(rdb *MockRedis, db *MockDB) *CacheAsideService {
return &CacheAsideService{rdb: rdb, db: db}
}
// GetUser 基础 Cache-Aside
func (s *CacheAsideService) GetUser(id int) (*User, error) {
key := fmt.Sprintf("user:%d", id)
// 1. 查缓存
if data, ok := s.rdb.Get(key); ok {
var user User
if json.Unmarshal(data, &user) == nil {
return &user, nil
}
}
// 2. 查 DB
user, err := s.db.FindUser(id)
if err != nil {
// 缓存穿透对策:缓存空值
if err.Error() == "user not found" {
s.rdb.Set(key, nil, 30*time.Second)
}
return nil, err
}
// 3. 回写缓存(雪崩对策:TTL 加随机抖动)
ttl := 5*time.Minute + time.Duration(rand.Intn(60))*time.Second
s.rdb.Set(key, user, ttl)
return user, nil
}
// UpdateUser 更新用户(先更新 DB,再删除缓存)
func (s *CacheAsideService) UpdateUser(user *User) error {
key := fmt.Sprintf("user:%d", user.ID)
// 1. 先更新 DB
s.db.users[user.ID] = user
// 2. 再删除缓存
s.rdb.Del(key)
return nil
}
// ============================================
// 缓存击穿对策:singleflight
// ============================================
type singleflightCall struct {
wg sync.WaitGroup
val interface{}
err error
}
// SFGroup 极简 singleflight 实现
type SFGroup struct {
mu sync.Mutex
calls map[string]*singleflightCall
}
func NewSFGroup() *SFGroup {
return &SFGroup{calls: make(map[string]*singleflightCall)}
}
func (g *SFGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
g.mu.Lock()
if call, ok := g.calls[key]; ok {
g.mu.Unlock()
call.wg.Wait() // 等待第一个请求完成
return call.val, call.err
}
call := &singleflightCall{}
call.wg.Add(1)
g.calls[key] = call
g.mu.Unlock()
// 执行实际查询
call.val, call.err = fn()
call.wg.Done()
g.mu.Lock()
delete(g.calls, key)
g.mu.Unlock()
return call.val, call.err
}
// AntiBreakdownService 防缓存击穿服务
type AntiBreakdownService struct {
rdb *MockRedis
db *MockDB
sf *SFGroup
}
func NewAntiBreakdownService(rdb *MockRedis, db *MockDB) *AntiBreakdownService {
return &AntiBreakdownService{rdb: rdb, db: db, sf: NewSFGroup()}
}
func (s *AntiBreakdownService) GetUser(id int) (*User, error) {
key := fmt.Sprintf("user:%d", id)
// 1. 先查缓存
if data, ok := s.rdb.Get(key); ok {
var user User
if json.Unmarshal(data, &user) == nil {
return &user, nil
}
}
// 2. 缓存未命中,用 singleflight 合并并发请求
val, err := s.sf.Do(key, func() (interface{}, error) {
// 双重检查:可能在等待期间其他请求已经写入了缓存
if data, ok := s.rdb.Get(key); ok {
var user User
if json.Unmarshal(data, &user) == nil {
return &user, nil
}
}
user, err := s.db.FindUser(id)
if err != nil {
return nil, err
}
s.rdb.Set(key, user, 5*time.Minute)
return user, nil
})
if err != nil {
return nil, err
}
return val.(*User), nil
}
// ============================================
// 主函数:演示所有缓存策略
// ============================================
func main() {
rdb := NewMockRedis()
db := NewMockDB()
// ============================================
// 1. Cache-Aside 基本演示
// ============================================
fmt.Println("=== Cache-Aside 模式 ===")
svc := NewCacheAsideService(rdb, db)
// 第一次查询(缓存未命中,查 DB)
user, _ := svc.GetUser(1)
fmt.Printf(" 首次查询 ID=1: %s, DB查询次数: %d\n", user.Name, db.QueryCount)
// 第二次查询(缓存命中,不查 DB)
user, _ = svc.GetUser(1)
fmt.Printf(" 二次查询 ID=1: %s, DB查询次数: %d (应不变)\n", user.Name, db.QueryCount)
// 更新后缓存失效
svc.UpdateUser(&User{ID: 1, Name: "张三丰", Age: 30})
user, _ = svc.GetUser(1)
fmt.Printf(" 更新后查询 ID=1: %s, DB查询次数: %d (应+1)\n", user.Name, db.QueryCount)
// ============================================
// 2. 缓存穿透演示
// ============================================
fmt.Println("\n=== 缓存穿透对策 ===")
queryCountBefore := db.QueryCount
// 查不存在的用户
_, err := svc.GetUser(999)
fmt.Printf(" 查询不存在的 ID=999: %v\n", err)
// 第二次查同一个不存在的用户(应命中空值缓存)
_, _ = svc.GetUser(999)
fmt.Printf(" 二次查询 ID=999: DB查询次数增加 %d (应为1,空值缓存生效)\n", db.QueryCount-queryCountBefore)
// ============================================
// 3. 缓存击穿对策(singleflight)
// ============================================
fmt.Println("\n=== 缓存击穿对策(singleflight) ===")
rdb2 := NewMockRedis()
db2 := NewMockDB()
db2.users[10] = &User{ID: 10, Name: "热点用户", Age: 20}
antiSvc := NewAntiBreakdownService(rdb2, db2)
// 手动清除缓存模拟热点 key 过期
rdb2.Del("user:10")
// 50 个 goroutine 同时请求同一个 key
var wg sync.WaitGroup
startCount := db2.QueryCount
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
antiSvc.GetUser(10)
}()
}
wg.Wait()
fmt.Printf(" 50个并发请求同一key: DB查询次数 %d (应为1,singleflight合并)\n", db2.QueryCount-startCount)
// ============================================
// 4. 缓存雪崩对策(TTL 随机抖动)
// ============================================
fmt.Println("\n=== 缓存雪崩对策(TTL 随机抖动) ===")
rdb3 := NewMockRedis()
db3 := NewMockDB()
svc3 := NewCacheAsideService(rdb3, db3)
// 批量加载缓存,每个 key 的 TTL 有随机偏移
for i := 1; i <= 5; i++ {
svc3.GetUser(i)
}
rdb3.mu.RLock()
fmt.Println(" 各 key 过期时间(应有差异):")
now := time.Now()
for k, v := range rdb3.store {
fmt.Printf(" %s: TTL=%v\n", k, v.expiresAt.Sub(now).Round(time.Second))
}
rdb3.mu.RUnlock()
// ============================================
// 5. 真实 go-redis 代码参考(不可运行,仅参考)
// ============================================
fmt.Println("\n=== 真实 go-redis 代码参考 ===")
fmt.Println(`
// 真实 Redis 连接
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
PoolSize: 20,
})
// String 操作
rdb.Set(ctx, "key", "value", 10*time.Minute)
val, err := rdb.Get(ctx, "key").Result()
// Hash 操作
rdb.HSet(ctx, "user:1", "name", "张三", "age", 28)
name, _ := rdb.HGet(ctx, "user:1", "name").Result()
// 分布式锁(SET NX EX)
ok, err := rdb.SetNX(ctx, "lock:order:123", "token", 10*time.Second).Result()
if ok {
defer rdb.Del(ctx, "lock:order:123")
// 执行业务逻辑...
}
// 计数器(INCR)
rdb.Incr(ctx, "page:views:home")
// ZSet 排行榜
rdb.ZAdd(ctx, "ranking", redis.Z{Score: 100, Member: "张三"})
rdb.ZRevRangeWithScores(ctx, "ranking", 0, 9)
`)
}
运行方式
bash
mkdir -p demo5 && cd demo5
go mod init demo5
go run main.go # 无需外部依赖,纯标准库
要点总结
- go-redis v9 所有操作接受
context.Context,支持超时和取消;连接池大小建议设置为CPU核数 * 10 - Cache-Aside 是最常用缓存模式:读时先缓存后 DB,写时先 DB 后删缓存;核心原则是"删除缓存"而非"更新缓存"
- 缓存穿透:查不存在的数据 → 缓存空值(短 TTL)或布隆过滤器
- 缓存击穿 :热点 key 过期 →
singleflight合并并发请求,只查一次 DB - 缓存雪崩 :大量 key 同时过期 → TTL 加随机抖动(
base + rand(0, jitter)) - 分布式锁用
SET key value NX EX seconds:原子性的"不存在才设置 + 过期";释放锁要用 Lua 脚本校验 token 防误删 - 生产环境 Redis key 命名规范 :
业务:实体:ID(如user:1001),冒号分隔层级,便于管理和监控