工做中常常会遇到争抢共享资源的场景,好比用户抢购秒杀商品,若是不对商品库存进行保护,可能会形成超卖的状况。超卖现象在售卖火车票的场景下更加明显,两我的购买到同一天同一辆列车,相同座位的状况是不容许出现的。交易系统中的退款一样如此,因为网络延迟和重复提交极端时间差的状况下,可能会形成同一个用户重复的退款请求。以上不管是超卖,仍是重复退款,都是没有对须要保护的资源或业务进行完善的保护而形成的,从设计方面必定要避免这种状况的发生。
php
本文以退款交易场景入手,引入分布式锁,尝试分析分布式锁须要考虑关注点,包括如下内容:html
锁是一种控制共享资源争抢的机制,采用互斥方式防止多线程(或多进程)间形成的冲突。锁是一种获取保护资源的凭证,就像公园门票,只有持有门票才有资格入园;锁是使得对同一类共享资源的访问串行化。没有得到锁只能排队等待,直到其余线程释放掉锁。这里须要对“同一类共享资源”正确理解,好比订单系统中的同一种商品库存,退款系统中同一个用户。git
在多线程中,Java 已经提供了很好原生锁(包括synchronized,lock),前面的其余文章中也已经讲到了内置锁和显示锁的理解和使用,在此再也不赘述。可是(是否是已经料到了我要说可是了呢?),在分布式系统中,由于要跨进程或者跨服务器 ,这种场景下JDK原生锁已经没法知足咱们的需求,须要一种可以分布式系统中保护共享资源的方式,分布式锁在这种状况下产生了。github
不少事情每每都是如此,为了解决一个问题,引入了新方案,而新方案却会带来其余的问题,又须要用更多的时间去解决新方案带来的问题。没有一个完美的方案,所以对方案的取舍,就是具体场景中应该重点关注哪些问题,忽略哪些问题的选择。redis
分布式锁是一个在分布式环境中很重要的原语,它代表不一样进程间采用互斥的方式操做共享资源。如何才称得上分布式锁呢?分布式锁须要知足三个基本的条件:json
外部存储
顾名思义,分布式锁是在分布式部署环境中给多个主机提供锁服务。Java具备天生的多线程优点,在同一个进程的线程中能够经过互斥锁住共享资源来保证多线程之间干扰,锁的载体是堆中共享变量,使用JDK原生锁synchronized和lock能够很方便的解决,可是将问题扩展到分布式环境中,就超出了JDK原生锁做用范畴。须要另外的存储载体,能够是共享内存或者磁盘文件。考虑到分布式锁的高可用性,避免单点问题,所以共享内存中数据是须要持久化的,这点内容会在下文中的分布式锁的高可用中涉及到。服务器
全局惟一标识
与JDK原生锁相似,分布式锁一样须要标记为全局惟一。在多线程环境中,锁可使一个对象引用,也能够是基本类型变量,都有惟一的标识来区分锁保护的不一样资源。仍然以上面的退款为例,为了保护用户的帐户资金,不容许同一个用户并发退款。所以同一个用户退款操做采用互斥锁保护起来,不一样用户之间不须要互斥操做。具体方法一种能够经过锁用户帐户的方式,另外一种对用户userId设置不一样的状态标识,这两种方式都是采用对堆中变量的原子操做保证互斥的。
分布式环境中上述第一种方法就不适用了,举个例子,小明的帐户能够同时在A、B两个不一样实例中加锁。那么能够采用第二种方法,自定义一个标识,使其全局惟一便可,每次申请退款时,首先尝试获取该标识,若是该标识已经被其余占用,则须要等待,直到释放该标识(是否是与synchronized很类似)。对于交易而言,全局惟一的标识很简单:业务+userId便可惟一标识。网络
至少有两种状态
锁至少须要两种状态:加锁(lock)和解锁(unlock)。用状态区分当前尝试获取的锁是否已经被其余操做占用,被占用只有等待锁释放后才能尝试获取锁并加锁,保护共享资源。多线程
为解决共享资源在分布式环境下并发访问带来的问题,引入分布式锁采用互斥访问的方式将并发访问串行化。下文中以Redis为例,分析使用分布式锁时重点须要考虑的状况。并发
获取锁操做的原子性
从读取锁的状态,到设置锁状态为加锁(获取锁的过程),不是原子性的操做,若是不能保证这两步做为一个的原子操做,可能存在竞态条件,在极端的时间差的状况下,会有多个服务同时获取到同一个锁,从而获取操做工做资源的凭证,这是不容许的。幸运的是Redis提供了CAS原子性功能SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置。
锁与保护共享资源的数据一致性
获取锁与开始操做共享资源必须保证一致性,结束操做共享资源和释放锁必须保证一致性。共享资源操做结束后必须释放锁,退出临界区,不然会形成锁饥饿;开始操做共享资源,必须是在获取锁以后,不然锁就没法保护共享资源。
分布式锁的性能
分布式锁须要考虑网络传输时间,超时时间一样须要考虑网络时间消耗。
可重入
某个请求试图得到一个已经由它本身持有的锁,那么这个请求就会成功,这是重入。当重入时须要将计数器加一,释放锁时,计数器相应减一,通常分布式锁一样支持可重入,所以须要设计标记不一样的请求。
公平锁和非公平锁
公平锁设定按照请求的顺序获取锁,不容许插队。公平是个好东西,不过大多数状况下非公平锁的性能要高于公平锁。
正常状况下,加锁,执行保护资源,释放锁。若是没有异常,那这世界就太美好了。那么生产环境中,使用分布式锁时应该注意哪些容错的问题。
锁没法释放
以退款为例,退款服务宕机,分布式锁服务正常。此时锁保护的资源(或部分)已没法对外提供服务,没法通知锁自身运行状况,为避免锁服务一直没法释放,能够为锁设置超时时间,当锁执行时间超过了超时时间,锁会过时,从而保证锁与保护服务的最终一致性。
固然锁设置超时时间又会引出另外一个问题:好比锁的超时时间是500ms,而部分退款服务可能因为网络等缘由执行时间为800ms(退款服务没有宕机,仅仅是执行时间相比平均执行时间较长而已),这种状况下,锁已通过期,而退款服务仍在执行,锁做为保护资源的功能失效了。有一个办法能够兼顾超时时间和锁失效的问题,退款服务保持心跳通知锁服务,锁服务收到心跳后延长锁的超时时间,不足在于即便退款服务已经宕机,锁服务仍然须要到达超时时间后才会解锁。Redisson分布式锁就是采用这种方式。
分布式锁时效设置的必要性:确保在将来的必定时间内,不管得到锁的节点发生了什么问题,最终锁都能被释放掉。
性能
针对访问量大的共享资源,尝试自旋方式获取锁时的长时间等待,即容易形成CPU空转性能的消耗,又容易形成节点阻塞;而每隔一段时间尝试获取锁,便没法保证资源的高效利用。基于以上两种解决方案的弊端,能够采用尝试获取锁必定次数后,加入到等待队列中,当锁释放后,通知等待队列中的下一个等待节点获取锁。既能够避免CPU空转带来的性能消耗,又能够及时响应,保证系统的性能和稳定性,避免毛刺的出现。设计上能够参考Java并发包中AQS。
锁饥饿
一个线程在尝试获取锁的过程当中一直没法获取锁,这种状况就是锁饥饿,好比体弱的狼很难在一群强壮的狼群中抢到食物,不少状况下锁饥饿是因为优先级较低形成的。发生锁饥饿时,没法获取锁便没法进行锁保护资源的操做。为避免锁饥饿状况的发生,设计时须要将锁设计成公平锁。
监控
监听锁的运行状况,掌握锁持有者的动态,若判断锁持有者处于不活动状态,要可以强制释放其持有的锁,引入第三方监控系统。
固然,分布式锁还有一些其余的问题:好比频繁获取锁释放锁带来的系统稳定和性能问题,如何保证锁的高可用,分布式锁的持久化,分布式锁单点问题,分布式锁网络传输性能等,还有分布式锁主节点宕机,从节点还没同步到锁,锁的惟一性被破坏,多个客户端能够得到同一个锁…
写做不易,痛并快乐着;理解可能存在误差,句句斟酌推敲;抵制抄袭,践行原创技术之路。若是本文能对您有所帮助,实为荣幸,我是葛一凡。
原文连接:http://geyifan.cn/2017/02/11/what-problems-when-using-distributed-locks/