摘要:在平常开发中,应用大多数是分布式部署的,常常会面临分布式环境下应用对数据操做的一致性问题。这时就须要找出一个在分布式环境下同一个应用多个实例之间可以访问的临界资源,并对该临界资源作互斥访问,从而保证数据一致性。本文结合笔者实际工做中的经验,对分布式环境下实现应用分布式锁的关键思路进行探讨。redis
关键词: 分布式锁、互斥资源、数据一致性 分布式环境下,分布式部署的应用不少时候须要对同一个资源数据进行操做以知足业务须要,这里就会面临数据一致性的问题。 例如商品售卖,同一时间可能会有多个线程请求库存扣减,若是不对库存扣减进行互斥访问,将会致使商品超卖,这是不可容忍的。所以,利用分布式锁对库存扣减进行互斥访问,能够解决库存数据的一致性问题。 本文介绍利用数据库,缓存,zookeepr三种资源实现分布式锁的关键思路。sql
利用一个分布式环境下,多实例可以访问的公共资源来实现分布式锁,具体这个公共资源能够是数据库,缓存(redis/memcache),zookeeper等等。 这里须要明确一点,以JDK为例,在实现分布式锁时可能会考虑使用ReentrantLock,但其实是有问题的,ReentrantLock是同一个JVM上的锁,lock和unlock都须要在同一个线程上进行,而分布式环境下,是多JVM的,并且lock和unlock极有多是两次不一样的甚至不相关的请求,所以确定不是同一个线程,从而没法使用ReentrantLock来实现分布式锁。数据库
目前主流的数据库引擎,都支持select for update的语法,这是一种排他锁,当两个不一样事务都执行到select xxx where id=? for update时,假设id上有惟一索引,那么后面执行的事务将会阻塞在该sql语句上,直到前一个事务提交或者回滚。所以能够利用这种特性来实现分布式锁。缓存
2.1流程图架构
图 1 数据库分布式锁关键流程 (1) 设置数据库链接为事务不自动提交。 (2) 执行select * for lock where lock_name=xxx for update语句,判断结果集,若是结果集有记录,表示某一个事务已经得到到了锁资源,则转向步骤(3),不然结束。 (3) 进行业务操做,转向步骤(4)。 (4) 释放锁:事务提交。并发
2.2 伪代码 2.2.1 获取锁框架
上述为获取锁的具体实现,关键是分布式
select * for lock where lock_name=xxx for update性能
语句,同一时间只有一个事务可以顺利执行该sql获得返回值,其他事务将在该sql语句上阻塞。 当sql语句的返回的结果集不为空时,表示获取到锁,不然结果集为空或者抛异常都视为没有获取到锁。操作系统
2.2.2 释放锁!
释放说的方式相对简单,只须要简单调用commit方法就能够。
2.3 锁表的设计 上述伪代码lock表的定义根据场景的不用而有所不一样。 这里举个例子,lock表能够设计成以下(以MySQL为例子):
注意到lock_name字段建了惟一索引,由于对于select for update语句,若是where条件的字段没有惟一索引,那么for update将会失效,这点须要特别注意。
分布式缓存,例如redis,memcache等都会提供一些命令,这些命令都有一个共同的特征:原子性。 例如:redis的setnx命令,memcache的add命令等等。这里以redis为例,实际场景中,通常是setnx,get,getset三个命令结合使用来实现分布式锁。 setnx命令的含义是set if not exists,参数有两个:key,value。该命令是原子的,若是key不存在,则设置当前key为value成功,返回1;若是key已经存在,则设置当前key为value失败,返回0。 get命令的含义是获取指定key的值。 getset命令的含义是设置指定key一个新的值,而且返回旧的值,该命令也是原子的。
3.1 流程图
图 2 redis缓存分布式锁关键流程 (1)调用setnx:flag=setnx(lockkey,当前时间+过时超时时间),判断flag的返回值,若是flag=1表明获取锁成功,则转向步骤(4)。若是flag=0表明没有获取到锁,则转向步骤(2)
(2)获取lockkey的值:oldExpireTime=get(lockkey)。若是当前系统时间<= oldExpireTime,表示未超时,仍然有另外的线程持有锁,所以转向步骤(1)继续尝试抢锁;若是前系统时间> oldExpireTime,表示已经超时,转向步骤(3)
(3)计算新的时间值:newExpireTime=当前时间+过时超时时间,调用getset命令,即currentExpireTime=getset(lockkey, newExpireTime),若是这时newExpireTime不等于currentExpireTime,表明已经有另外的线程在当前线程调用getset以前调用了setnx,而且返回值是1,所以当前线程抢锁失败,转向步骤(1)继续尝试抢锁。若是newExpireTime等于currentExpireTime,表示当前线程抢锁成功,则转向步骤(4)。
(4)具体业务操做。业务操做完成以后,比较业务处理时间和锁的超时时间。若是业务处理时间>=锁超时时间,表示锁已经被redis的超时机制删除了,则转向步骤(6)。若是业务处理时间<锁超时时间,则转向步骤(5)。
(5)调用del命令删除锁:del(lockkey)。转向步骤(6)。
(6)其余业务操做。
3.2 伪代码 3.2.1 获取锁
3.2.2 释放锁
Zookeeper是一个分布式的应用程序协调服务,它包含一些简单的原语集,分布式应用能够基于它实现诸如同步服务,配置管理,命名服务等。 Zookeeper的管理基于层级命名空间,相似操做系统的目录结构,每一个目录节点能够关联数据。其中有一种很是特殊的节点:临时节点,同一时间只有一个会话能够建立节点成功,当会话结束节点会被自动删除。所以能够利用该特性来实现分布式锁。
4.1 流程图
图 3 zookeeper缓存分布式锁关键流程 (1) 建立链接zookeeper的会话。 (2) 尝试建立临时节点:/exclusiveLock/lock,若是建立成功,则转向步骤(3),不然继续进行循环,尝试下一次建立节点。 (3) 进行业务操做,转向步骤(4)。 (4) 释放锁:会话结束或者删除临时节点。
4.2 伪代码 4.2.1 获取锁 经过构建一个目录,当叶子节点能建立成功,则认为获取到锁,由于一旦一个节点被某个会话建立,其它会话再次建立创这个节点时,将会抛出异常。 好比目录为:
图 4 zookeeper临时节点结构
4.2.2 释放锁 删除节点或者会话失效
本文阐述了分布式环境下实现分布式锁的关键技术,经过基于数据库的分布式锁,基于缓存的分布式锁,基于zookeeper的分布式锁三种经常使用的分布式锁实现技术进行原理说明,对关键代码进行了流程说明和伪代码说明。 这三种方案基本能够解决平常工做中不少业务场景下的分布式锁问题,从而解决数据一致性问题。
固然,三种方案都有各自的优缺点: (1)基于数据库的分布式锁:在并发量很高的状况下,系统会有不少个分布式锁资源,对数据库性能有必定影响,特别是在分布式锁表和业务表在同一个数据库时性能降低尤其明显,这时能够对分布式锁表进行分库分表来下降压力,提供性能。总的来讲,基于数据库分布式锁的方案,只适用于并发量不大的场景下使用。 (2)基于缓存的分布式锁:会存在单点问题,若是master节点宕机了,那么分布式锁就无效了,从而致使数据一致性问题。而假如redis是master-slave架构,那么会有以下状况出现:请求A在master节点上拿到了锁,master节点把请求A建立的锁信息写入到slave节点以前就宕机了,slave节点变成master节点以后,这时请求B有可能会拿到跟请求A相同的锁,由于slave节点尚未请求A的锁信息。 (3)基于zookeeper的分布式锁:zookeeper的优势是高可用,公平锁,心跳保持锁,顺序节点,临时节点,可以支撑大并发。同事zookeeper有成熟的客户端框架Curator,该框架封装了与zookeeper通讯的细节,实现起分布式锁更加简单。总上所述:没有一种一劳永逸的分布式锁解决方案,只有适用某种场景下的具体实现方案,在真实环境下须要具体问题具体分析。 另外,同时也须要考虑以下问题: (1)如何避免死锁的出现,一旦出现死锁,对应用的影响是致命的。(2)怎么释放锁。(3)怎么知道锁释放了。(4)锁超时处理。