golang,go,博客,开源,编程

redis实现可重入锁

Published on with 0 views and 0 comments

在分布式系统中,可重入锁(Reentrant Lock)是指一个线程或客户端在已经持有锁的情况下,仍然可以重新获取锁而不会发生死锁。换句话说,锁可以被当前持有者多次请求,每次请求都需要释放一次才能完全释放锁。这种锁对于防止死锁尤其重要,尤其是在递归调用或多次请求同一资源时。

Redis 本身没有内置的可重入锁机制,但是可以通过一些技巧实现。我们可以通过 SETNX(SET if Not Exists)命令结合一些额外的逻辑来模拟一个可重入锁。

可重入锁的设计思路

  1. 锁的持有者:每个客户端请求锁时,Redis 会设置一个唯一标识符(如客户端 ID 或 UUID),表示当前持有锁的客户端。如果同一个客户端再次请求该锁,就不会失败,而是增加锁的重入次数。
  2. 重入计数:通过在 Redis 中存储一个计数器,表示当前锁被持有的次数。如果客户端已经持有锁并且再次请求锁,它只需递增计数器;当锁的持有者释放锁时,减少计数器,直到计数器为 0 时才删除锁。
  3. 锁的过期时间:为了避免由于客户端故障导致锁不被释放,锁会有一个过期时间,防止死锁。客户端可以在持有锁期间更新过期时间。

实现步骤

  1. 获取锁
    • 如果锁不存在,使用 SET 命令加锁并设置一个唯一标识符,且设置过期时间。
    • 如果锁已经被当前客户端持有,增加重入计数。
    • 如果锁已被其他客户端持有,则返回失败。
  2. 释放锁
    • 在释放锁时,减少重入计数。如果计数器为 0,则删除锁。

示例代码

以下是基于 Redis 实现可重入锁的 Go 代码。

1. 实现可重入锁

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.")
	}
}

2. 代码解析

  1. 获取锁
    • 使用 SETNX 命令尝试设置锁。如果锁已存在,则检查是否是当前客户端持有的锁。如果是,则增加重入计数。
    • 如果锁已被其他客户端持有,则返回失败。
  2. 重入计数
    • 使用 Redis 键(如 lockName:count)来存储重入计数器。如果当前客户端已经持有锁,再次请求时只需增加计数器。
    • 每次获取锁时,都会检查计数器,并在释放锁时减少计数器。
  3. 释放锁
    • 如果重入计数为 1,则删除锁并删除计数器。如果重入计数大于 1,则只减少计数器。
  4. 过期时间
    • 使用锁的过期时间(TTL)确保锁不会无限期持有,防止死锁。客户端可以在持有锁的过程中更新 TTL。
  5. 客户端唯一标识
    • 每个客户端都应该有一个唯一标识(如 UUID),用于标识当前持有锁的客户端。

总结

通过使用 Redis 的 SETNX 命令和 Lua 脚本,我们可以实现一个简单的可重入分布式锁。此锁允许一个客户端多次请求并持有锁,直到释放锁时才会真正释放。通过重入计数器,我们能够确保每次加锁都能正确地释放锁。使用过期时间可以防止因客户端崩溃导致锁无法释放的情况。


标题:redis实现可重入锁
作者:mooncakeee
地址:http://blog.dd95828.com/articles/2025/01/07/1736233519634.html
联系:scotttu@163.com