分布式应用中的分布式锁
如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要通过一些互斥手段来防止彼此之间的干扰,以保证一致性,在这种情况下,就需要使用分布式锁,分布式锁就是指在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问。
分布式锁的要求:
1. 互斥性–在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2. 高性能的获取锁与释放锁;
3. 具备可重入特性–当一个客户端获取锁之后,这个客户端可以再次获取锁;
一般有三种实现方案:
1. 数据库(乐观锁/悲观锁);
2. Redis缓存;
3. Zookeeper
一下简要分析各个方案的实现思路:
1. 基于数据库的分布式锁
1.1 乐观锁机制
乐观锁机制其实就是在数据库表中引入一个版本号(version)字段来实现的。
当我们要从数据库中读取数据的时候,同时把这个version字段也读出来,如果要对读出来的数据进行更新后写回数据库,则需要将version加1,同时将新的数据与新的version更新到数据表中,且必须在更新的时候同时检查目前数据库里version值是不是之前的那个version,如果是,则正常更新。如果不是,则更新失败,说明在这个过程中有其它的进程去更新过数据了。
1.2 悲观锁机制
在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁 (需要注意的是,在InnoDB中只有字段加了索引的,才会是行级锁,否者是表级锁,所以就要给要执行的方法字段名添加索引),当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。我们可以认为获得排他锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后释放锁。
2. 基于 ZooKeeper 做分布式锁
ZooKeeper 锁相关基础知识
zk 一般由多个节点构成(单数),采用 zab 一致性协议。因此可以将 zk 看成一个单点结构,对其修改数据其内部自动将所有节点数据进行修改而后才提供查询服务。
zk 的数据以目录树的形式,每个目录称为 znode, znode 中可存储数据(一般不超过 1M),还可以在其中增加子节点。
子节点有三种类型。序列化节点,每在该节点下增加一个节点自动在该节点的名称上自增。临时节点,一旦创建这个 znode 的客户端与服务器失去联系,这个 znode 也将自动删除。最后就是普通节点。
Watch 机制,client 可以监控每个节点的变化,当产生变化会给 client 产生一个事件。
原理:利用临时节点与 watch 机制。每个锁占用一个普通节点 /lock,当需要获取锁时在 /lock 目录下创建一个临时节点,创建成功则表示获取锁成功,失败则 watch/lock 节点,有删除操作后再去争锁。临时节点好处在于当进程挂掉后能自动上锁的节点自动删除即取消锁。
3. 基于Redis分布式锁
基于setnx()、EXPIRE() 方法做分布式锁
setnx 的含义就是 SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果 key 不存在,则设置当前 key 成功,返回 1;如果当前 key 已经存在,则设置当前 key 失败,返回 0。
expire 设置过期时间,要注意的是 setnx 命令不能设置 key 的超时时间,只能通过 expire() 来对 key 设置。