分布式锁主要解决分布式系统中并发更新安全问题,单机服务的话很简单直接使用语言自身的锁就可以了,现在随便一个服务基本上都是多台机器部署的,只是语言自身的锁就不满足需求了,因为它只能锁本台机器,管不了其他机器,这时候就需要分布式锁了,下面介绍下基于redis分布式锁的实现。

以下是使用 Lua 脚本实现的 Redis 分布式锁示例,使用lua脚本是为了保证多个指令执行的原子性。

  • 加锁脚本
if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end

这个脚本会尝试给指定的key加锁。如果加锁成功,返回1;如果key已经存在,加锁失败,返回0。PX指的是毫秒级别的key过期时间,换成EX则是秒级时间,可以根据需要使用。这里为什么要加过期时间,可不可以只使用 SETNX?为了防止释放锁脚本执行失败,加上过期时间可以当锁释放失败的时候,等锁自动过期不影响下次获取锁,防止死锁。过期时间长短根据锁期间的业务逻辑执行时间来确定,比这个时间稍长。 上面lua加锁脚本用起来还是比较繁琐的,实际上redis 2.6.12版本支持set命令设置nx、ex参数,我们可以直接用set命令完成加锁set [key] [val] px [expire_val] nx,更加简洁方便。

  • 释放锁脚本
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end

这个脚本会尝试释放指定的key对应的锁。如果key不存在或者key对应的锁值与传入的锁值不匹配,释放锁失败,返回0;否则,释放锁成功,返回1。这里值不匹配说明锁已经被重新获取了。

使用这两个脚本时,需要使用 Redis 的 EVAL 命令(或类似命令,如 evalsha)在 Redis 服务器上执行。另外,尽管使用 Lua 脚本可以提高分布式锁的可靠性,但在某些情况下(例如 Redis 主从切换)仍可能导致锁出现问题。在分布式系统中,除非必须,否则尽量避免使用分布式锁。

  • go 代码使用示例
package main

import (
	"context"
	"fmt"
	"github.com/go-redis/redis/v8"
	"log"
	"time"
)

const LockScript = `if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end`
const unlockScript = `if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end`

func main() {
	// 初始化 Redis 客户端
	redisClient := redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	})
	// 锁的 Key 和 Value
	lockKey := "my_lock"
	lockValue := "unique_value"
	lockExpire := 5000 // 锁的过期时间,单位:毫秒
	var ctx = context.Background()
	defer func() {
		// 尝试释放锁
		if releaseLock(ctx, redisClient, lockKey, lockValue) {
			fmt.Println("释放锁成功")
			return
		}
		fmt.Println("释放锁失败,可能已经过期或者被其他客户端获取")
	}()
	// 尝试加锁
	if acquireLock(ctx, redisClient, lockKey, lockValue, time.Duration(lockExpire)) {
		fmt.Println("加锁成功,开始执行业务逻辑")
		// 模拟业务逻辑耗时
		time.Sleep(3 * time.Second)
		fmt.Println("业务逻辑处理完成,释放锁")
		return
	}
	fmt.Println("获取锁失败")

}

// acquireLockByScript 使用lua 脚本获取锁
func acquireLockByScript(ctx context.Context, redisCli *redis.Client, lockKey, lockValue string, lockExpire int) bool {
	res, err := redisCli.Eval(ctx, LockScript, []string{lockKey}, lockValue, lockExpire).Int64()
	if err != nil {
		fmt.Println("ac err", err)
		return false
	}
	return res == 1
}

// acquireLock 使用set ex nx 获取锁
func acquireLock(ctx context.Context, redisCli *redis.Client, lockKey, lockValue string, lockExpire time.Duration) bool {
	ac, err := redisCli.SetNX(ctx, lockKey, lockValue, lockExpire*time.Millisecond).Result()
	if err != nil {
		fmt.Println("ac err", err)
		return false
	}
	return ac
}

// releaseLock 尝试释放分布式锁
func releaseLock(ctx context.Context, redisCli *redis.Client, lockKey, lockValue string) bool {
	result, err := redisCli.Eval(ctx, unlockScript, []string{lockKey}, lockValue).Int64()
	if err != nil {
		log.Fatalf("释放锁失败:%v", err)
	}
	return result == 1
}