分布式锁-经常使用技术方案

分布式锁的解决方式

一、是否能够考虑采用ReentrantLock来实现,可是实际上去实现的时候是有问题的,ReentrantLock的lock和unlock要求必须是在同一线程进行,而分布式应用中,lock和unlock是两次不相关的请求,所以确定不是同一线程,所以致使没法使用ReentrantLock。redis

二、基于数据库表作乐观锁,用于分布式锁。数据库

三、使用memcached的add()方法,用于分布式锁。缓存

四、使用memcached的cas()方法,用于分布式锁。(不经常使用) 服务器

五、使用redis的setnx()、expire()方法,用于分布式锁。并发

六、使用redis的setnx()、get()、getset()方法,用于分布式锁。分布式

七、使用redis的watch、multi、exec命令,用于分布式锁。(不经常使用) memcached

八、使用zookeeper,用于分布式锁。(不经常使用) 高并发

于数据库资源表作乐观锁,用于分布式锁

大多数是基于数据版本(version)的记录机制实现的。何谓数据版本号?即为数据增长一个版本标识,在基于数据库表的版本解决方案中,通常是经过为数据库表添加一个 “version”字段来实现读取出数据时,将此版本号一同读出,以后更新时,对此版本号加1。在更新过程当中,会对版本号进行比较,若是是一致的,没有发生改变,则会成功执行本次操做;若是版本号不一致,则会更新失败。优化

ABA问题:spa

假设咱们有一张资源表,以下图所示: t_resource , 其中有6个字段id, resoource,  state, add_time, update_time, version,分别表示表主键、资源、分配状态(1未分配  2已分配)、资源建立时间、资源更新时间、资源数据版本号。

假设咱们如今咱们对id=5780这条数据进行分配,那么非分布式场景的状况下,咱们通常先查询出来state=1(未分配)的数据,而后从其中选取一条数据能够经过如下语句进行,若是能够更新成功,那么就说明已经占用了这个资源。 

update t_resource set state=2 where state=1 and id=5780。(相似于CAS操做)返回影响行数0即失败,1即成功。

若是在分布式场景中,因为数据库的update操做是原子是原子的,其实上边这条语句理论上也没有问题,可是这条语句若是在典型的“ABA”状况下,咱们是没法感知的。好比银行帐户存款或者扣款的过程当中,这种状况是比较恐怖的。

乐观锁解决:

a. 先执行select操做查询当前数据的数据版本号,好比当前数据版本号是26:

 select id, resource, state,version from t_resource  where state=1 and id=5780;

 b. 执行更新操做:

 update t_resoure set state=2, version=27, update_time=now() where resource=xxxxxx and state=1 and version=26

 c. 若是上述update语句真正更新影响到了一行数据,那就说明占位成功。若是没有更新影响到一行数据,则说明这个资源已经被别人占位了。

 乐观锁的缺点:

(1). 这种操做方式,使本来一次的update操做,必须变为2次操做: select版本号一次;update一次。增长了数据库操做的次数。

(2). 若是业务场景中的一次业务流程中,多个资源都须要用保证数据一致性,那么若是所有使用基于数据库资源表的乐观锁,就要让每一个资源都有一张资源表,这个在实际使用场景中确定是没法知足的。并且这些都基于数据库操做,在高并发的要求下,对数据库链接的开销必定是没法忍受的

 (3)乐观锁机制每每基于系统中的数据存储逻辑,所以可能会形成脏数据被更新到数据库中。在系统设计阶段,咱们应该充分考虑到这些状况出现的可能性,并进行相应调整,如将乐观锁策略在数据库存储过程当中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开。 

使用memcached的add()方法

对于使用memcached的add()方法作分布式锁,这个在互联网公司是一种比较常见的方式,并且基本上能够解决本身手头上的大部分应用场景。在使用这个方法以前,只要能搞明白memcached的add()和set()的区别,而且知道为何能用add()方法作分布式锁就好。若是key是已经存在的set是更新原来的数据,而add则不会。

memcache::add 方法:add方法用于向memcache服务器添加一个要缓存的数据。

memcache::set 方法:set方法用于设置一个指定key的缓存内容,set方法是add方法和replace方法的集合体

mmecache::replace方法: replace方法用于替换一个指定key的缓存内容,若是key不存在则返回false

比较:

方法 当key存在 当key不存在
add false true
replace 替换(true) false
set 替换(true) true

避免死锁问题:

若是使用memcached的add()命令对资源占位成功了咱们须要在add()的使用指定当前添加的这个key的有效时间,若是不指定有效时间,正常状况下,你能够在执行完本身的业务后,使用delete方法将这个key删除掉,也就是释放了占用的资源。可是,若是在占位成功后,memecached或者本身的业务服务器发生宕机了,那么这个资源将没法获得释放。因此经过对key设置超时时间,即使发生了宕机的状况,也不会将资源一直占用,能够避免死锁的问题。

使用redis的setnx()、expire()方法

对于使用redis的setnx()、expire()来实现分布式锁,这个方案相对于memcached()的add()方案,redis占优点的是,其支持的数据类型更多,而memcached只支持String一种数据类型。

 首先说明一下setnx()命令,setnx的含义就是SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,若是key不存在,则设置当前key成功,返回1;若是当前key已经存在,则设置当前key失败,返回0。可是要注意的是setnx命令不能设置key的超时时间,只能经过expire()来对key设置。

 具体的使用步骤以下: 

一、setnx(lockkey, 1)  若是返回0,则说明占位失败;若是返回1,则说明占位成功

二、expire()命令对lockkey设置超时时间,为的是避免死锁问题。

三、执行完业务代码后,能够经过delete命令删除key。

 这个方案实际上是能够解决平常工做中的需求的,但从技术方案的探讨上来讲,可能还有一些能够完善的地方。好比,若是在第一步setnx执行成功后,在expire()命令执行成功前,发生了宕机的现象,那么就依然会出现死锁的问题,因此若是要对其进行完善的话,可使用redis的setnx()、get()和getset()方法来实现分布式锁。   

使用redis的setnx()、get()、getset()方法

这个方案的背景主要是在setnx()和expire()的方案上针对可能存在的死锁问题,作了一版优化。

getset()命令?这个命令主要有两个参数 getset(key,newValue)。该方法是原子的,对key设置newValue这个值,而且返回key原来的旧值。假设key原来是不存在的,那么屡次执行这个命令,会出现下边的效果:

一、getset(key, "value1")  返回nil   此时key的值会被设置为value1

2. getset(key, "value2")  返回value1   此时key的值会被设置为value2

3. 依次类推!

 介绍完要使用的命令后,具体的使用步骤以下:

 一、setnx(lockkey, 当前时间+过时超时时间) ,若是返回1,则获取锁成功;若是返回0则没有获取到锁,转向2。

 二、get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,若是小于当前系统时间,则认为这个锁已经超时,能够容许别的请求从新获取,转向3。

 三、计算newExpireTime=当前时间+过时超时时间,而后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime。

 四、判断currentExpireTime与oldExpireTime 是否相等,若是相等,说明当前getset设置成功,获取到了锁。若是不相等,说明这个锁又被别的请求获取走了,那么当前请求能够直接返回失败,或者继续重试。

 五、在获取到锁以后,当前线程能够开始本身的业务处理,当处理完毕后,比较本身的处理时间和对于锁设置的超时时间,若是小于锁设置的超时时间,则直接执行delete释放锁;若是大于锁设置的超时时间,则不须要再锁进行处理。

相关文章
相关标签/搜索