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("获取锁失败,资源已被锁定")
}
}