# Go Redis Lua 分布式锁

分布式锁在分布式系统中非常重要,它可以帮助我们避免多个进程或线程同时访问共享资源,从而避免数据一致性问题。在Go语言中,我们可以使用Redis来实现分布式锁。

# 获取锁

在Redis中,我们可以使用SETNX命令来获取锁。SETNX命令的作用是,如果指定的key不存在,则设置key的值为指定的value,并返回1;如果指定的key已经存在,则不设置key的值,并返回0。

success, err := d.client.SetNX(ctx, key, value, expiration).Result()

# 释放锁

为了保证分布式锁的拥有者才能释放锁,我们需要在释放锁时判断锁的拥有者是否是当前进程。在Redis中,我们可以使用Lua脚本来实现这个功能。Lua脚本可以在Redis服务器端执行,这样可以保证在执行Lua脚本时,其他客户端无法执行其他命令,从而保证了Lua脚本的原子性。

Lua脚本如下:

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

这个Lua脚本的作用是,如果指定的key的值等于指定的value,则删除key,并返回1;否则,不删除key,并返回0。

在Go语言中,我们可以使用redis.Eval方法来执行Lua脚本:

success, err := d.client.Eval(ctx, luaScript, []string{key}, value).Result()

# RedLock

单个redis实例可能会出现故障,为了解决这个问题,我们可以使用多个redis实例来实现分布式锁。RedLock是一种分布式锁的实现方式,它可以在多个Redis实例上实现分布式锁。RedLock的实现方式如下:

  1. 获取锁时,尝试在多个Redis实例上获取锁,如果获取锁的实例数量大于等于N/2+1,则认为获取锁成功;
  2. 释放锁时,尝试在所有Redis实例上释放锁。

# 代码

package fsdktype

import (
	"context"
	"errors"
	"time"

	"gitea.office.51fanli.com/goweb/fsdk-go/fsdknodelog"
	"gitea.office.51fanli.com/goweb/nodelog"
	"github.com/go-redis/redis/v8"
)

// 分布式锁
type DistributedLock struct {
	client *redis.Client
}

// 实例化分布式锁
func NewDistributedLock(client *redis.Client) *DistributedLock {
	return &DistributedLock{
		client: client,
	}
}

// 尝试获取分布式锁
func (d *DistributedLock) AcquireLock(ctx context.Context, key string, value string, expiration time.Duration) bool {
	success, err := d.client.SetNX(ctx, key, value, expiration).Result()
	if err != nil {
		// 记录日志
		fsdknodelog.Record(nodelog.LogEntry{
			SerialId: 902606,
			Data1:    "acquire-lock-error",
			Data2:    err.Error(),
		})
		return false
	}
	return success
}

// 释放分布式锁
func (d *DistributedLock) ReleaseLock(ctx context.Context, key string, value string) bool {
	// lua脚本 删除key 并判断value是否一致
	luaScript := `
	if redis.call("get", KEYS[1]) == ARGV[1] then
		return redis.call("del", KEYS[1])
	else
		return 0
	end
	`
	success, err := d.client.Eval(ctx, luaScript, []string{key}, value).Result()
	if err != nil {
		// 记录日志
		fsdknodelog.Record(nodelog.LogEntry{
			SerialId: 902606,
			Data1:    "release-lock-error",
			Data2:    err.Error(),
		})
		return false
	}
	return success == int64(1)
}

// RedLock
type RedLock struct {
	clients []*redis.Client
}

func NewRedLock(clients []*redis.Client) (*RedLock, error) {
	if len(clients) < 3 {
		return nil, errors.New("至少需要3个Redis实例")
	}
	return &RedLock{
		clients: clients,
	}, nil
}

// 尝试获取分布式锁
func (r *RedLock) AcquireLock(ctx context.Context, key string, value string, expiration time.Duration) bool {
	var acquireCount int
	quorum := len(r.clients)/2 + 1

	for _, client := range r.clients {
		dl := NewDistributedLock(client)
		if dl.AcquireLock(ctx, key, value, expiration) {
			acquireCount++
		}
		if acquireCount >= quorum {
			return true
		}
	}
	return false
}

// 释放分布式锁
func (r *RedLock) ReleaseLock(ctx context.Context, key string, value string) bool {
	var releaseCount int
	quorum := len(r.clients)/2 + 1

	for _, client := range r.clients {
		dl := NewDistributedLock(client)
		if dl.ReleaseLock(ctx, key, value) {
			releaseCount++
		}
		if releaseCount >= quorum {
			return true
		}
	}
	return false
}
  • DistributedLock 结构体表示一个分布式锁,它包含一个 Redis 客户端。

    • NewDistributedLock 函数用于创建一个新的分布式锁实例。
    • AcquireLock 方法尝试获取分布式锁,如果成功则返回 true,否则返回 false。
    • ReleaseLock 方法释放分布式锁,如果成功则返回 true,否则返回 false。
  • RedLock 结构体表示一个 RedLock,它包含多个 Redis 客户端。

    • NewRedLock 函数用于创建一个新的 RedLock 实例,需要至少3个 Redis 实例。
    • AcquireLock 方法尝试获取 RedLock,如果成功则返回 true,否则返回 false。
    • ReleaseLock 方法释放 RedLock,如果成功则返回 true,否则返回 false。