普通进程锁的调用者只在该进程中(或该进程的线程中),所以较为容易进行资源使用协调。在分布式环境中,不一样机器的不一样进程会对同一个资源进行使用/争夺,那么如何对资源的使用进行协调呢?这时就须要分布式锁来进行进程间的协调,以实现同一时刻只能有一个进程占有该资源。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) );
加锁操做: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
上述这种基于惟一索引或者主键的实现机制特色以下:分布式
基于数据库实现锁,还用另外一种方式:排他锁。此处暂不介绍。性能
setnx: 当锁不存在的时候则加锁成功,不然返回false,获取锁失败。为防止redis故障,能够增长expire来设置锁的最大持有时间。防止调用者长时间不释放锁(如调用者意外退出而没有释放锁)。
setnx method user // user为调用者,是为了锁可重入 expire method 30
这种方法的不足之处是没法保证setnx和expire的原子。想象一下,若是setnx成功以后,设置expire以前,调用者因为意外退出而没法释放锁,就会形成锁没法被正确释放,形成死锁现象。为此,能够以下命令代替:
setex method 30
setex是redis2.6提供的功能。解锁操做判断锁是否超时,若是没超时删除锁,若是已超时,不用处理(防止删除其余线程的锁)。
可见这种方法不可重入,可是针对一些无需可重入的场景这种实现方法也是可行的。
为了提升可用性,redis的做者提倡使用五个甚至更多的redis节点,使用上述方法的一种来获取/释放锁,当成功获取到三个或者三个以上节点的锁,则认为成功持有锁。因为必须获取到5个节点中的3个以上,因此可能出现获取锁冲突,即你们都得到了1-2把锁,结果谁也不能获取到锁,针对这种状况能够随机等待一段时间后再从新尝试得到锁。
zookeeper的内部结构相似于一个文件系统,同一目录下的文件不能同名,便可以保证建立文件(也称节点)是一个原子性的操做。
zookeeper数据模型:
根据zookeeper的这些特性,咱们来看看如何利用这些特性来实现分布式锁,先建立一个锁目录lock。
获取锁:
释放锁:
etcd是一个开源的、分布式的键值对数据存储系统,提供共享配置、服务的注册和发现。etcd与zookeeper相比算是轻量级系统,二者的一致性协议也同样,因为etcd设计之初就针对服务注册,也有事物机制,所以基于etcd的分布式锁更为简单。
etcd 特性:
// 比较的是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
获取锁:
释放锁: