本文摘抄自如下2篇博文java
http://www.hollischuang.com/archives/1716node
https://blog.csdn.net/u010963948/article/details/79006572mysql
分布式系统:以数据类分布式系统为例,随着数据量愈来愈大,之前单一或者主从的数据存储服务器已经不能知足生产的要求,因而考虑将数据根据必定的算法打散存储到不一样的服务器上。redis
分布式锁:在分布式数据库中若是访问共享的资源,那么就要求在同一时刻只有一个线程的某个方法访问数据,以保证数据的一致性,不出现并发的问题。在这种场景下,之前数据库的表级锁和行级锁不能知足要求,需设计一个跨数据库的锁,也就是分布式锁。分布式锁的核心原理就是利用一个物理上全局的对象控制分布式系统中资源的访问。好比数据库表的一行记录、一个文件、缓存数据库中的对象、内存中的一个对象等等,具体有数据库、redis、memorycache、zookeeper等算法
分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉咱们“任何一个分布式系统都没法同时知足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时知足两项。”因此,不少系统在设计之初就要对这三者作出取舍。在互联网领域的绝大多数的场景中,都须要牺牲强一致性来换取系统的高可用性,系统每每只须要保证“最终一致性”,只要这个最终时间是在用户能够接受的范围内便可。sql
在不少场景中,为了保证数据的最终一致性,须要不少的技术方案来支持,好比分布式事务、分布式锁等。有的时候,咱们须要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,Java中其实提供了不少并发处理相关的API,可是这些API在分布式场景中就无能为力了。也就是说单纯的Java Api并不能提供分布式锁的能力。因此针对分布式锁的实现目前有多种方案。数据库
针对分布式锁的实现,目前比较经常使用的有如下几种方案:编程
基于数据库实现分布式锁 基于缓存(redis,memcached)实现分布式锁 基于Zookeeper实现分布式锁
在分析这几种实现方案以前先来想一下,咱们须要的分布式锁应该是怎么样的?(这里以方法锁为例,资源锁同理)后端
能够保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。 这把锁要是一把可重入锁(避免死锁) 这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条) 有高可用的获取锁和释放锁功能 获取锁和释放锁的性能要好
要实现分布式锁,最简单的方式可能就是直接建立一张锁表,而后经过操做该表中的数据来实现了。缓存
当咱们要锁住某个方法或资源时,咱们就在该表中增长一条记录,想要释放锁的时候就删除这条记录。
建立这样一张数据库表:
CREATE TABLE `methodLock` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名', `desc` varchar(1024) NOT NULL DEFAULT '备注信息', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成', PRIMARY KEY (`id`), UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
当咱们想要锁住某个方法时,执行如下SQL:
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)
由于咱们对method_name
作了惟一性约束,这里若是有多个请求同时提交到数据库的话,数据库会保证只有一个操做能够成功,那么咱们就能够认为操做成功的那个线程得到了该方法的锁,能够执行方法体内容。
当方法执行完毕以后,想要释放锁的话,须要执行如下Sql:
delete from methodLock where method_name ='method_name'
上面这种简单的实现有如下几个问题:
一、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会致使业务系统不可用。 二、这把锁没有失效时间,一旦解锁操做失败,就会致使锁记录一直在数据库中,其余线程没法再得到到锁。 三、这把锁只能是非阻塞的,由于数据的insert操做,一旦插入失败就会直接报错。没有得到锁的线程并不会进入排队队列,要想再次得到锁就要再次触发得到锁操做。 四、这把锁是非重入的,同一个线程在没有释放锁以前没法再次得到该锁。由于数据中数据已经存在了。
固然,咱们也能够有其余方式解决上面的问题。
除了能够经过增删操做数据表中的记录之外,其实还能够借助数据中自带的锁来实现分布式的锁。
咱们还用刚刚建立的那张数据库表。能够经过数据库的排他锁来实现分布式锁。 基于MySql的InnoDB引擎,可使用如下方法来实现加锁操做:
public boolean lock(){ connection.setAutoCommit(false) while(true){ try{ result = select * from methodLock where method_name=xxx for update; if(result==null){ return true; } }catch(Exception e){ } sleep(1000); } return false; }
在查询语句后面增长for update
,数据库会在查询过程当中给数据库表增长排他锁(这里再多提一句,InnoDB引擎在加锁的时候,只有经过索引进行检索的时候才会使用行级锁,不然会使用表级锁。这里咱们但愿使用行级锁,就要给method_name添加索引,值得注意的是,这个索引必定要建立成惟一索引,不然会出现多个重载方法之间没法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁以后,其余线程没法再在该行记录上增长排他锁。
咱们能够认为得到排它锁的线程便可得到分布式锁,当获取到锁以后,能够执行方法的业务逻辑,执行完方法以后,再经过如下方法解锁:
public void unlock(){ connection.commit(); }
经过connection.commit()
操做来释放锁。
这种方法能够有效的解决上面提到的没法释放锁和阻塞锁的问题。
for update
语句会在执行成功后当即返回,在执行失败时一直处于阻塞状态,直到成功。可是仍是没法直接解决数据库单点和可重入问题。
这里还可能存在另一个问题,虽然咱们对method_name
使用了惟一索引,而且显示使用for update
来使用行级锁。可是,MySql会对查询进行优化,即使在条件中使用了索引字段,可是否使用索引来检索数据是由 MySQL 经过判断不一样执行计划的代价来决定的,若是 MySQL 认为全表扫效率更高,好比对一些很小的表,它就不会使用索引,这种状况下 InnoDB 将使用表锁,而不是行锁。若是出现这种状况,使用force index强制mysql走索引。
还有一个问题,就是咱们要使用排他锁来进行分布式锁的lock,那么一个排他锁长时间不提交,就会占用数据库链接。一旦相似的链接变得多了,就可能把数据库链接池撑爆。若是出现这种状况,能够设置SQL执行的超时时间,好比mybatis的全局设置中 <setting name="defaultStatementTimeout" value="25"/>
总结一下使用数据库来实现分布式锁的方式,这两种方式都是依赖数据库的一张表,一种是经过表中的记录的存在状况肯定当前是否有锁存在,另一种是经过数据库的排他锁来实现分布式锁。
数据库实现分布式锁的优势
直接借助数据库,容易理解。
数据库实现分布式锁的缺点
会有各类各样的问题,在解决问题的过程当中会使整个方案变得愈来愈复杂。
操做数据库须要必定的开销,性能问题须要考虑。
使用数据库的行级锁并不必定靠谱,尤为是当咱们的锁表并不大的时候。
相比较于基于数据库实现分布式锁的方案来讲,基于缓存来实如今性能方面会表现的更好一点。
目前有不少成熟的缓存产品,包括Redis,memcached等。这里以Redis为例来分析下使用缓存实现分布式锁的方案。
基于Redis实现分布式锁在网上有不少相关文章,其中主要的实现方式是使用Jedis.setNX方法来实现
public boolean trylock(String key) { ResultCode code = jedis.setNX(key, "This is a Lock."); if (ResultCode.SUCCESS.equals(code)) return true; else return false; } public boolean unlock(String key){ ldbTairManager.invalid(NAMESPACE, key); }
以上实现方式一样存在几个问题:
一、单点问题。
二、这把锁没有失效时间,一旦解锁操做失败,就会致使锁记录一直在redis中,其余线程没法再得到到锁。
三、这把锁只能是非阻塞的,不管成功仍是失败都直接返回。
四、这把锁是非重入的,一个线程得到锁以后,在释放锁以前,没法再次得到该锁,由于使用到的key在redis中已经存在。没法再执行setNX操做。
五、这把锁是非公平的,全部等待的线程同时去发起setNX操做,运气好的线程能获取锁。
固然,一样有方式能够解决。
如今主流的缓存服务都支持集群部署,经过集群来解决单点问题。
没有失效时间?redis的setExpire方法支持传入失效时间,到达时间以后数据会自动删除。
非阻塞?while重复执行。
非可重入?在一个线程获取到锁以后,把当前主机信息和线程信息保存起来,下次再获取以前先检查本身是否是当前锁的拥有者。
非公平?在线程获取锁以前先把全部等待的线程放入一个队列中,而后按先进先出原则获取锁。
redis的做者Salvatore Sanfilippo,提出了Redlock算法,该算法实现了比单一节点更安全、可靠的分布式锁管理(DLM)。
Redlock算法假设有N个redis节点,这些节点互相独立,通常设置为N=5,这N个节点运行在不一样的机器上以保持物理层面的独立。
算法的步骤以下:
该专家提到,考虑分布式锁的时候须要考虑两个方面:性能和正确性。
若是使用高性能的分布式锁,对正确性要求不高的场景下,那么使用缓存锁就足够了。
若是使用可靠性高的分布式锁,那么就须要考虑严格的可靠性问题。而Redlock则不符合正确性。为何不符合呢?专家列举了几个方面。
如今不少编程语言使用的虚拟机都有GC功能,在Full GC的时候,程序会停下来处理GC,有些时候Full GC耗时很长,甚至程序有几分钟的卡顿,文章列举了HBase的例子,HBase有时候GC几分钟,会致使租约超时。并且Full GC何时到来,程序没法掌控,程序的任什么时候候均可能停下来处理GC,好比下图,客户端1得到了锁,正准备处理共享资源的时候,发生了Full GC直到锁过时。这样,客户端2又得到了锁,开始处理共享资源。在客户端2处理的时候,客户端1 Full GC完成,也开始处理共享资源,这样就出现了2个客户端都在处理共享资源的状况。
专家给出了解决办法,以下图,看起来就是MVCC,给锁带上token,token就是version的概念,每次操做锁完成,token都会加1,在处理共享资源的时候带上token,只有指定版本的token可以处理共享资源。
而后专家还说到了算法依赖本地时间,并且redis在处理key过时的时候,依赖gettimeofday方法得到时间,而不是monotonic clock,这也会带来时间的不许确。好比一下场景,两个客户端client 1和client 2,5个redis节点nodes (A, B, C, D and E)。
总结专家关于Redlock不可用的两点:
redis做者看到这个专家的文章后,写了一篇博客予以回应。做者很客气的感谢了专家,而后表达出了对专家观点的不认同。
I asked for an analysis in the original Redlock specification here: http://redis.io/topics/distlock. So thank you Martin. However I don’t agree with the analysis.
redis做者关于使用token解决锁超时问题能够归纳成下面五点:
专家说到的另外一个时钟问题,redis做者也给出了解释。客户端实际得到的锁的时间是默认的超时时间,减去获取锁所花费的时间,若是获取锁花费时间过长致使超过了锁的默认超时间,那么此时客户端并不能获取到锁,不会存在专家提出的例子。
第一个问题我归纳为,在一个客户端获取了分布式锁后,在客户端的处理过程当中,可能出现锁超时释放的状况,这里说的处理中除了GC等非抗力外,程序流程未处理完也是可能发生的。以前在说到数据库锁设置的超时时间2分钟,若是出现某个任务占用某个订单锁超过2分钟,那么另外一个交易中心就能够得到这把订单锁,从而两个交易中心同时处理同一个订单。正常状况,任务固然秒级处理完成,但是有时候,加入某个rpc请求设置的超时时间过长,一个任务中有多个这样的超时请求,那么,极可能就出现超过自动解锁时间了。当初咱们的交易模块是用C++写的,不存在GC,若是用java写,中间还可能出现Full GC,那么锁超时解锁后,本身客户端没法感知,是件很是严重的事情。我以为这不是锁自己的问题,上面说到的任何一个分布式锁,只要自带了超时释放的特性,都会出现这样的问题。若是使用锁的超时功能,那么客户端必定得设置获取锁超时后,采起相应的处理,而不是继续处理共享资源。Redlock的算法,在客户端获取锁后,会返回客户端能占用的锁时间,客户端必须处理该时间,让任务在超过该时间后中止下来。
第二个问题,天然就是分布式专家没有理解Redlock。Redlock有个关键的特性是,获取锁的时间是锁默认超时的总时间减去获取锁所花费的时间,这样客户端处理的时间就是一个相对时间,就跟本地时间无关了。
由此看来,Redlock的正确性是能获得很好的保证的。仔细分析Redlock,相比于一个节点的redis,Redlock提供的最主要的特性是可靠性更高,这在有些场景下是很重要的特性。可是我以为Redlock为了实现可靠性,却花费了过大的代价。
分析了这么多缘由,我以为Redlock的问题,最关键的一点在于Redlock须要客户端去保证写入的一致性,后端5个节点彻底独立,全部的客户端都得操做这5个节点。若是5个节点有一个leader,客户端只要从leader获取锁,其余节点能同步leader的数据,这样,分区、超时、冲突等问题都不会存在。因此为了保证分布式锁的正确性,我以为使用强一致性的分布式协调服务能更好的解决问题
使用缓存实现分布式锁的优势
性能好,实现起来较为方便。
使用缓存实现分布式锁的缺点
经过超时时间来控制锁的失效时间并非十分的靠谱。
基于zookeeper临时有序节点能够实现的分布式锁。
大体思想即为:每一个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个惟一的瞬时有序节点。 判断是否获取锁的方式很简单,只须要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除便可。同时,其能够避免服务宕机致使的锁没法释放,而产生的死锁问题。
来看下Zookeeper能不能解决前面提到的问题。
锁没法释放?使用Zookeeper能够有效的解决锁没法释放的问题,由于在建立锁的时候,客户端会在ZK中建立一个临时节点,一旦客户端获取到锁以后忽然挂掉(Session链接断开),那么这个临时节点就会自动删除掉。其余客户端就能够再次得到锁。
非阻塞锁?使用Zookeeper能够实现阻塞的锁,客户端能够经过在ZK中建立顺序节点,而且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端能够检查本身建立的节点是否是当前全部节点中序号最小的,若是是,那么本身就获取到锁,即可以执行业务逻辑了。
不可重入?使用Zookeeper也能够有效的解决不可重入的问题,客户端在建立节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就能够了。若是和本身的信息同样,那么本身直接获取到锁,若是不同就再建立一个临时的顺序节点,参与排队。
单点问题?使用Zookeeper能够有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就能够对外提供服务。
公平问题?使用Zookeeper能够解决公平锁问题,客户端在ZK中建立的临时节点是有序的,每次锁被释放时,ZK能够通知最小节点来获取锁,保证了公平。
Zookeeper是一个保证了弱一致性即最终一致性的分布式组件。
Zookeeper采用称为Quorum Based Protocol的数据同步协议。假如Zookeeper集群有N台Zookeeper服务器(N一般取奇数,3台可以知足数据可靠性同时有很高读写性能,5台在数据可靠性和读写性能方面平衡最好),那么用户的一个写操做,首先同步到N/2 + 1台服务器上,而后返回给用户,提示用户写成功。基于Quorum Based Protocol的数据同步协议决定了Zookeeper可以支持什么强度的一致性。
在分布式环境下,知足强一致性的数据储存基本不存在,它要求在更新一个节点的数据,须要同步更新全部的节点。这种同步策略出如今主从同步复制的数据库中。可是这种同步策略,对写性能的影响太大而不多见于实践。由于Zookeeper是同步写N/2+1个节点,还有N/2个节点没有同步更新,因此Zookeeper不是强一致性的。
用户的数据更新操做,不保证后续的读操做可以读到更新后的值,可是最终会呈现一致性。牺牲一致性,并非彻底无论数据的一致性,不然数据是混乱的,那么系统可用性再高分布式再好也没有了价值。牺牲一致性,只是再也不要求关系型数据库中的强一致性,而是只要系统能达到最终一致性便可。
Zookeeper是否知足因果一致性,须要看客户端的编程方式。
不知足因果一致性的作法
知足因果一致性的作法
第二种事件监听机制也是对Zookeeper进行正确编程应该使用的方法,因此,Zookeeper应该是知足因果一致性的
因此咱们在基于Zookeeper实现分布式锁的时候,应该使用知足因果一致性的作法,即等待锁的线程都监听Zookeeper上锁的变化,在锁被释放的时候,Zookeeper会将锁变化的通知告诉知足公平锁条件的等待线程。
能够直接使用zookeeper第三方库 Curator 客户端,这个客户端中封装了一个可重入的锁服务。
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { try { return interProcessMutex.acquire(timeout, unit); } catch (Exception e) { e.printStackTrace(); } return true; } public boolean unlock() { try { interProcessMutex.release(); } catch (Throwable e) { log.error(e.getMessage(), e); } finally { executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS); } return true; }
Curator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁,release方法用于释放锁。
使用ZK实现的分布式锁好像彻底符合了本文开头咱们对一个分布式锁的全部指望。可是,其实并非,Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并无缓存服务那么高。由于每次在建立锁和释放锁的过程当中,都要动态建立、销毁瞬时节点来实现锁功能。ZK中建立和删除节点只能经过Leader服务器来执行,而后将数据同不到全部的Follower机器上。
其实,使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的状况,因为网络抖动,客户端可ZK集群的session链接断了,那么zk觉得客户端挂了,就会删除临时节点,这时候其余客户端就能够获取到分布式锁了。就可能产生并发问题。这个问题不常见是由于zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。屡次重试以后还不行的话才会删除临时节点。(因此,选择一个合适的重试策略也比较重要,要在锁的粒度和并发之间找一个平衡。)
使用Zookeeper实现分布式锁的优势
有效的解决单点问题,不可重入问题,非阻塞问题以及锁没法释放的问题。实现起来较为简单。
使用Zookeeper实现分布式锁的缺点
性能上不如使用缓存实现分布式锁。 须要对ZK的原理有所了解。
上面几种方式,哪一种方式都没法作到完美。就像CAP同样,在复杂性、可靠性、性能等方面没法同时知足,因此,根据不一样的应用场景选择最适合本身的才是王道。
数据库 > 缓存 > Zookeeper
Zookeeper >= 缓存 > 数据库
缓存 > Zookeeper >= 数据库
Zookeeper > 缓存 > 数据库