快速掌握使用redis分布式锁

redis SETNX

一般redis的分布式锁操作,都是基于redis setnx操作实现的。setnx 意思为 SET if Eot eXist 如果不存在就set。一般通过这个,可以在线上服务实现离线任务,或者做异常流量拦截。

SETNX的缺陷

单纯的setnx,会有错误释放的问题,即: A进程的锁1自动过期后,其他进程加锁2,A错误del掉锁2。

解决思路:

为了避免这种问题,我们需要锁1、锁2都分配一个身份ID,当前进程只释放属于自己的锁。

落地流程:

实战中,容易想到先get一遍锁,再去del。 但实际也同样会有问题,因为如果直接get、del,由于由于这两个指令是多次执行的,没有保证指令的原子性,会导致数据不一致问题(get、del和真实redis不一致)。

redis原生提供了对lua脚本的支持,我们可以通过lua脚本,把get和del都封装到一起,去解决这个问题:

go 复制代码
// 释放锁脚本:验证并删除
var releaseScript = redis.NewScript(`
	if redis.call('GET', KEYS[1]) == ARGV[1] then
		return redis.call('DEL', KEYS[1])
	else
		return 0
	end
`)

// Release 释放锁
func (l *RedisLock) Release() (bool, error) {
	// 执行Lua脚本,原子性地验证并删除锁
	res, err := releaseScript.Run(
		l.ctx,
		l.client,
		[]string{l.key},
		l.value,
	).Result()

	if err != nil {
		return false, err
	}

	// 返回结果为1表示成功释放锁
	return res.(int64) == 1, nil
}

这个value我们可以用一些唯一ID的生成算法,比如UUID,或者雪花ID,去保证唯一。

Golang示例代码

go 复制代码
package main

import (
	"context"
	"fmt"
	"log"
	"time"

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

// RedisLock Redis分布式锁结构
type RedisLock struct {
	client     *redis.Client
	key        string
	value      string
	expiration time.Duration
	ctx        context.Context
}

// NewRedisLock 创建一个新的Redis锁实例
func NewRedisLock(client *redis.Client, key string, expiration time.Duration) *RedisLock {
	return &RedisLock{
		client:     client,
		key:        key,
		value:      fmt.Sprintf("%d", time.Now().UnixNano()), // 使用纳秒时间戳作为唯一标识
		expiration: expiration,
		ctx:        context.Background(),
	}
}

// 加锁脚本:SETNX + EXPIRE 原子操作
var acquireScript = redis.NewScript(`
	if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then
		redis.call('PEXPIRE', KEYS[1], ARGV[2])
		return 1
	else
		return 0
	end
`)

// Acquire 获取锁
func (l *RedisLock) Acquire() (bool, error) {
	// 执行Lua脚本,原子性地执行SETNX和EXPIRE操作
	res, err := acquireScript.Run(
		l.ctx,
		l.client,
		[]string{l.key},
		l.value,
		l.expiration.Milliseconds(),
	).Result()

	if err != nil {
		return false, err
	}

	// 返回结果为1表示成功获取锁
	return res.(int64) == 1, nil
}

// 释放锁脚本:验证并删除
var releaseScript = redis.NewScript(`
	if redis.call('GET', KEYS[1]) == ARGV[1] then
		return redis.call('DEL', KEYS[1])
	else
		return 0
	end
`)

// Release 释放锁
func (l *RedisLock) Release() (bool, error) {
	// 执行Lua脚本,原子性地验证并删除锁
	res, err := releaseScript.Run(
		l.ctx,
		l.client,
		[]string{l.key},
		l.value,
	).Result()

	if err != nil {
		return false, err
	}

	// 返回结果为1表示成功释放锁
	return res.(int64) == 1, nil
}

func main() {
	// 创建Redis客户端
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "", // no password set
		DB:       0,  // use default DB
	})

	// 创建一个锁,设置锁的过期时间为10秒
	lock := NewRedisLock(rdb, "my-distributed-lock", 10*time.Second)

	// 尝试获取锁
	acquired, err := lock.Acquire()
	if err != nil {
		log.Fatalf("获取锁失败: %v", err)
	}

	if acquired {
		defer func() {
			// 确保锁最终被释放
			released, err := lock.Release()
			if err != nil {
				log.Printf("释放锁失败: %v", err)
			} else if released {
				log.Println("锁已成功释放")
			} else {
				log.Println("锁已过期或被其他客户端释放")
			}
		}()

		// 模拟业务处理
		log.Println("获取锁成功,开始处理业务逻辑...")
		time.Sleep(5 * time.Second)
		log.Println("业务逻辑处理完成")
	} else {
		log.Println("获取锁失败,资源已被锁定")
	}
}    
相关推荐
学长毕设9 小时前
基于springboot的食品仓库管理系统
java·spring boot·后端
滴滴答答哒9 小时前
ASP.NET Core + FaceRecognitionDotNet 人脸识别实战:DI 单例、特征向量持久化与 1:N 比对
后端·asp.net
captain37612 小时前
MyBatis操作数据库(入门2)
java·数据库·spring boot·后端·spring·mybatis
IT_陈寒13 小时前
Java序列化踩坑记:对象循环引用让我加班两小时
前端·人工智能·后端
泡海椒13 小时前
05-雷达图多维画像-信用评级多维画像
后端
王中阳Go13 小时前
5.27 分钟干完后端和全部测试:一场三小时笔试的五个失分点,四个不在代码上
大数据·数据库·人工智能·后端·大语言模型
大哥430913 小时前
Modbus 的"字序":一个配错就变成天文数字,或者干脆变成 0
后端
岩钧13 小时前
New API 源码解析:一个生产级 AI 网关的架构与实现
后端
学到老活到老13 小时前
第 24 集:配置文件写了 80,Spring Boot 为什么读到 90?
java·后端·spring
用户3600555790013 小时前
Kestrel_VLLM - 25 · 全注意力层设备化、整层对拍与访存粒度
后端