首页 > 运维 > linux运维 > 正文

Redis分布式锁实现理解

爱谁谁
发布: 2025-07-13 08:52:02
原创
206人浏览过

redis中,通过对key值的独占来实现分布式锁看似简单快捷,但实际上存在许多挑战。确保锁资源的安全和及时释放是分布式锁实现的核心问题。以下是逐层分析redis实现分布式锁的过程,以及存在的问题和解决方案。

方案1:setnx

通过setnx命令设置key的方式实现独占锁。

  1. 并发线程抢占锁资源

    setnx an_special_lock 1

  2. 如果1抢占到当前锁,并发线程中的当前线程执行

    if(成功获取锁) execute business_method()

  3. 释放锁

    del an_special_lock

存在的问题很明显:从抢占锁到并发线程中当前线程操作,再到最后的释放锁,并不是一个原子性操作。如果最后的锁没有被成功释放(del an_special_lock),即2到3之间发生了异常,就会导致其他线程永远无法重新获取锁。

方案2:setnx + expire key

为了避免方案1中的这种情况,需要对锁资源加一个过期时间,比如10秒钟。一旦从占锁到释放锁的过程中发生异常,可以保证过期之后锁资源的自动释放。

  1. 并发线程抢占锁资源

    setnx an_special_lock 1

  2. 设置锁的过期时间

    expire an_special_lock 10

  3. 如果1抢占到当前锁,并发线程中的当前线程执行

    if(成功获取锁) execute business_method()

  4. 释放锁

    del an_special_lock

通过设置过期时间(expire an_special_lock 10),避免了占锁到释放锁的过程中发生异常而导致锁无法释放的问题。但是仍旧存在问题:在并发线程抢占锁成功到设置锁的过期时间之间发生了异常,即这里的1到2之间发生了异常,锁资源仍旧无法释放。方案2虽然解决了方案1中锁资源无法释放的问题,但同时引入了一个非原子操作,同样无法保证set key到expire key的原子性执行。因此,目前的问题集中在:如何使得设置一个锁&&设置锁超时时间,即这里的1到2操作,保证以原子的方式执行?

方案3:set key value ex 10 nx

Redis 2.8之后加入了一个set key && expire key的原子操作:set an_special_lock 1 ex 10 nx。

  1. 并发线程抢占锁资源,原子操作

    set an_special_lock 1 ex 10 nx

    奇布塔
    奇布塔

    基于AI生成技术的一站式有声绘本创作平台

    奇布塔41
    查看详情 奇布塔
  2. 如果1抢占到当前锁,并发线程中的当前线程执行

    if(成功获取锁) business_method()

  3. 释放锁

    del an_special_lock

目前,加锁&&设置锁超时成为一个原子操作,可以解决当前线程异常之后,锁可以得到释放的问题。

但是仍旧存在问题:如果在锁超时之后,比如10秒之后,execute_business_method()仍旧没有执行完成,此时锁因过期而被动释放,其他线程仍旧可以获取an_special_lock的锁,并发线程对独占资源的访问仍无法保证。

方案4:业务代码加强

到目前为止,方案3仍旧无法完美解决并发线程访问独占资源的问题。笔者能够想到解决上述问题的办法就是:设置business_method()执行超时时间,如果应用程序中在锁超时之后仍无法执行完成,则主动回滚(放弃当前线程的执行),然后主动释放锁,而不是等待锁的被动释放(超过expire时间释放)。如果无法确保business_method()在锁过期放之前得到成功执行或者回滚,则分布式锁仍是不安全的。

  1. 并发线程抢占锁资源,原子操作

    set an_special_lock 1 ex 10 nx

  2. 如果抢占到当前锁,并发线程中的当前线程执行

    if(成功获取锁) business_method()

    在应用层面控制,业务逻辑操作在Redis锁超时之前,主动回滚

  3. 释放锁

    del an_special_lock

方案5:RedLock - 解决单点Redis故障

截止目前,(假设)可以认为方案4解决了“占锁”&&“安全释放锁”的问题,仍旧无法保证“锁资源的主动释放”:Redis往往通过Sentinel或者集群保证高可用,即便是有了Sentinel或者集群,但是面对Redis的当前节点的故障时,仍旧无法保证并发线程对锁资源的真正独占。具体说就是,当前线程获取了锁,但是当前Redis节点尚未将锁同步至从节点,此时因为单节点的故障造成锁的“被动释放”,应用程序的其它线程(因故障转移)在从节点仍旧可以占用实际上并未释放的锁。Redlock需要多个Redis节点,RedLock加锁时,通过多数节点的方式,解决了Redis节点故障转移情况下,因为数据不一致造成的锁失效问题。其实现原理,简单地说就是,在加锁过程中,如果实现了多数节点加锁成功(非集群的Redis节点),则加锁成功,解决了单节点故障,发生故障转移之后数据不一致造成的锁失效。而释放锁的时候,仅需要向所有节点执行del操作。

Redlock需要多个Redis节点,由于从一台Redis实例转为多台Redis实例,Redlock实现的分布式锁,虽然更安全了,但是必然伴随着效率的下降。

至此,从方案1到方案2到方案3到方案4到方案5,依次解决了前一步的问题,但仍旧是一个非完美的分布式锁实现。

以下通过一个简单的测试来验证Redlock的效果。

案例是一个典型的对数据库“存在则更新,不存在则插入”的并发操作(这里忽略数据库层面的锁),通过对比是否通过Redis分布式锁控制来看效果。

#!/usr/bin/env python3
import redis
import sys
import time
import uuid
import threading
from time import ctime, sleep
from redis import StrictRedis
from redlock import Redlock
from multiprocessing import Pool
import pymssql
import random
<p>class RedLockTest:
_connection_list = None
_lock_resource = None
_ttl = 10  # ttl</p><pre class="brush:php;toolbar:false;"><code>def __init__(self, *args, **kwargs):
    for k, v in kwargs.items():
        setattr(self, k, v)

def get_conn(self):
    try:
        # 如果当前线程获取不到锁,重试次数以及重试等待时间
        conn = Redlock(self._connection_list, retry_count=100, retry_delay=10)
    except:
        raise
    return conn

def execute_under_lock(self, thread_id):
    conn = self.get_conn()
    lock = conn.lock(self._lock_resource, self._ttl)
    if lock:
        self.business_method(thread_id)
        conn.unlock(lock)
    else:
        print("try later")

''' 模拟一个经典的不存在则插入,存在则更新,起多线程并发操作
实际中可能是一个非常复杂的需要独占性的原子性操作 '''
def business_method(self, thread_id):
    print(" thread -----{0}------ execute business method begin".format(thread_id))
    conn = pymssql.connect(host="127.0.0.1", server="SQL2014", port=50503, database="DB01")
    cursor = conn.cursor()
    id = random.randint(0, 100)
    sql_script = ''' select 1 from TestTable where Id = {0} '''.format(id)
    cursor.execute(sql_script)
    if not(cursor.fetchone()):
        sql_script = ''' insert into TestTable values ({0},{1},{1},getdate(),getdate()) '''.format(id, thread_id)
    else:
        sql_script = ''' update TestTable set LastUpdateThreadId ={0} ,LastUpdate = getdate() where Id = {1} '''.format(thread_id, id)
    cursor.execute(sql_script)
    conn.commit()
    cursor.close()
    conn.close()
    print(" thread -----{0}------ execute business method finish".format(thread_id))
登录后复制

if name == "main": redis_servers = [ {"host": "...", "port": 9000, "db": 0}, {"host": "...", "port": 9001, "db": 0}, {"host": "...", "port": 9002, "db": 0}, ] lock_resource = "mylock" ttl = 2000 # 毫秒 redlock_test = RedLockTest(_connection_list=redis_servers, _lock_resource=lock_resource, _ttl=ttl)

<code># redlock_test.execute_under_lock(redlock_test.business_method)
threads = []
for i in range(50):
    # 普通的并发模式调用业务逻辑的方法,会产生大量的主键冲突
    # t = threading.Thread(target=redlock_test.business_method, args=(i,))
    # Redis分布式锁控制下的多线程
    t = threading.Thread(target=redlock_test.execute_under_lock, args=(i,))
    threads.append(t)
begin_time = ctime()
for t in threads:
    t.setDaemon(True)
    t.start()
for t in threads:
    t.join()</code></pre><p><strong>测试1:简单多线程并发</strong></p><p>简单地起多线程执行测试的方法,测试中出现两个很明显的问题:</p><ol><li>出现主键冲突(而报错)</li><li>从打印的日志来看,各个线程在测试的方法中存在交叉执行的情况(日志信息的交叉意味着线程的交叉执行)</li></ol><p><img src="/uploads/20250419/17450238636802f3771e1e1.jpg" alt="Redis分布式锁实现理解" /></p><p><strong>测试2:Redis锁控制下多线程并发</strong></p><p>Redlock的Redis分布式锁为三个独立的Redis节点,无需做集群。</p><p><img src="/uploads/20250419/17450238636802f377a0216.jpg" alt="Redis分布式锁实现理解" /></p><p>当加入Redis分布式锁之后,可以看到,虽然是并发多线程操作,但是在执行实际的测试的方法的时候,都是独占性地执行,从日志也能够看出来,都是一个线程执行完成之后,另一个线程才进入临界资源区。</p><p><img src="/uploads/20250419/17450238636802f377d84c6.jpg" alt="Redis分布式锁实现理解" /></p><p>Redlock相对安全地解决了一开始分布式锁的潜在问题,与此同时,也增加了复杂度,同时在一定程度上降低了效率。</p></code>
登录后复制

以上就是Redis分布式锁实现理解的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号