在Redis上,能够经过对key值的独占来实现分布式锁,表面上看,Redis能够简单快捷经过set key这一独占的方式来实现,也有许多重复性轮子,但实际状况并不是如此。
总得来讲,Redis实现分布式锁,如何确保锁资源的安全&及时释放,是分布式锁的最关键因素。
以下逐层分析Redis实现分布式锁的一些过程,以及存在的问题和解决办法。html
solution 1 :setnxpython
setnx命令设置key的方式实现独占锁redis
1,#并发线程抢占锁资源
setnx an_special_lock 1
2,#若是1抢占到当前锁,并发线程中的当前线程执行
if(成功获取锁)
execute business_method()
3,#释放锁
del an_special_locksql
存在的问题很明显:
从抢占锁,而后并发线程中当前的线程操做,到最后的释放锁,并非一个原子性操做,
若是最后的锁没有被成功释放(del an_special_lock),也即2~3之间发生了异常,就会形成其余线程永远没法从新获取锁数据库
solution 2:setnx + expire key安全
为了不solution 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之间发生了异常,锁资源仍旧没法释放
solution 2虽然解决了solution 1中锁资源没法释放的问题,但与此同时,又引入了一个非原子操做,一样没法保证set key到expire key的以原子的方式执行
所以目前问题集中在:如何使得设置一个锁&&设置锁超时时间,也即这里的1~2操做,保证以原子的方式执行?app
solution 3 : set key value ex 10 nxdom
Redis 2.8以后加入了一个set key && expire key的原子操做:set an_special_lock 1 ex 10 nx
1,#并发线程抢占锁资源,原子操做 set an_special_lock 1 ex 10 nx 2,#若是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 n 2,#若是抢占到当前锁,并发线程中的当前线程执行 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 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 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相对安全地解决了一开始分布式锁的潜在问题,与此同时,也增长了复杂度,同时在必定程度上下降了效率。
以上粗浅分析了Redis分布式锁的各类实现以及潜在问题,即使是Redlock,也不是一个完美的分布式锁解决方案,关于Redis的Redlock的争议也有
http://zhangtielei.com/posts/blog-redlock-reasoning.html仔细阅读会发现,偏偏这些“争议”自己,才是Redis分布式锁最大的精髓所在。