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