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 次