Redis 实现分布式锁的原子性与可靠性:Redlock vs Setnx

好的,没问题!直接开始我们的Redis分布式锁之旅!

各位观众,晚上好!我是今晚的锁匠——锁住并发Bug,打开性能之门的锁匠! 今天咱们要聊聊Redis分布式锁,这玩意儿就像咱们生活中的锁,目的是为了保护共享资源,防止多个进程(或者线程)同时修改导致数据混乱。

第一幕:锁,锁,锁,锁住我的心(和资源)

想象一下,电商秒杀活动,100件商品,成千上万的人同时抢购,如果没锁住库存,101个人都显示抢购成功,那就尴尬了! 这时候,就需要分布式锁来保证只有一个人能成功扣减库存。

为什么要用Redis?

  • 高性能: Redis是基于内存的,速度快,能抗住高并发。
  • 简单易用: Redis命令简单直观,容易上手。
  • 天然分布式: Redis集群模式本身就支持分布式环境。

第二幕:Setnx的独角戏:简单但有缺陷

最简单的实现方式就是SETNX(SET if Not eXists)命令。 意思是,如果Key不存在,就设置这个Key的值,如果存在,就什么也不做。

代码示例 (Python):

import redis
import time
import uuid

class SimpleRedisLock:
    def __init__(self, redis_client, lock_name, expire_time=10):
        self.redis_client = redis_client
        self.lock_name = lock_name
        self.expire_time = expire_time  # 锁的过期时间,秒

    def acquire_lock(self):
        lock_value = str(uuid.uuid4()) #生成唯一的锁value,防止误删
        lock_key = "lock:" + self.lock_name #加上前缀,方便管理
        acquired = self.redis_client.setnx(lock_key, lock_value)
        if acquired:
            self.redis_client.expire(lock_key, self.expire_time) #设置过期时间
            return lock_value # 返回锁value,用于解锁验证
        else:
            return None # 获取锁失败

    def release_lock(self, lock_value):
        lock_key = "lock:" + self.lock_name
        # 使用lua脚本保证原子性
        script = """
        if redis.call("get",KEYS[1]) == ARGV[1] then
            return redis.call("del",KEYS[1])
        else
            return 0
        end
        """
        result = self.redis_client.eval(script, 1, lock_key, lock_value)
        return result == 1

if __name__ == '__main__':
    redis_client = redis.Redis(host='localhost', port=6379, db=0)
    lock = SimpleRedisLock(redis_client, "my_resource")

    lock_value = lock.acquire_lock()
    if lock_value:
        try:
            print("获取到锁,开始处理资源...")
            time.sleep(5)  # 模拟处理资源
            print("处理资源完成,释放锁...")
        finally:
            if lock.release_lock(lock_value):
                print("锁释放成功")
            else:
                print("锁释放失败")
    else:
        print("未能获取到锁,请稍后重试")

原理:

  1. 加锁: 使用SETNX尝试设置锁的Key,如果设置成功,说明获取到锁。
  2. 设置过期时间: 为了防止程序崩溃导致锁无法释放,需要设置一个过期时间。
  3. 解锁: 删除锁的Key。

存在的问题:

  • 原子性问题: SETNXEXPIRE不是原子操作。 如果SETNX成功后,程序崩溃,EXPIRE就无法执行,导致死锁。
  • 误删问题: 如果锁的过期时间设置太短,任务还没执行完,锁就被自动释放了,后面的请求可能会误删别人的锁。
  • 重入性问题: 同一个线程/进程无法重复获取锁。

改进:

  • 使用SET命令的NXEX参数: Redis 2.6.12以后,SET命令支持NX (Not eXists) 和 EX (EXpire time in seconds) 参数,可以将SETNXEXPIRE合并成一个原子操作。
    def acquire_lock(self):
        lock_value = str(uuid.uuid4())
        lock_key = "lock:" + self.lock_name
        acquired = self.redis_client.set(lock_key, lock_value, nx=True, ex=self.expire_time)
        if acquired:
            return lock_value
        else:
            return None
  • 解锁时校验Value: 为了防止误删别人的锁,解锁时需要判断锁的Value是否是自己的Value。 可以使用Lua脚本保证原子性。 上面的代码已经包含了这个改进。

第三幕:Redlock:终极解决方案?

Redlock是Redis官方推荐的分布式锁算法,旨在解决单Redis节点故障导致锁失效的问题。 它通过在多个独立的Redis节点上加锁,只有超过半数的节点加锁成功,才认为获取锁成功。

原理:

  1. 获取当前时间: 获取当前时间戳。
  2. 尝试在N个Redis节点上加锁: 依次尝试在N个Redis节点上使用相同的Key和Value加锁。 加锁时设置一个超时时间,如果在超时时间内未能加锁成功,则跳过该节点。
  3. 判断是否加锁成功: 如果超过半数的节点加锁成功,且加锁总耗时小于锁的有效时间,则认为获取锁成功。
  4. 释放锁: 如果加锁成功,则需要释放所有节点上的锁。 如果加锁失败,也要释放所有节点上的锁。

代码示例 (Python):

import redis
import time
import uuid

class Redlock:
    def __init__(self, redis_nodes, lock_name, lock_timeout=10, retry_delay=0.2, retry_count=3):
        self.redis_nodes = redis_nodes  # Redis节点列表,例如:[{'host': '127.0.0.1', 'port': 6379}, {'host': '127.0.0.1', 'port': 6380}, ...]
        self.lock_name = lock_name
        self.lock_timeout = lock_timeout  # 锁的超时时间,秒
        self.retry_delay = retry_delay  # 重试延迟,秒
        self.retry_count = retry_count  # 重试次数
        self.quorum = len(redis_nodes) // 2 + 1  # 大多数节点的数量

    def acquire_lock(self):
        lock_value = str(uuid.uuid4())
        lock_key = "redlock:" + self.lock_name
        retry = 0
        while retry < self.retry_count:
            acquired_count = 0
            start_time = time.time()
            for node in self.redis_nodes:
                try:
                    redis_client = redis.Redis(**node)
                    acquired = redis_client.set(lock_key, lock_value, nx=True, ex=self.lock_timeout)
                    if acquired:
                        acquired_count += 1
                except Exception as e:
                    print(f"Error acquiring lock on node {node}: {e}")

            elapsed_time = time.time() - start_time
            is_valid = acquired_count >= self.quorum and elapsed_time < self.lock_timeout

            if is_valid:
                return lock_value
            else:
                # 释放所有节点上的锁
                self._release_lock(lock_value)
                time.sleep(self.retry_delay)
                retry += 1

        return None

    def _release_lock(self, lock_value):
        lock_key = "redlock:" + self.lock_name
        script = """
        if redis.call("get",KEYS[1]) == ARGV[1] then
            return redis.call("del",KEYS[1])
        else
            return 0
        end
        """
        for node in self.redis_nodes:
            try:
                redis_client = redis.Redis(**node)
                redis_client.eval(script, 1, lock_key, lock_value)
            except Exception as e:
                print(f"Error releasing lock on node {node}: {e}")

    def release_lock(self, lock_value):
        self._release_lock(lock_value)

if __name__ == '__main__':
    redis_nodes = [
        {'host': 'localhost', 'port': 6379, 'db': 0},
        {'host': 'localhost', 'port': 6380, 'db': 0},
        {'host': 'localhost', 'port': 6381, 'db': 0}
    ]
    lock = Redlock(redis_nodes, "my_resource")

    lock_value = lock.acquire_lock()
    if lock_value:
        try:
            print("获取到Redlock锁,开始处理资源...")
            time.sleep(5)  # 模拟处理资源
            print("处理资源完成,释放Redlock锁...")
        finally:
            lock.release_lock(lock_value)
            print("Redlock锁释放成功")
    else:
        print("未能获取到Redlock锁,请稍后重试")

注意事项:

  • 节点数量: Redlock需要奇数个Redis节点,通常是3或5个。
  • 故障转移: Redlock依赖于所有Redis节点的时钟同步,如果时钟不同步,可能会导致锁失效。
  • 性能: Redlock需要在多个节点上加锁,性能比单节点锁要差。

Redlock的争议:

Redlock虽然是官方推荐的算法,但也存在一些争议。 有些人认为Redlock并不能完全解决分布式锁的问题,比如网络分区问题。 具体可以参考Martin Kleppmann的文章: "How to do distributed locking correctly"。

第四幕:总结与选择

特性 Setnx (改进版) Redlock
实现难度 简单 复杂
性能 较低
可靠性 较低 较高 (理论上)
适用场景 并发不高 对可靠性要求高,允许一定性能损耗
抗单点故障
复杂度
是否需要时钟同步 是,节点之间需要时钟同步,否则可能出现问题

如何选择?

  • 并发不高,对可靠性要求不高: 使用SET命令的NXEX参数即可。
  • 并发高,对可靠性要求高: 可以考虑Redlock,但需要充分了解其原理和风险。 同时,需要对Redis节点进行监控,确保时钟同步。
  • 不差钱: 直接用Zookeeper,虽然性能不如Redis,但可靠性更高。

最佳实践:

  1. 设置合理的过期时间: 过期时间要足够长,保证任务能执行完成,但又不能太长,防止死锁。
  2. 解锁时校验Value: 防止误删别人的锁。
  3. 使用Lua脚本保证原子性: 防止并发操作导致数据不一致。
  4. 监控Redis节点: 及时发现并处理故障。

第五幕:未来的锁:更智能,更可靠

分布式锁是一个不断发展的领域。 未来,可能会出现更智能,更可靠的分布式锁解决方案。 比如,基于RAFT或者Paxos算法的分布式锁,可以提供更强的容错性和一致性。

最后,记住一句真理:没有银弹!

选择合适的分布式锁方案,需要根据具体的业务场景和需求进行权衡。 不要盲目追求高大上的技术,适合自己的才是最好的。

感谢各位的观看,希望今天的分享对大家有所帮助! 散会!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注