基于数据库表作乐观锁,用于分布式锁。(version)
基于数据库表作悲观锁(InnoDB,for update)
基于数据库表数据记录作惟一约束(表中记录方法名称)
复制代码
使用redis的setnx()用于分布式锁。(setNx,直接设置值为当前时间+超时时间,保持操做原子性)
使用memcached的add()方法,用于分布式锁。
使用Tair的put()方法,用于分布式锁。
复制代码
每一个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个惟一的瞬时有序节点。
判断是否获取锁只须要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除便可。
复制代码
要实现分布式锁,最简单的方式可能就是直接建立一张锁表,而后经过操做该表中的数据来实现了。redis
当咱们要锁住某个方法或资源时,咱们就在该表中增长一条记录,想要释放锁的时候就删除这条记录。 建立这样一张数据库表:数据库
当咱们想要锁住某个方法时,执行如下SQL:缓存
由于咱们对method_name作了惟一性约束,这里若是有多个请求同时提交到数据库的话,数据库会保证只有一个操做能够成功(原子性),那么咱们就能够认为操做成功的那个线程得到了该方法的锁,能够执行方法体内容。bash
当方法执行完毕以后,想要释放锁的话,须要执行如下Sql: 服务器
上面这种简单的实现有如下几个问题:网络
一、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会致使业务系统不可用。session
二、这把锁没有失效时间,一旦解锁操做失败,就会致使锁记录一直在数据库中,其余线程没法再得到到锁。并发
三、这把锁只能是非阻塞的,由于数据的insert操做,一旦插入失败就会直接报错。没有得到锁的线程并不会进入排队队列,要想再次得到锁就要再次触发得到锁操做。分布式
四、这把锁是非重入的,同一个线程在没有释放锁以前没法再次得到该锁。由于数据中数据已经存在了。memcached
固然,咱们也能够有其余方式解决上面的问题。
除了能够经过增删操做数据表中的记录之外,其实还能够借助数据中自带的锁来实现分布式的锁。 咱们还用刚刚建立的那张数据库表。能够经过数据库的排他锁来实现分布式锁。 基于MySql的InnoDB引擎,可使用如下方法来实现加锁操做:
在查询语句后面增长for update,数据库会在查询过程当中给数据库表增长排他锁(这里再多提一句,InnoDB引擎在加锁的时候,只有经过索引进行检索的时候才会使用行级锁,不然会使用表级锁。这里咱们但愿使用行级锁,就要给method_name添加索引,值得注意的是,这个索引必定要建立成惟一索引,不然会出现多个重载方法之间没法同时被访问的问题。重载方法的话建议把参数类型也加上)
。
当某条记录被加上排他锁以后,其余线程没法再在该行记录上增长排他锁。 咱们能够认为得到排它锁的线程便可得到分布式锁,当获取到锁以后,能够执行方法的业务逻辑,执行完方法以后,再经过如下方法解锁:
public void lock(){
connection.setAutoCommit(false)
int count = 0;
while(count < 4){
try{
select * from lock where lock_name=xxx for update;
if(结果不为空){
//表明获取到锁
return;
}
}catch(Exception e){
}
//为空或者抛异常的话都表示没有获取到锁
sleep(1000);
count++;
}
throw new LockException();
}
复制代码
经过connection.commit()操做来释放锁。
这种方法能够有效的解决上面提到的没法释放锁和阻塞锁的问题。
可是仍是没法直接解决数据库单点和可重入问题。
这里还可能存在另一个问题,虽然咱们对method_name 使用了惟一索引,而且显示使用for update来使用行级锁。可是,MySql会对查询进行优化,即使在条件中使用了索引字段,可是否使用索引来检索数据是由 MySQL 经过判断不一样执行计划的代价来决定的,若是 MySQL 认为全表扫效率更高,好比对一些很小的表,它就不会使用索引,这种状况下 InnoDB 将使用表锁,而不是行锁。若是发生这种状况就悲剧了。。。
还有一个问题,就是咱们要使用排他锁来进行分布式锁的lock,那么一个排他锁长时间不提交,就会占用数据库链接。一旦相似的链接变得多了,就可能把数据库链接池撑爆
首先说明乐观锁的含义:
大多数是基于数据版本(VERSION)的记录机制实现的。何谓数据版本号?即为数据增长一个版本标识,
在基于数据库表的版本解决方案中,通常是经过为数据库表添加一个“VERSION”字段来实现读取出数据时
,将此版本号一同读出,以后更新时,对此版本号加1。
在更新过程当中,会对版本号进行比较,若是是一致的,没有发生改变,则会成功执行本次操做;
若是版本号不一致,则会更新失败。
复制代码
对乐观锁的含义有了必定的了解后,结合具体的例子,咱们来推演下咱们应该怎么处理:
假设咱们有一张资源表,以下图所示: T_RESOURCE , 其中有6个字段ID, RESOOURCE, STATE, ADD_TIME, UPDATE_TIME, VERSION,分别表示表主键、资源、分配状态(1未分配 2已分配)、资源建立时间、资源更新时间、资源数据版本号。
假设咱们如今咱们对ID=5780这条数据进行分配,那么非分布式场景的状况下,咱们通常先查询出来STATE=1(未分配)的数据,而后从其中选取一条数据能够经过如下语句进行,若是能够更新成功,那么就说明已经占用了这个资源 UPDATE T_RESOURCE SET STATE=2 WHERE STATE=1 AND ID=5780。
若是在分布式场景中,因为数据库的UPDATE操做是原子是原子的,其实上边这条语句理论上也没有问题,可是这条语句若是在典型的“ABA”状况下,咱们是没法感知的。有人可能会问什么是“ABA”问题呢?你们能够网上搜索一下,这里我说简单一点就是,若是在你第一次SELECT和第二次UPDATE过程当中,因为两次操做是非原子的,因此这过程当中,若是有一个线程,先是占用了资源(STATE=2),而后又释放了资源(STATE=1),实际上最后你执行UPDATE操做的时候,是没法知道这个资源发生过变化的。也许你会说这个在你说的场景中应该也还好吧,可是在实际的使用过程当中,好比银行帐户存款或者扣款的过程当中,这种状况是比较恐怖的。
那么若是使用乐观锁咱们如何解决上边的问题呢?
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语句真正更新影响到了一行数据,那就说明占位成功。若是没有更新影响到一行数据
,则说明这个资源已经被别人占位了。
复制代码
(1). 这种操做方式,使本来一次的UPDATE操做,必须变为2次操做: SELECT版本号一次;UPDATE一次。增长了数据库操做的次数。
(2). 若是业务场景中的一次业务流程中,多个资源都须要用保证数据一致性,那么若是所有使用基于数据库资源表的乐观锁,就要让每一个资源都有一张资源表,这个在实际使用场景中确定是没法知足的。并且这些都基于数据库操做,在高并发的要求下,对数据库链接的开销必定是没法忍受的。
(3). 乐观锁机制每每基于系统中的数据存储逻辑,所以可能会形成脏数据被更新到数据库中。在系统设计阶段,咱们应该充分考虑到这些状况出现的可能性,并进行相应调整,如将乐观锁策略在数据库存储过程当中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开。
讲了乐观锁的实现方式和缺点,是否是会以为不敢使用乐观锁了呢???固然不是,在文章开头我本身的业务场景中,场景1和场景2的一部分都使用了基于数据库资源表的乐观锁,已经很好的解决了线上问题。因此你们要根据的具体业务场景选择技术方案,并非随便找一个足够复杂、足够新潮的技术方案来解决业务问题就是好方案?!好比,若是在个人场景一中,我使用zookeeper作锁,能够这么作,可是真的有必要吗???答案以为是没有必要的!!!
总结一下使用数据库来实现分布式锁的方式,这两种方式都是依赖数据库的一张表,一种是经过表中的记录的存在状况肯定当前是否有锁存在,另一种是经过数据库的排他锁来实现分布式锁。
数据库实现分布式锁的优势
直接借助数据库,容易理解。
数据库实现分布式锁的缺点
会有各类各样的问题,在解决问题的过程当中会使整个方案变得愈来愈复杂。
操做数据库须要必定的开销,性能问题须要考虑。
使用数据库的行级锁并不必定靠谱,尤为是当咱们的锁表并不大的时候。
复制代码
使用redis的setnx()用于分布式锁。(原子性)
SETNX是将 key 的值设为 value,当且仅当 key 不存在。若给定的 key 已经存在,则 SETNX 不作任何动做。
• 返回1,说明该进程得到锁,SETNX将键 lock.id 的值设置为锁的超时时间,当前时间 +加上锁的有效时间。
• 返回0,说明其余进程已经得到了锁,进程不能进入临界区。进程能够在一个循环中不断地尝试 SETNX 操做,以得到锁。
复制代码
存在死锁的问题
SETNX实现分布式锁,可能会存在死锁的状况。与单机模式下的锁相比,分布式环境下不只须要保证进程可见,还须要考虑进程与锁之间的网络问题。某个线程获取了锁以后,断开了与Redis 的链接,锁没有及时释放,竞争该锁的其余线程都会hung,产生死锁的状况。因此在这种状况下须要对获取的锁进行超时时间设置,即setExpire,超时自动释放锁
基于zookeeper临时有序节点能够实现的分布式锁。
大体思想即为:
每一个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个惟一的临时有
序节点。 判断是否获取锁的方式很简单,只须要判断有序节点中序号最小的一个。
当释放锁的时候,只需将这个临时节点删除便可。同时,排队的节点须要监听排在本身以前的节点,这样能
在节点释放时候接收到回调通知,让其得到锁。zk的session由客户端管理,其能够避免服务宕机致使的锁无
法释放,而产生的死锁问题,不须要关注锁超时。
复制代码
来看下Zookeeper能不能解决前面提到的问题。
能够直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。
Curator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁,release方法用于释放锁。
使用ZK实现的分布式锁好像彻底符合了本文开头咱们对一个分布式锁的全部指望。可是,其实并非,Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并无缓存服务那么高
。由于每次在建立锁和释放锁的过程当中,都要动态建立、销毁瞬时节点来实现锁功能。ZK中建立和删除节点只能经过Leader服务器来执行,而后将数据同步到全部的Follower机器上。
其实,使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的状况,因为网络抖动,客户端到ZK集群的session链接断了,那么zk觉得客户端挂了,就会删除临时节点,这时候其余客户端就能够获取到分布式锁了。就可能产生并发问题。这个问题不常见是由于zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。屡次重试以后还不行的话才会删除临时节点。(因此,选择一个合适的重试策略也比较重要,要在锁的粒度和并发之间找一个平衡。)
使用Zookeeper实现分布式锁的优势
有效的解决单点问题,不可重入问题,非阻塞问题以及锁没法释放的问题。实现起来较为简单。
使用Zookeeper实现分布式锁的缺点
性能上不如使用缓存实现分布式锁。 须要对ZK的原理有所了解。
三种方案的比较
上面几种方式,哪一种方式都没法作到完美。就像CAP同样,在复杂性、可靠性、性能等方面没法同时知足,因此,根据不一样的应用场景选择最适合本身的才是王道。
从理解的难易程度角度(从低到高)
数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)
Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)
缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)
Zookeeper > 缓存 > 数据库