谈谈分布式锁的解决办法

Posted 客技院

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了谈谈分布式锁的解决办法相关的知识,希望对你有一定的参考价值。

谈谈分布式锁的解决办法


目前,分布式的网站或应用越来越受到很多大型网站及应用的青睐,而数据一致性问题在分布式场景中一直都是比较重要的话题。在很多场景中,我们为了保证数据的最终一致性,需要很多技术方案来支持,比如分布式事务、分布式锁等。当某个资源在多系统之间,具有共享性的时候,为了保证大家访问这个资源数据是一致的,那么就必须要求在同一时刻只能被一个客户端处理,不能并发的执行,否者就会出现同一时刻有人写,有人读,大家访问到的数据就不一致了。


一、为什么要用分布式锁

在单机时代,Java中其实提供了很多并发处理相关的API,如果有多个线程要同时访问某个共享资源的时候,我们可以采用线程间加锁的机制,即当某个线程获取到这个资源后,就立即对这个资源进行加锁,当使用完资源之后,再解锁,其它线程就可以接着使用了。例如,在Java中,甚至专门提供了一些处理锁机制的一些API(synchronize/Lock等)。

但是到了分布式系统的时代,这种线程之间的锁机制就没有作用了,系统可能会有多份并且部署在不同的机器上,这些资源已经不是在线程之间共享了,而是属于进程之间共享的资源,也就是说单纯的Java API并不能提供分布式锁的能力。因此,为了解决这个问题,我们就必须引入分布式锁的概念。


二、分布式锁的条件

  • 可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。

  • 这把锁是一把可重入锁(避免死锁)

  • 这把锁是一把阻塞锁(根据业务需求)

  • 有高可用的获取锁和释放锁功能

  • 获取锁和释放锁的性能要



三、分布式锁的实现方式

目前主流的有三种,从实现的复杂度上来看,从上往下难度依次增加:

  • 基于数据库实现

  • 基于Redis实现

  • 基于ZooKeeper实现



1. 基于数据库实现数据库锁

基于数据库实现分布式锁分为两种:乐观锁和悲观锁。

乐观锁机制其实就是在数据库表中引入一个唯一标识字段来实现的,这个字段具有递增特性,例如时间戳或者版本id。当我们要从数据库中读取数据的时候,同时把这个以为标识的字段也读出来,更新数据的同时也更新这个唯一标识字段,再写回数据库并更新,此时,其他线程读取数据库后,先检查数据库里的唯一标识字段的值和读取出来的值是不是一致,如果是,则正常更新,如果不是,则更新失败,说明在这个过程中有其它的进程去更新过数据了。

举个生活中例子:

                           

图解:

(1)甲和乙有一个共同的账户,两个人同时到ATM取钱,且账户上余额为2000元

(2)甲取1500元,乙取1000元

(3)甲操作ATM机,从数据库中取出数据为account=2000和verson_id=1

(4)乙操作ATM机,从数据库中取出数据为account=2000和version_id=1

(5)甲动作快一点,先比对取出的version_id和数据库中的version_id值是否一致,发现均为1,于是修改数据为account=500和version_id=2,并执行操作和事务提交,数据库中version_id值改为2

(6)乙此时也将数据修改为account=1000,并对比取出version_id和数据库的version_id,此时却发现取出的是version_id=1,而数据库中的version_id=2,提交执行失败,操作回滚。

从上面的例子可以看出,满足乐观锁,必须要具备具备以下条件:

  • 必须有一个具备自增条件的标识字段

  • 在操作数据库之前,必须要对比读取的值和数据库的值是否一致

悲观锁(排它锁)是借助数据库中自带的锁来实现分布式的锁。在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。代码实现如下:

  
    
    
  
  1. public boolean lock(){

  2.   connection.setAutoCommit(false);

  3.   while(true){

  4.       result = select * from user where id = 100 for update;

  5.       if(result){

  6.             //结果不为空,则说明获取到了锁

  7.            return true;

  8.       }

  9.       //没有获取到锁,继续获取

  10.       sleep(1000);

  11.   }

  12.   return false;

  13. }


上面的示例中,user表中,id是主键,通过 forupdate 操作,数据库在查询的时候就会给这条记录加上锁。当这条记录加上排它锁之后,其它线程是无法操作这条记录的。

如果要释放这个锁,可以使用如下代码:

  
    
    
  
  1. public void unlock(){

  2.     connection.commit();

  3. }


2. 基于缓存实现Redis锁

相比较于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点。而且很多缓存是可以集群部署的,可以解决单点问题。而Redis锁正是依赖其本身的原子性进行加锁操作。

那么Redis锁的实现原理是什么呢?

Redis提供了一个setnx()方法,这个方法是只有在某个key不存在的时候,才会执行成功。那么当多个进程同时并发的去设置同一个key的时候,就永远只会有一个进程成功。该方法返回的是一个long型结果,1为成功,0为失败,因此,可以通过判断result确定写入成败。

当某个进程设置成功之后,就可以去执行业务逻辑了,等业务逻辑执行完毕之后,再去进行解锁。解锁很简单,只需要删除(delete)这个key就可以了,不过删除之前需要判断,这个key对应的value是当初自己设置的那个。

当然Redis也提供了自动失效时间方法expire,通过给key设置一个超时时间来自动释放锁,避免死锁。

3.基于zookeeper实现zookeeper锁

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。

基于ZooKeeper实现分布式锁的步骤如下:

(1)创建一个目录mylock;

(2)线程A想获取锁就在mylock目录下创建临时顺序节点;

(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;

(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;

(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。


以上就是三种锁机制,欢迎各位交流。





以上是关于谈谈分布式锁的解决办法的主要内容,如果未能解决你的问题,请参考以下文章

分布式锁三种解决方案

谈谈Raft

Redis实现分布式锁(设计模式应用实战)

Redis实现分布式锁(设计模式应用实战)

redis分布式锁的问题和解决

Redis进阶学习03---Redis完成秒杀和Redis分布式锁的应用