Go 分布式锁与缓存击穿防护:用 go-redis + redsync 把并发收住
test12026-10-022 次阅读
单机的锁不够用
当服务横向扩容到多个实例,进程内的 sync.Mutex 只能锁住自己这一个节点。需要跨进程互斥时,Redis 是常见选择。redsync 在 Redis 上实现了 Redlock 思路:用一个带过期时间、带随机值的 key 作为锁,释放时校验值一致才删除,避免误删别人的锁。
第一步:拿到锁
用 go-redis 客户端构造 redsync 池,Obtain() 会阻塞直到拿到锁或超时。锁一定要带 TTL,防止持有者崩溃后锁永远不释放。
package main
import (
"context"
"log"
"time"
"github.com/redis/go-redis/v9"
"github.com/go-redsync/redsync/v4"
"github.com/go-redsync/redsync/v4/redis/goredis/v9"
)
func main() {
client := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"})
pool := goredis.NewPool(client)
rs := redsync.New(pool)
mutex := rs.NewMutex("order:lock:1001",
redsync.WithExpiry(8*time.Second),
redsync.WithTries(10),
)
ctx := context.Background()
if err := mutex.LockContext(ctx); err != nil {
log.Fatalf("获取锁失败: %v", err)
}
defer mutex.UnlockContext(ctx)
// 临界区:同一时刻只有一个实例能进入
processOrder(1001)
}
第二步:防缓存击穿
热点 key 过期瞬间大量请求同时打到数据库,就是缓存击穿。常见解法是"只放一个请求回源,其余请求等它填好缓存"。用锁串行化回源即可。
func getHotData(ctx context.Context, key string) (string, error) {
val, err := client.Get(ctx, key).Result()
if err == nil {
return val, nil // 命中缓存,直接返回
}
if err != redis.Nil {
return "", err
}
// 缓存未命中:用锁保证只有一个请求回源
mutex := rs.NewMutex("cache:rebuild:"+key, redsync.WithExpiry(5*time.Second))
if err := mutex.LockContext(ctx); err != nil {
// 抢不到锁,短暂等待后重试读缓存(别的协程可能已填好)
time.Sleep(50 * time.Millisecond)
return client.Get(ctx, key).Result()
}
defer mutex.UnlockContext(ctx)
// 双重检查:抢到锁后再看一次,避免重复回源
if val, err := client.Get(ctx, key).Result(); err == nil {
return val, nil
}
data := queryFromDB(key)
client.Set(ctx, key, data, 5*time.Minute)
return data, nil
}
工程要点
- 锁的 value 必须是随机值,释放时用 Lua 脚本校验后删除,防止 A 持有超时、B 拿到锁后 A 把 B 的锁删了。
- 过期时间要大于临界区最长执行时间;业务可能跑很久时,用看门狗定期续期。
- Redlock 依赖多个独立 Redis 节点,单机 Redis 宕机仍有风险;对一致性要求极高时配合数据库唯一约束兜底。
- 不要把锁当成事务用,它只解决互斥,不保证业务幂等,关键写操作仍需幂等键或唯一索引。
T
test1
文章作者
单机的锁不够用 当服务横向扩容到多个实例,进程内的 sync.Mutex 只能锁住自己这一个节点。需要跨进程互斥时,R...
- 分类
- 技术
- 发布时间
- 2026-10-02
- 字数
- 约 1823 字
- 阅读
- 2 次
