redis 分布式锁 setnx

本文介绍了如何利用Redis的setnx命令实现分布式锁。通过setnx的原子性特性,确保了锁的设置是唯一的。在锁的获取过程中,如果未获取到锁,会根据配置决定是否阻塞等待。释放锁时,使用lua脚本进行操作,保证解锁过程的原子性,避免并发问题。整个流程详细阐述了分布式锁在Redis中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

def _create_lock_object(self, key):
    '''
    Returns a lock object, split for testing
    '''
    return redis_lock.Lock(self.redis_conn, key,
                           expire=self.settings['REDIS_LOCK_EXPIRATION'],
                           auto_renewal=True)

这是调用redis_lock.Lock返回了一个lock的对象。

对象获取锁的逻辑代码大致如下

    def acquire(self, blocking=True, timeout=None):
        busy = True
        blpop_timeout = timeout or self._expire or 0
        timed_out = False
        while busy:
            busy = not self._client.set(self._name, self._id, nx=True, ex=self._expire)
            if busy:
                if timed_out:
                    return False
                elif blocking:
                    timed_out = not self._client.blpop(self._signal, blpop_timeout) and timeout
                else:
                    logger.debug("Failed to get %r.", self._name)
                    return False

        logger.debug("Got lock for %r.", self._name)
        if self._lock_renewal_interval is not None:
            self._start_lock_renewer()
        return True

可以看到 busy是用于循环的条件,busy是基于redis的setnx命令完成的分布式锁。

SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。
若key已经存在,则不做任何操作

利用key的插入唯一性,即可以完成锁的设置,争夺锁即争夺key的插入,有且只有一个插入操作可以完成。

回到上面代码,当set不成功时(为争到锁时),查看是否允许阻塞,阻塞即等待信号一个超时时间,不阻塞则立即返回false。

如下为封装redis执行set name value nx的代码:

def set(self, name, value, ex=None, px=None, nx=False, xx=False):

    """
    Set the value at key ``name`` to ``value``


    ``ex`` sets an expire flag on key ``name`` for ``ex`` seconds.

    ``px`` sets an expire flag on key ``name`` for ``px`` milliseconds.

    ``nx`` if set to True, set the value at key ``name`` to ``value`` if it
        does not already exist.

    ``xx`` if set to True, set the value at key ``name`` to ``value`` if it
        already exists.
    """
    pieces = [name, value]
    if ex:
        pieces.append('EX')
        if isinstance(ex, datetime.timedelta):
            ex = ex.seconds + ex.days * 24 * 3600
        pieces.append(ex)
    if px:
        pieces.append('PX')
        if isinstance(px, datetime.timedelta):
            ms = int(px.microseconds / 1000)
            px = (px.seconds + px.days * 24 * 3600) * 1000 + ms
        pieces.append(px)

    if nx:
        pieces.append('NX')
    if xx:
        pieces.append('XX')
    return self.execute_command('SET', *pieces)

释放锁

def release(self):
    """Releases the lock, that was acquired with the same object.

    .. note::

        If you want to release a lock that you acquired in a different place you have two choices:

        * Use ``Lock("name", id=id_from_other_place).release()``
        * Use ``Lock("name").reset()``
    """
    if self._lock_renewal_thread is not None:
        self._stop_lock_renewer()
    logger.debug("Releasing %r.", self._name)
    error = _eval_script(self._client, UNLOCK, self._name, self._signal, args=(self._id,))
    if error == 1:
        raise NotAcquired("Lock %s is not acquired or it already expired." % self._name)
    elif error:
        raise RuntimeError("Unsupported error code %s from EXTEND script." % error)
    else:
        self._delete_signal()

def _delete_signal(self):
    self._client.delete(self._signal)

这里先判断了一下 renewal,为true则会在设置的时间之内会对expire time进行更新,默认设置为float(2/3*expire)。然后即对key进行UNLOCK操作。

看看 UNLOCK如何进行

# 传入的参数为 (self._client, UNLOCK, self._name, self._signal, args=(self._id,))
def _eval_script(redis, script_id, *keys, **kwargs):
    """Tries to call ``EVALSHA`` with the `hash` and then, if it fails, calls
    regular ``EVAL`` with the `script`.
    """
    args = kwargs.pop('args', ()) # (self._id,)
    if kwargs:
        raise TypeError("Unexpected keyword arguments %s" % kwargs.keys())
    try:
        return redis.evalsha(SCRIPTS[script_id], len(keys), *keys + args)
         #笔者注释加: SCRIPTS[script_id]=UNLOCK,len(keys)=2,keys=self._name, self._signal+(self._id,)
    except NoScriptError:
        logger.warn("%s not cached.", SCRIPTS[script_id + 2])
        return redis.eval(SCRIPTS[script_id + 1], len(keys), *keys + args)

使用redis 的evalsha命令。redis 从2.6.0开始可以通过其内置的lua解释器运行lua脚本。其命令即为EVAL。SCRIPTS[script_id] 其实是传入’UNLOCK’使其可以找到下面对应的UNLOCK的lua脚本。

EVAL script numkeys key [key …] arg [arg …]
script 为lua脚本代码
numkeys 为传入的键名参数的个数,即key的个数。
key 为redis存入的键(KEY)。在lua脚本中可以通过1 为基址的形式访问,如key[1]…(以1开始哟~)形式获得其值
arg为 不是键名的参数。在lua脚本中可以通过args[1]…获得其值

lua脚本如下:

UNLOCK_SCRIPT = b"""
    if redis.call("get", KEYS[1]) ~= ARGV[1] then
        return 1
    else
        redis.call("del", KEYS[2])
        redis.call("lpush", KEYS[2], 1)
        redis.call("del", KEYS[1])
        return 0
    end
"""
UNLOCK_SCRIPT_HASH = sha1(UNLOCK_SCRIPT).hexdigest()
Redis分布式锁的实现可以使用RedisSETNX命令(SET if Not eXists)来完成。SETNX命令在键不存在时设置键的值,如果键已经存在则不执行任何操作。 以下是使用SETNX命令实现Redis分布式锁的示例代码(使用Python语言): ```python import redis def acquire_lock(redis_conn, lock_key, expire_time): # 使用SETNX命令尝试获取锁 lock_acquired = redis_conn.setnx(lock_key, 1) if lock_acquired: # 设置锁的过期时间 redis_conn.expire(lock_key, expire_time) return True else: return False def release_lock(redis_conn, lock_key): # 释放锁,删除键 redis_conn.delete(lock_key) # 创建Redis连接 redis_conn = redis.Redis(host='localhost', port=6379, db=0) # 获取锁 if acquire_lock(redis_conn, 'mylock', 10): try: # 执行业务逻辑 print("Lock acquired. Do something here...") finally: # 释放锁 release_lock(redis_conn, 'mylock') else: print("Failed to acquire lock. Another process holds the lock.") ``` 以上代码中,`acquire_lock`函数尝试获取锁并设置过期时间,如果成功获取到锁则返回True,否则返回False。`release_lock`函数用于释放锁,即删除键。 在使用分布式锁时,可以根据具体业务需求设置合适的锁键(`lock_key`)和过期时间(`expire_time`)。确保在获取到锁之后,执行业务逻辑的代码在适当的位置调用`release_lock`函数释放锁,避免锁一直被占用而无法释放。 需要注意的是,RedisSETNX命令是原子操作,可以确保在并发情况下只有一个客户端能够成功获取到锁。同时,设置合适的过期时间能够避免因为异常情况导致锁一直被占用而无法释放。 这只是一个简单的示例,实际使用中还需要考虑异常处理、加锁失败的重试策略以及其他线程安全的因素,以确保分布式锁的可靠性和性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值