内容来自对 chatgpt 的咨询

分布式锁是在多个节点上运行的应用程序中协调工作的一种常用方法,而Redis是实现分布式锁的流行选择。以下是使用Go语言和github.com/go-redis/redis库来设置Redis分布式锁的一个简单示例:

首先,确保你已经安装了该库:

go get -u github.com/go-redis/redis/v8

demo

然后,这是一个简单的Go代码,用于设置和释放Redis分布式锁:

package main

import (
	"context"
	"fmt"
	"time"

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

var ctx = context.Background()

func main() {
	rdb := redis.NewClient(&redis.Options{
		Addr: "localhost:6379", // 你的Redis地址
		DB:   0,                 // 使用默认DB
	})

	lockKey := "my-lock-key"
	lockValue := "my-unique-identifier" // 这应该是一个每次都唯一的值,例如,可以使用UUID
	lockDuration := 5 * time.Second     // 锁持续时间

	// 尝试设置锁
	ok, err := rdb.SetNX(ctx, lockKey, lockValue, lockDuration).Result()
	if err != nil {
		panic(err)
	}

	if ok {
		fmt.Println("Lock acquired!")

		// 你的业务逻辑

		// 释放锁
		val, err := rdb.Get(ctx, lockKey).Result()
		if err == nil && val == lockValue {
			_, err := rdb.Del(ctx, lockKey).Result()
			if err != nil {
				panic(err)
			}
			fmt.Println("Lock released!")
		}
	} else {
		fmt.Println("Could not acquire lock!")
	}
}

注意:

1、这是一个简单的实现,而在实际生产环境中,你可能需要添加一些额外的功能,如重试机制、锁超时检查等。

2、使用SetNX(Set if Not eXists)确保只有一个客户端可以获得锁。

3、为了确保安全性,释放锁之前要检查锁的值确实是你设置的值。这样可以防止错误地释放其他客户端的锁。

4、在大型分布式系统中,你可能还需要考虑使用像Redlock这样的更复杂的算法来确保锁的安全性和稳定性。

问:Redis 分布式锁过期时间怎么合理设置,长了可能影响性能,短了可能任务没执行完

方案一:保证过期时间足够长,大部分的任务都能执行完,任务执行完主动释放锁

方案二:设置一个PT99任务能执行完的过期时间,没执行完则使用守护对锁进行续期,

对于方案二的代码实现

设置锁成功后,初始化一个 channel,随后启用一个协程作为守护进程监听定时器和channel,如果定时器到期则进行续期,如果从channel中取到数据,立刻return返回终止程序。由于任务执行完毕会主动释放锁,释放锁时会关闭守护进程的chan,此时 select 的case 从channel中读取到一个零值,立刻返回,结束锁续期程序。
守护进程定期检测锁是否还在,只要 chan没有被关闭,说明锁还在。
只要锁还在就说明任务还没执行完,那么重置过期时间,等待任务执行完主动释放锁。
问题:该程序的守护进程怎么判断任务是否执行完毕,答:任务执行完毕会主动释放锁,释放锁时会关闭守护进程的chan,这时守护线程就会关闭,就不再续期了。

type Lock struct {
	daemon chan interface{} // 看门狗
	client   *redis.Client  // redis 客户端
	ttl      time.Duration    // 过期时间
	key      string           // 锁key
}

func (l *Lock) Lock(ctx context.Context) error {
	// 尝试获取锁
	var value = "1"
	success, err := l.client.SetNX(l.key, value, l.ttl).Result()
	fmt.Printf("success is %+v err is %+v", success, err)
	if err != nil {
		return errors.New("获取分布式锁失败")
	}
	if !success {
		return errors.New("获取分布式锁失败")
	}
    
	// 加锁成功,启动守护进程进行锁续期
	go l.DaemonLock()
	return nil
}

func (l *Lock) DaemonLock() { // 锁续期
    ticker := time.NewTicker(l.ttl / 2)
    defer ticker.Stop()
    for {
        select {
            case <-ticker.C:
            // 延长锁的过期时间
            ok, err := l.client.Expire(l.key, l.ttl).Result()
            // 异常或锁已经不存在则不再续期
            if err != nil || !ok {
                return
            }
            case <-l.daemon:
            // 已经解锁
            return
        }
    }
}

func (l *Lock) Unlock(ctx context.Context) error {
	// 关闭看门狗
	close(l.daemon)
	err := l.client.Del(l.key).Err()
	if err != nil {
		fmt.Printf("Unlock err=%s", err.Error())
		return err
	}
	return err
}

DaemonLock 会在 l.daemon 这个通道被关闭或者在续约锁失败时退出。

这是因为当一个 Go 语言的通道被关闭后,尝试从这个通道中读取数据将会立即返回一个与通道类型相对应的零值,并且第二个返回值(一个布尔值)会被设为 false。select 语句会不断地尝试从 l.daemon 通道中读取值,如果通道被关闭,select 就会立即从对应的 case 中返回。