由于在平时的工作中线上服务器是分布式多台部署的经常会面临解决分布式场景下数据一致性的问题那么就要利用分布式锁来解决这些问题。所以自己结合实际工作中的一些经验和网上看到的一些资料做一个讲解和总结。希望这篇文章可以方便自己以后查阅同时要是能帮助到他人那也是很好的。长长的分割线正文:第一步自身的业务场景:在我日常做的项目中目前涉及了以下这些业务场景:场景一:比如分配任务场景。在这个场景中由于是公司的业务后台系统主要是用于审核人员的审核工作并发量并不是很高而且任务的分配规则设计成了通过审核人员每次主动的请求拉取然后服务端从任务池中随机的选取任务进行分配。这个场景看到这里你会觉得比较单一但是实际的分配过程中由于涉及到了按用户聚类的问题所以要比我描述的复杂但是这里为了说明问题大家可以把问题简单化理解。那么在使用过程中主要是为了避免同一个任务同时被两个审核人员获取到的问题。我最终使用了基于数据库资源表的分布式锁来解决的问题。场景二:比如支付场景。在这个场景中我提供给用户三个用于保护用户隐私的手机号码(这些号码是从运营商处获取的和真实手机号码看起来是一样的)让用户选择其中一个进行购买用户购买付款后我需要将用户选择的号码分配给用户使用同时也要将没有选择的释放掉。在这个过程中给用户筛选的号码要在一定时间内(用户筛选正常时间范围内)让当前用户对这个产品具有独占性以便保证付款后是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). 假设我们现在我们对id5780这条数据进行分配那么非分布式场景的情况下我们一般先查询出来state1(未分配)的数据然后从其中选取一条数据可以通过以下语句进行如果可以更新成功那么就说明已经占用了这个资源update t_resource set state2 where state1 and id5780。(5). 如果在分布式场景中由于数据库的update操作是原子是原子的其实上边这条语句理论上也没有问题但是这条语句如果在典型的“ABA”情况下我们是无法感知的。有人可能会问什么是“ABA”问题呢大家可以网上搜索一下这里我说简单一点就是如果在你第一次select和第二次update过程中由于两次操作是非原子的所以这过程中如果有一个线程先是占用了资源(state2)然后又释放了资源(state1)实际上最后你执行update操作的时候是无法知道这个资源发生过变化的。也许你会说这个在你说的场景中应该也还好吧但是在实际的使用过程中比如银行账户存款或者扣款的过程中这种情况是比较恐怖的。(6).那么如果使用乐观锁我们如何解决上边的问题呢a. 先执行select操作查询当前数据的数据版本号,比如当前数据版本号是26select id, resource, state,version from t_resource where state1 and id5780;b. 执行更新操作update t_resoure set state2, version27, update_timenow() where resourcexxxxxx and state1 and version26c. 如果上述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(keynewValue)。该方法是原子的对key设置newValue这个值并且返回key原来的旧值。假设key原来是不存在的那么多次执行这个命令会出现下边的效果1. getset(key, value1) 返回nil 此时key的值会被设置为value12. getset(key, value2) 返回value1 此时key的值会被设置为value23. 依次类推介绍完要使用的命令后具体的使用步骤如下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用于分布式锁:下篇文章我们再细说第十步总结:综上关于分布式锁的第一篇文章我就写到这儿了在文章中主要说明了日常项目中会比较常用到四种方案大家掌握了这四种方案其实在日常的工作中就可以解决很多业务场景下的分布式锁的问题。从文章开头我自己的实际使用中也可以看到这么说完全是有一定的依据。对于另外那三种方案我会在下一篇关于分布式锁的文章中和大家再探讨一下。常用的四种方案: