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

前言:redis

      因为在平时的工做中,线上服务器是分布式多台部署的,常常会面临解决分布式场景下数据一致性的问题,那么就要利用分布式锁来解决这些问题。因此本身结合实际工做中的一些经验和网上看到的一些资料,作一个讲解和总结。但愿这篇文章能够方便本身之后查阅,同时要是能帮助到他人那也是很好的。数据库

 

===============================================================长长的分割线====================================================================服务器

 

正文:并发

      第一步,自身的业务场景:分布式

      在我平常作的项目中,目前涉及了如下这些业务场景:memcached

      场景一: 好比分配任务场景。在这个场景中,因为是公司的业务后台系统,主要是用于审核人员的审核工做,并发量并非很高,并且任务的分配规则设计成了经过审核人员每次主动的请求拉取,而后服务端从任务池中随机的选取任务进行分配。这个场景看到这里你会以为比较单一,可是实际的分配过程当中,因为涉及到了按用户聚类的问题,因此要比我描述的复杂,可是这里为了说明问题,你们能够把问题简单化理解。那么在使用过程当中,主要是为了不同一个任务同时被两个审核人员获取到的问题。我最终使用了基于数据库资源表的分布式锁来解决的问题。高并发

      场景二: 好比支付场景。在这个场景中,我提供给用户三个用于保护用户隐私的手机号码(这些号码是从运营商处获取的,和真实手机号码看起来是同样的),让用户选择其中一个进行购买,用户购买付款后,我须要将用户选择的号码分配给用户使用,同时也要将没有选择的释放掉。在这个过程当中,给用户筛选的号码要在必定时间内(用户筛选正常时间范围内)让当前用户对这个产品具备独占性,以便保证付款后是100%能够拿到;同时因为产品资源池的资源有限,还要保持资源的流动性,即不能让资源长时间被某个用户占用着。对于服务的设计目标,一期项目上线的时候至少可以支持峰值qps为300的请求,同时在设计的过程当中要考虑到用户体验的问题。我最终使用了memecahed的add()方法和基于数据库资源表的分布式锁来解决的问题。性能

      场景三: 我有一个数据服务,天天调用量在3亿,天天按86400秒计算的qps在4000左右,因为服务的白天调用量要明显高于晚上,因此白天下午的峰值qps达到6000的,一共有4台服务器,单台qps要能达到3000以上。我最终使用了redis的setnx()和expire()的分布式锁解决的问题。优化

       场景四:场景一和场景二的升级版。在这个场景中,不涉及支付。可是因为资源分配一次过程当中,须要保持涉及一致性的地方增长,并且一期的设计目标要达到峰值qps500,因此须要咱们对场景进一步的优化。我最终使用了redis的setnx()、expire()和基于数据库表的分布式锁来解决的问题。线程

 

      看到这里,无论你以为我提出的业务场景qps是否足够大,都但愿你能继续看下去,由于不管你身处一个什么样的公司,最开始的工做可能都须要从最简单的作起。不要提阿里和腾讯的业务场景qps如何大,由于在这样的大场景中你未必能亲自参与项目,亲自参与项目未必能是核心的设计者,是核心的设计者未必能独自设计。若是能真能知足以上三条,关闭页面能够不看啦,若是不是的话,建议仍是看完,我有说的不足的地方欢迎提出建议,我说的好的地方,也但愿给我点个赞或者评论一下,算是对我最大的鼓励哈。

 

  第二步,分布式锁的解决方式:

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

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

      3. 使用memcached的add()方法,用于分布式锁。

      4. 使用memcached的cas()方法,用于分布式锁。(不经常使用) 

      5. 使用redis的setnx()、expire()方法,用于分布式锁。

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

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

      8. 使用zookeeper,用于分布式锁。(不经常使用) 

     

      第三步,基于数据库资源表作乐观锁,用于分布式锁:

      1. 首先说明乐观锁的含义:

          大多数是基于数据版本(version)的记录机制实现的。何谓数据版本号?即为数据增长一个版本标识,在基于数据库表的版本解决方案中,通常是经过为数据库表添加一个 “version”字段来实现读取出数据时,将此版本号一同读出,以后更新时,对此版本号加1。

          在更新过程当中,会对版本号进行比较,若是是一致的,没有发生改变,则会成功执行本次操做;若是版本号不一致,则会更新失败。

      2. 对乐观锁的含义有了必定的了解后,结合具体的例子,咱们来推演下咱们应该怎么处理:

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

          

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

               update t_resource set state=2 where state=1 and id=5780。

         (5). 若是在分布式场景中,因为数据库的update操做是原子是原子的,其实上边这条语句理论上也没有问题,可是这条语句若是在典型的“ABA”状况下,咱们是没法感知的。有人可能会问什么是“ABA”问题呢?你们能够网上搜索一下,这里我说简单一点就是,若是在你第一次select和第二次update过程当中,因为两次操做是非原子的,因此这过程当中,若是有一个线程,先是占用了资源(state=2),而后又释放了资源(state=1),实际上最后你执行update操做的时候,是没法知道这个资源发生过变化的。也许你会说这个在你说的场景中应该也还好吧,可是在实际的使用过程当中,好比银行帐户存款或者扣款的过程当中,这种状况是比较恐怖的。

         (6). 那么若是使用乐观锁咱们如何解决上边的问题呢?

               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语句真正更新影响到了一行数据,那就说明占位成功。若是没有更新影响到一行数据,则说明这个资源已经被别人占位了。

      3. 经过2中的讲解,相信你们已经对如何基于数据库表作乐观锁有有了必定的了解了,可是这里仍是须要说明一下基于数据库表作乐观锁的一些缺点:

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

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

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

      4. 讲了乐观锁的实现方式和缺点,是否是会以为不敢使用乐观锁了呢???固然不是,在文章开头我本身的业务场景中,场景1和场景2的一部分都使用了基于数据库资源表的乐观锁,已经很好的解决了线上问题。因此你们要根据的具体业务场景选择技术方案,并非随便找一个足够复杂、足够新潮的技术方案来解决业务问题就是好方案?!好比,若是在个人场景一中,我使用zookeeper作锁,能够这么作,可是真的有必要吗???答案以为是没有必要的!!!

 

      第四步,使用memcached的add()方法,用于分布式锁:

      对于使用memcached的add()方法作分布式锁,这个在互联网公司是一种比较常见的方式,并且基本上能够解决本身手头上的大部分应用场景。在使用这个方法以前,只要能搞明白memcached的add()和set()的区别,而且知道为何能用add()方法作分布式锁就好。若是还不知道add()和set()方法,请直接百度吧,这个须要本身了解一下。

      我在这里想说明的是另一个问题,人们在关注分布式锁设计的好坏时,还会重点关注这样一个问题,那就是是否能够避免死锁问题???!!!

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

     

      第五步,使用memcached的cas()方法,用于分布式锁:     

      下篇文章咱们再细说!

 

      第六步,使用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设置。

      具体的使用步骤以下:

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

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

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

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

 

      第七步,使用redis的setnx()、get()、getset()方法,用于分布式锁:

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

      那么先说明一下这三个命令,对于setnx()和get()这两个命令,相信不用再多说什么。那么getset()命令?这个命令主要有两个参数 getset(key,newValue)。该方法是原子的,对key设置newValue这个值,而且返回key原来的旧值。假设key原来是不存在的,那么屡次执行这个命令,会出现下边的效果:

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

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

      3. 依次类推!

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

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

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

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

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

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

      注意: 这个方案我当初在线上使用的时候是没有问题的,因此当初写这篇文章时也认为是没有问题的。可是截止到2017.05.13(周六),本身在从新回顾这篇文章时,看了文章下网友的不少评论,我发现有两个问题比较集中:

      问题1:  在“get(lockkey)获取值oldExpireTime ”这个操做与“getset(lockkey, newExpireTime) ”这个操做之间,若是有N个线程在get操做获取到相同的oldExpireTime后,而后都去getset,会不会返回的newExpireTime都是同样的,都会是成功,进而都获取到锁???

      我认为这套方案是不存在这个问题的。依据有两条: 第一,redis是单进程单线程模式,串行执行命令。 第二,在串行执行的前提条件下,getset以后会比较返回的currentExpireTime与oldExpireTime 是否相等。

      问题2: 在“get(lockkey)获取值oldExpireTime ”这个操做与“getset(lockkey, newExpireTime) ”这个操做之间,若是有N个线程在get操做获取到相同的oldExpireTime后,而后都去getset,假设第1个线程获取锁成功,其余锁获取失败,可是获取锁失败的线程它发起的getset命令确实执行了,这样会不会形成第一个获取锁的线程设置的锁超时时间一直在延长???

      我认为这套方案确实存在这个问题的可能。但我我的认为这个微笑的偏差是能够忽略的,不过技术方案上存在缺陷,你们能够自行抉择哈。

 

      第八步,使用redis的watch、multi、exec命令,用于分布式锁:

      下篇文章咱们再细说!

 

      第九步,使用zookeeper,用于分布式锁:

      下篇文章咱们再细说!

 

      第十步,总结:

      综上,关于分布式锁的第一篇文章我就写到这儿了,在文章中主要说明了平常项目中会比较经常使用到四种方案,你们掌握了这四种方案,其实在平常的工做中就能够解决不少业务场景下的分布式锁的问题。从文章开头我本身的实际使用中,也能够看到,这么说彻底是有必定的依据。对于另外那三种方案,我会在下一篇关于分布式锁的文章中,和你们再探讨一下。

      经常使用的四种方案:

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

      2. 使用memcached的add()方法,用于分布式锁。

      3. 使用redis的setnx()、expire()方法,用于分布式锁。

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

      不经常使用可是能够用于技术方案探讨的:

      1. 使用memcached的cas()方法,用于分布式锁。 

      2. 使用redis的watch、multi、exec命令,用于分布式锁。

      3. 使用zookeeper,用于分布式锁。

相关文章
相关标签/搜索