Redis分布式锁实现理解

2023年 4月 29日 37.2k 0

在Redis上,可以通过对key值的独占来实现分布式锁,表面上看,Redis可以简单快捷通过set key这一独占的方式来实现,也有许多重复性轮子,但实际情况并非如此。 总得来说,Redis实现分

在Redis上,可以通过对key值的独占来实现分布式锁,表面上看,Redis可以简单快捷通过set key这一独占的方式来实现,也有许多重复性轮子,但实际情况并非如此。总得来说,Redis实现分布式锁,如何确保锁资源的安全&及时释放,是分布式锁的最关键因素。如下逐层分析Redis实现分布式锁的一些过程,以及存在的问题和解决办法。

solution 1 :setnx

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

1,#并发线程抢占锁资源setnx an_special_lock 12,#如果1抢占到当前锁,并发线程中的当前线程执行if(成功获取锁)  execute business_method()  3,#释放锁   del an_special_lock

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

solution 2:setnx + expire key

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

1,#并发线程抢占锁资源setnx an_special_lock 12,#设置锁的过期时间expire an_special_lock 103,#如果1抢占到当前锁,并发线程中的当前线程执行if(成功获取锁)  execute business_method()  4,#释放锁   del an_special_lock

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

solution 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 nx2,#如果1抢占到当前锁,并发线程中的当前线程执行if(成功获取锁)  business_method()  3,#释放锁  del an_special_lock

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

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

solution 4: 业务代码加强

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

1,#并发线程抢占锁资源,原子操作set an_special_lock 1 ex 10 n2,#如果抢占到当前锁,并发线程中的当前线程执行if(成功获取锁)  business_method()#在应用层面控制,业务逻辑操作在Redis锁超时之前,主动回滚  3,#释放锁  del an_special_lock

solution 5 RedLock: 解决单点Redis故障

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

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

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

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

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

#!/usr/bin/env Python3import redisimport sysimport timeimport uuidimport threadingfrom time import ctime,sleepfrom redis import StrictRedisfrom redlock import Redlockfrom multiprocessing import Poolimport pymssqlimport random

class RedLockTest:

    _connection_list = None    _lock_resource = None    _ttl = 10  #ttl

    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)

    #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()

测试 1,简单多线程并发

简单地起多线程执行测试的方法,测试中出现两个很明显的问题1,出现主键冲突(而报错)2,从打印的日志来看,各个线程在测试的方法中存在交叉执行的情况(日志信息的交叉意味着线程的交叉执行)

测试 2,Redis锁控制下多线程并发

Redlock的Redis分布式锁为三个独立的Redis节点,无需做集群

当加入Redis分布式锁之后,可以看到,虽然是并发多线程操作,但是在执行实际的测试的方法的时候,都是独占性地执行,从日志也能够看出来,都是一个线程执行完成之后,另一个线程才进入临界资源区。

Redlock相对安全地解决了一开始分布式锁的潜在问题,与此同时,也增加了复杂度,同时在一定程度上降低了效率。

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论