在分布式系统中,分布式锁是一种非常重要的机制,它能够保证多个进程、线程或机器之间对共享资源的互斥访问。Redis作为一个高效的内存数据库,经常用于实现分布式锁。以下是通过Redis实现分布式锁的七种常见方案,每种方案的优缺点都有不同的侧重点,适用于不同的场景。

1. SETNX + EXPIRE(最基础的分布式锁)

实现方式:

通过Redis的 SETNX 命令来实现锁的加锁操作,通过 EXPIRE 设置过期时间来避免死锁。

  • SETNX 是“SET if Not Exists”的缩写,它的作用是只有当指定的 key 不存在时,才会设置该 key 的值。
  • 设置一个锁,锁的键是唯一的,通过 SETNX 获取锁。
  • 设置一个过期时间,避免在锁持有者死掉时锁不会释放。

代码示例:

import redis
import time

r = redis.Redis()

def acquire_lock(lock_name, timeout=10):
    lock_key = f"lock:{lock_name}"
    # 设置锁,过期时间为timeout
    if r.setnx(lock_key, 1):
        r.expire(lock_key, timeout)
        return True
    return False

def release_lock(lock_name):
    lock_key = f"lock:{lock_name}"
    r.delete(lock_key)

# 使用
if acquire_lock("mylock"):
    try:
        # 执行业务逻辑
        pass
    finally:
        release_lock("mylock")

优缺点:

  • 优点
    • 简单、易于理解。
    • 通过过期时间防止死锁。
  • 缺点
    • 如果客户端崩溃或断开连接,锁会被持有直到超时。
    • 不支持锁重入,无法解决锁的持有者在运行过程中多次请求锁的情况。

2. SET command(Redis 2.6+)

实现方式:

使用 SET 命令的 NX 和 EX 参数,能够同时设置值和过期时间。这种方法在Redis 2.6+中可以使用。

  • NX 表示只有当 key 不存在时才会设置值。
  • EX 设置键的过期时间。

代码示例:

import redis
import time

r = redis.Redis()

def acquire_lock(lock_name, timeout=10):
    lock_key = f"lock:{lock_name}"
    # 使用SET命令实现加锁和设置过期时间
    if r.set(lock_key, 1, nx=True, ex=timeout):
        return True
    return False

def release_lock(lock_name):
    lock_key = f"lock:{lock_name}"
    r.delete(lock_key)

# 使用
if acquire_lock("mylock"):
    try:
        # 执行业务逻辑
        pass
    finally:
        release_lock("mylock")

优缺点:

  • 优点
    • 原子性更强,SET 命令直接支持设置过期时间和NX条件,避免了使用 SETNX 和 EXPIRE 组合的额外操作。
    • 不容易出现 race condition(竞态条件)。
  • 缺点
    • 只适用于Redis 2.6及以上版本。

3. RedLock(Redis官方分布式锁方案)

实现方式:

RedLock 是由 Redis 官方提出的一种分布式锁算法,适用于多个 Redis 实例的分布式环境。

  • RedLock 算法利用了多个独立的 Redis 实例进行加锁,确保当某个 Redis 实例出现故障时,其他实例仍然可以确保锁的安全性。

工作流程:

  1. 客户端从多个 Redis 实例中获取锁。
  2. 客户端对大多数实例中的锁加锁成功后认为获得了锁。
  3. 设置锁的过期时间,并确保锁在每个实例中超时一致。
  4. 解锁时需要释放所有 Redis 实例中的锁。

代码示例:

import redis
import time

class RedLock:
    def __init__(self, redis_nodes):
        self.redis_nodes = [redis.Redis(host=node[0], port=node[1]) for node in redis_nodes]
        
    def acquire_lock(self, lock_name, timeout=10):
        lock_key = f"lock:{lock_name}"
        locks = []
        for r in self.redis_nodes:
            if r.set(lock_key, 1, nx=True, ex=timeout):
                locks.append(r)
        
        if len(locks) >= len(self.redis_nodes) // 2 + 1:
            return True
        else:
            # 释放已获得的锁
            for r in locks:
                r.delete(lock_key)
            return False

    def release_lock(self, lock_name):
        lock_key = f"lock:{lock_name}"
        for r in self.redis_nodes:
            r.delete(lock_key)

# 使用
redis_nodes = [("127.0.0.1", 6379), ("127.0.0.1", 6380), ("127.0.0.1", 6381)]
redlock = RedLock(redis_nodes)
if redlock.acquire_lock("mylock"):
    try:
        # 执行业务逻辑
        pass
    finally:
        redlock.release_lock("mylock")

优缺点:

  • 优点
    • 高可用性,适用于分布式环境,故障容忍度高。
    • 提供了一种基于多个节点的锁实现,提高了系统的可靠性。
  • 缺点
    • 实现复杂。
    • 对Redis的依赖较大。

4. Lua脚本(原子性加锁与释放)

实现方式:

通过Redis的Lua脚本实现加锁和释放的原子操作,保证在执行过程中不会出现竞态条件。

  • 使用Redis的 EVAL 命令执行Lua脚本,可以将加锁和解锁操作封装成一个原子操作。

代码示例:

import redis

r = redis.Redis()

def acquire_lock(lock_name, timeout=10):
    lock_key = f"lock:{lock_name}"
    lock_script = """
    if redis.call("exists", KEYS[1]) == 0 then
        redis.call("set", KEYS[1], ARGV[1], "ex", ARGV[2])
        return 1
    else
        return 0
    end
    """
    # 使用Lua脚本加锁
    return r.eval(lock_script, 1, lock_key, 1, timeout)

def release_lock(lock_name):
    lock_key = f"lock:{lock_name}"
    r.delete(lock_key)

# 使用
if acquire_lock("mylock"):
    try:
        # 执行业务逻辑
        pass
    finally:
        release_lock("mylock")

优缺点:

  • 优点
    • 原子性强,避免了多个操作之间的竞态条件。
    • 适用于复杂的分布式锁场景。
  • 缺点
    • Lua脚本可能影响Redis的性能,尤其是在大量并发请求下。

5. RedLock 的延时过期锁(Redisson方案)

实现方式:

基于 RedLock 算法的实现,不同的是会为每个锁设置随机的过期时间,以防止不同客户端争抢锁的情况。

6. Zookeeper 实现的分布式锁

虽然 Zookeeper 不属于 Redis,但它是一种常见的分布式锁实现,可以和 Redis 配合使用,在需要更高一致性的场景中使用。

7. Redis + 客户端控制锁的超时机制

通过 Redis 和客户端代码配合实现超时机制。客户端会定期发送心跳请求保持锁定,防止死锁。


以上是常见的七种 Redis 实现分布式锁的方案。不同方案适用于不同的场景,选择时需要根据系统的规模、容错要求、锁的粒度等进行权衡。