golang,go,博客,开源,编程
在分布式系统中,可重入锁(Reentrant Lock)是指一个线程或客户端在已经持有锁的情况下,仍然可以重新获取锁而不会发生死锁。换句话说,锁可以被当前持有者多次请求,每次请求都需要释放一次才能完全释放锁。这种锁对于防止死锁尤其重要,尤其是在递归调用或多次请求同一资源时。
Redis 本身没有内置的可重入锁机制,但是可以通过一些技巧实现。我们可以通过 SETNX
(SET if Not Exists)命令结合一些额外的逻辑来模拟一个可重入锁。
SET
命令加锁并设置一个唯一标识符,且设置过期时间。以下是基于 Redis 实现可重入锁的 Go 代码。
package main
import (
"fmt"
"log"
"time"
"github.com/go-redis/redis/v8"
"context"
"github.com/google/uuid"
)
var ctx = context.Background()
// Redis 客户端
var rdb *redis.Client
// 锁的过期时间
var lockTTL = 10 * time.Second
func init() {
// 初始化 Redis 客户端
rdb = redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis 地址
Password: "", // 默认没有密码
DB: 0, // 默认数据库
})
}
// 获取可重入锁
func acquireReentrantLock(lockName string, clientID string) (bool, error) {
// 检查锁是否存在
// 如果锁不存在,使用 SET 命令加锁并设置 clientID 和重入计数
locked, err := rdb.SetNX(ctx, lockName, clientID+":1", lockTTL).Result()
if err != nil {
return false, err
}
if locked {
// 锁成功,返回
return true, nil
}
// 锁已被占用,检查是否是当前客户端持有的锁
currentOwner, err := rdb.Get(ctx, lockName).Result()
if err != nil {
if err == redis.Nil {
return false, fmt.Errorf("lock not found")
}
return false, err
}
// 如果是当前客户端持有的锁,增加重入计数
if currentOwner == clientID {
// 获取当前重入次数
lockCount, err := rdb.Get(ctx, lockName+":count").Result()
if err != nil && err != redis.Nil {
return false, err
}
// 默认初始重入次数为 0
count := 0
if lockCount != "" {
fmt.Sscanf(lockCount, "%d", &count)
}
// 增加重入计数
count++
_, err = rdb.Set(ctx, lockName+":count", count, lockTTL).Result()
if err != nil {
return false, err
}
return true, nil
}
// 锁已被其他客户端持有
return false, nil
}
// 释放可重入锁
func releaseReentrantLock(lockName string, clientID string) error {
// 获取当前重入次数
lockCount, err := rdb.Get(ctx, lockName+":count").Result()
if err != nil && err != redis.Nil {
return err
}
// 默认初始重入次数为 0
count := 0
if lockCount != "" {
fmt.Sscanf(lockCount, "%d", &count)
}
// 如果当前客户端持有锁并且重入次数为 1,则删除锁
if count == 1 {
// 删除锁和重入计数
_, err := rdb.Del(ctx, lockName, lockName+":count").Result()
if err != nil {
return err
}
return nil
}
// 否则,仅减少重入计数
if count > 1 {
count--
_, err := rdb.Set(ctx, lockName+":count", count, lockTTL).Result()
if err != nil {
return err
}
}
return nil
}
func main() {
// 客户端唯一标识
clientID := uuid.New().String()
// 锁的名称
lockName := "myReentrantLock"
// 尝试获取锁
acquired, err := acquireReentrantLock(lockName, clientID)
if err != nil {
log.Fatalf("Error acquiring lock: %v", err)
}
if acquired {
fmt.Println("Lock acquired!")
// 模拟一些操作
time.Sleep(2 * time.Second)
// 释放锁
err := releaseReentrantLock(lockName, clientID)
if err != nil {
log.Fatalf("Error releasing lock: %v", err)
}
fmt.Println("Lock released!")
} else {
fmt.Println("Failed to acquire lock.")
}
}
SETNX
命令尝试设置锁。如果锁已存在,则检查是否是当前客户端持有的锁。如果是,则增加重入计数。lockName:count
)来存储重入计数器。如果当前客户端已经持有锁,再次请求时只需增加计数器。通过使用 Redis 的 SETNX
命令和 Lua 脚本,我们可以实现一个简单的可重入分布式锁。此锁允许一个客户端多次请求并持有锁,直到释放锁时才会真正释放。通过重入计数器,我们能够确保每次加锁都能正确地释放锁。使用过期时间可以防止因客户端崩溃导致锁无法释放的情况。