分布式锁的原理及几种实现方式

分布式锁

普通进程锁的调用者只在该进程中(或该进程的线程中),所以较为容易进行资源使用协调。在分布式环境中,不一样机器的不一样进程会对同一个资源进行使用/争夺,那么如何对资源的使用进行协调呢?这时就须要分布式锁来进行进程间的协调,以实现同一时刻只能有一个进程占有该资源。html

分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,经常须要协调他们的动做。若是不一样的系统或是同一个系统的不一样主机之间共享了一个或一组资源,那么访问这些资源的时候,每每须要互斥来防止彼此干扰来保证一致性,在这种状况下,便须要使用到分布式锁。——《维基百科》git

分布式锁的特色

  • 原子性:同一时刻,只能有一个机器的一个线程获得锁;
  • 可重入性:同一对象(如线程、类)能够重复、递归调用该锁而不发生死锁;
  • 可阻塞:在没有得到锁以前,只能阻塞等待直至得到锁;
  • 高可用:哪怕发生程序故障、机器损坏,锁仍然可以获得被获取、被释放;
  • 高性能:获取、释放锁的操做消耗小。

上述特色和要求,根据业务需求、场景不一样而有所取舍。github

下面介绍几种基于经常使用数据库/缓存实现的分布式锁。redis

数据库

数据库实现分布式锁通常有两种方式:使用惟一索引或者主键;使用数据库自带的锁进行。数据库

惟一索引或者主键

表定义例子:缓存

create table distributed_lock(
    id int not null auto_increment primay key,
    method varchar(255) not null defult '' comment '方法名,同一时刻,该方法只能有一个调用者',
    expire timestamp not null default current_timestamp()+60 comment '过时时间,过时后要被删除',
    unique key(method)
);
  • method 惟一索引,表示须要互斥调用的方法
  • expire 用于标记锁的最长持有时间。须要按期检查expire,将大于now的记录删除,防止调用者长时间不释放锁(如调用者意外退出而没有释放锁)

加锁操做:session

insert into distributed_lock(method, expire) values("the name of your method", current_timestamp+30s);

解锁操做并发

delete from distributed_lock where method="the name of your method"
// or
delete from distributed_lock where id=$id

上述这种基于惟一索引或者主键的实现机制特色以下:分布式

  • 易于理解和使用、调试;
  • 数据库在单点状况下,若是宕机会失去全部的锁信息;(能够经过主备数据库形式提升可用性)
  • 非重入;(能够给数据库增长一个字段(如belong_to)解决,获取锁的时候当发现调用者已经获取获得锁就直返回成功)
  • 不是非阻塞锁,即尝试获取可是失败会直接返回错误。(能够经过阻塞轮询来解决)
  • 性能:传统数据库在并发量高的时候若是解决该问题(如淘宝的下单场景,一致性哈希?)?思考中

基于数据库实现锁,还用另外一种方式:排他锁。此处暂不介绍。性能

内存数据库Redis

方式一

setnx: 当锁不存在的时候则加锁成功,不然返回false,获取锁失败。为防止redis故障,能够增长expire来设置锁的最大持有时间。防止调用者长时间不释放锁(如调用者意外退出而没有释放锁)。

setnx method user // user为调用者,是为了锁可重入
expire method 30

这种方法的不足之处是没法保证setnx和expire的原子。想象一下,若是setnx成功以后,设置expire以前,调用者因为意外退出而没法释放锁,就会形成锁没法被正确释放,形成死锁现象。为此,能够以下命令代替:

setex method 30

setex是redis2.6提供的功能。解锁操做判断锁是否超时,若是没超时删除锁,若是已超时,不用处理(防止删除其余线程的锁)。

方法二

  1. 线程/进程A setnx,key为method,值为超时的时间戳(t1),若是返回true,得到锁。 // 这一步能够理解为锁的初始化?
  2. 线程B用get命令获取t1,与当前时间戳比较,判断是否超时,没超时false,若是已超时执行步骤3
  3. 计算新的超时时间t2,使用"getset method t2"命令返回t3(这个值可能其余线程已经修改过),若是t1==t3,得到锁,若是t1!=t3说明锁被其余线程获取了
  4. 获取锁后,处理完业务逻辑,再去判断锁是否超时,若是没超时删除锁,若是已超时,不用处理(防止删除其余线程的锁)

可见这种方法不可重入,可是针对一些无需可重入的场景这种实现方法也是可行的。

方法三

为了提升可用性,redis的做者提倡使用五个甚至更多的redis节点,使用上述方法的一种来获取/释放锁,当成功获取到三个或者三个以上节点的锁,则认为成功持有锁。因为必须获取到5个节点中的3个以上,因此可能出现获取锁冲突,即你们都得到了1-2把锁,结果谁也不能获取到锁,针对这种状况能够随机等待一段时间后再从新尝试得到锁。

zookeeper

zookeeper的内部结构相似于一个文件系统,同一目录下的文件不能同名,便可以保证建立文件(也称节点)是一个原子性的操做。

zookeeper数据模型:

  • 永久节点:节点建立后,不会由于会话失效而消失
  • 临时节点:与永久节点相反,若是客户端链接失效,则当即删除节点。(能够做为超时控制)
  • 顺序节点:与上述两个节点特性相似,若是指定建立这类节点时,zk会自动在节点名后加一个数字后缀,而且是递增。
  • 监视器(watcher):当建立一个节点时,能够注册一个该节点的监视器,当节点状态发生改变时,watch被触发时,ZooKeeper将会向客户端发送且仅发送一条通知,由于watch只能被触发一次。

根据zookeeper的这些特性,咱们来看看如何利用这些特性来实现分布式锁,先建立一个锁目录lock。

获取锁:

  • 在lock目录创建一个新节点,类型为临时顺序节点,节点名为method_xx,xx为节点的序号。
  • 查看lock目录下有没有比xx更小的节点,若是没有,则获取节点成功并返回,不然,使用监听器监听lock目录下序号次小于xx的节点的变动。
  • 当接收到lock目录的变动通知,若是收到变动通知,则获取成功。

释放锁:

  • 删除节点method_xx

etcd

etcd是一个开源的、分布式的键值对数据存储系统,提供共享配置、服务的注册和发现。etcd与zookeeper相比算是轻量级系统,二者的一致性协议也同样,因为etcd设计之初就针对服务注册,也有事物机制,所以基于etcd的分布式锁更为简单。

etcd 特性:

  • etcd v3引入了lease(租约)的概念,concurrency包基于lease封装了session,每个客户端都有本身的lease,也就是说每一个客户端都有一个惟一的64位整形值;lease能够设置过时时间。
  • etcdv3新引入的多键条件事务,替代了v2中Compare-And-put操做。etcdv3的多键条件事务的语意相似于C语言的三目运算符:condition?action_1:action_2
  • 每次对etcd存储进行改动都会分配一个这个序号,在v2中叫index,createRevision是表示这个key建立时被分配的这个序号。当key不存在时,createRivision是0。相似于zookeeper的节点序号。
// 比较的是key的createRevision
cmp := v3.Compare(v3.CreateRevision(method), "=", 0)
// 存入一个key
put := v3.OpPut(method, "", v3.WithLease(lease_id))
// 读取这个key
get := v3.OpGet(method)
// 若是revision为0,则存入,不然获取
resp, err := client.Txn(ctx).If(cmp).Then(put).Else(get).Commit()
if err != nil {
    return err
}
// 本次操做的revision
myRev = resp.Header.Revision
// 操做失败,则获取else返回的值,即已有的revision
if !resp.Succeeded {
    myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision
}
ownerKey := resp.Responses[1].GetResponseRange().Kvs
    if len(ownerKey) == 0 || ownerKey[0].CreateRevision == myRev {
        成功获取锁
}
err = waitDeletes(ctx, client, m.pfx, myRev-1)
if err!=nil{
  失败
}
成功

上述代码来自github.com/etcd-io/etcd

获取锁:

  • 基于lease去获取锁,key为method,获取成功则直接返回
  • 若是获取失败,则监听key的变化,监听到key被删除后,从新尝试获取锁。

释放锁:

  • 处理完业务逻辑,删除锁

Ref

相关文章
相关标签/搜索