今天我们来讲一讲如何使用 Go 语言实现 Redis 分布式锁的完整示例,涵盖 加锁、解锁、自动续期、手动续期 以及 Redlock 算法 的实现方式。我们使用 go-redis 作为 Redis 客户端库,github.com/google/uuid 用于生成唯一标识符。

🧰 一、依赖安装

go get github.com/go-redis/redis/v8
go get github.com/google/uuid

✅二、 基础 Redis 客户端配置

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/go-redis/redis/v8"
	"github.com/google/uuid"
)

var (
	rdb *redis.Client
)

func initRedis() {
	rdb = redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "", // no password set
		DB:       0,  // use default DB
	})
}

🔒 三、获取锁(基础实现)

// 获取锁
func acquireLock(ctx context.Context, key string, ttl time.Duration) (string, error) {
	clientID := uuid.NewString()
	result, err := rdb.SetNX(ctx, key, clientID, ttl).Result()
	if err != nil {
		return "", err
	}
	if !result {
		return "", fmt.Errorf("failed to acquire lock")
	}
	return clientID, nil
}

🚪 四、释放锁(使用 Lua 脚本确保原子性)

// Lua 脚本:只有持有者才能释放锁
const unlockScript = `
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
`

func releaseLock(ctx context.Context, key, clientID string) error {
	script := redis.NewScript(unlockScript)
	result, err := script.Run(ctx, rdb, []string{key}, clientID).Result()
	if err != nil {
		return err
	}
	if result.(int64) == 0 {
		return fmt.Errorf("unlock failed: not the lock holder")
	}
	return nil
}

⏳ 五、自动续期(看门狗机制)

// 自动续期函数
func renewLock(ctx context.Context, key, clientID string, interval time.Duration, stopCh <-chan struct{}) {
	ticker := time.NewTicker(interval)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			// 检查是否是当前客户端持有锁,是的话进行续期
			script := redis.NewScript(`
				if redis.call("get", KEYS[1]) == ARGV[1] then
					redis.call("expire", KEYS[1], ARGV[2])
					return 1
				else
					return 0
				end
			`)
			res, err := script.Run(ctx, rdb, []string{key}, clientID, 30).Int64()
			if err != nil || res == 0 {
				return
			}
		case <-stopCh:
			return
		}
	}
}

使用示例:

func demoAutoRenew(ctx context.Context) {
	key := "my_lock"
	ttl := 30 * time.Second
	interval := 10 * time.Second
	stop := make(chan struct{})
	defer close(stop)

	clientID, err := acquireLock(ctx, key, ttl)
	if err != nil {
		panic(err)
	}

	defer releaseLock(ctx, key, clientID)

	// 启动看门狗
	go renewLock(ctx, key, clientID, interval, stop)

	// 模拟业务逻辑
	time.Sleep(60 * time.Second)

	// 停止续期
	close(stop)
}

🔁 六、手动续期(调用 Lua 脚本)

// 手动续期
func renewLockManually(ctx context.Context, key, clientID string, newTTL time.Duration) error {
	script := redis.NewScript(`
		if redis.call("get", KEYS[1]) == ARGV[1] then
			redis.call("expire", KEYS[1], ARGV[2])
			return 1
		else
			return 0
		end
	`)
	result, err := script.Run(ctx, rdb, []string{key}, clientID, int64(newTTL.Seconds())).Result()
	if err != nil {
		return err
	}
	if result.(int64) == 0 {
		return fmt.Errorf("renew failed: not the lock holder")
	}
	return nil
}

使用示例:

func demoManualRenew(ctx context.Context) {
	key := "my_lock"
	ttl := 30 * time.Second

	clientID, err := acquireLock(ctx, key, ttl)
	if err != nil {
		panic(err)
	}

	defer releaseLock(ctx, key, clientID)

	// 手动续期 30s
	err = renewLockManually(ctx, key, clientID, 30*time.Second)
	if err != nil {
		panic(err)
	}
}

🔄 七、Redlock 算法(简化实现)

type RedisNode struct {
	Host string
	Port int
}

func createRedisClients(nodes []RedisNode) []*redis.Client {
	clients := make([]*redis.Client, len(nodes))
	for i, node := range nodes {
		clients[i] = redis.NewClient(&redis.Options{
			Addr: fmt.Sprintf("%s:%d", node.Host, node.Port),
		})
	}
	return clients
}

func redLock(ctx context.Context, key string, value string, ttl time.Duration, nodes []*redis.Client) (bool, error) {
	quorum := len(nodes) / 2 + 1
	lockedNodes := 0

	for _, client := range nodes {
		result, err := client.SetNX(ctx, key, value, ttl).Result()
		if err != nil {
			continue
		}
		if result {
			lockedNodes++
		}
	}

	if lockedNodes >= quorum {
		return true, nil
	}
	return false, fmt.Errorf("failed to acquire lock on majority of nodes")
}

使用示例:

func demoRedLock(ctx context.Context) {
	nodes := []RedisNode{
		{"127.0.0.1", 6379},
		{"127.0.0.1", 6380},
		{"127.0.0.1", 6381},
	}
	clients := createRedisClients(nodes)
	key := "my_redlock"
	value := uuid.NewString()
	ttl := 30 * time.Second

	locked, err := redLock(ctx, key, value, ttl, clients)
	if err != nil {
		panic(err)
	}
	if !locked {
		fmt.Println("Failed to acquire RedLock")
		return
	}

	// 执行业务逻辑
	time.Sleep(10 * time.Second)

	// 释放锁(遍历所有节点)
	for _, client := range clients {
		releaseLock(ctx, key, value)
	}
}

🧩 八、总结

功能 实现方式
获取锁 使用 SETNX
释放锁 使用 Lua 脚本保证原子性
自动续期 启动 goroutine 定期续约
手动续期 Lua 脚本更新过期时间
Redlock 算法 多个 Redis 节点上加锁,多数成功即可

✅ 九、最佳实践建议

  • 使用 defer 释放锁,避免忘记释放导致死锁;
  • 使用 context.Context 控制 goroutine 生命周期;
  • 在实际生产中,建议使用成熟的库如 go-redsync
  • 锁持有期间,业务逻辑应具备幂等性,以应对锁过期后的并发问题。
Logo

更多推荐