[关闭]
@adamhand 2019-02-27T12:03:12.000000Z 字数 4155 阅读 852

分布式锁


什么是分布式锁

在单节点情况下,如果多个线程同时访问共享变量时,为了保证线程安全,需要对共享变量加锁。比如Java语言中的synchronized关键字和Lock锁。这样就解决了多线程并发问题

在集群模式情况下,可能两个线程不再属于同一台机器上的同一个进程,线程同步上升到进程同步,这样synchronized和Lock就不起作用了。为了对共享变量进行保护,防止多个进程同时操作共享变量,这样是为了解决多进程的并发问题。类似于单节点的锁,这种保护方式被称为分布式锁

分布式锁和分布式事务是两个不同的概念,它们解决的是两个问题。举个例子,一个商品的买卖过程中有两个这样的操作:

此时购物车的数据和库存的数据被存放在两个数据库中。

分布式锁解决的是这样的问题:多个进程同时修改购物车中商品的数量,只有一个进程能够成功,最后的结果是购物车中商品数量+1,不能多也不能少。

分布式事务解决的是这样的问题:购物车中商品数量和库存中商品数量联动改变,不能一个变了一个没变。

分布式锁的实现方式

常见的分布式锁有三种实现方式:

基于数据库的分布式锁

基于数据库实现分布式锁,可以有两种做法:

而实现悲观锁也有两种方法:使用数据库唯一索引的特性使用数据库的排他表锁

需要注意的是,基于数据库的锁并不是为了保证数据库访问的安全,而是为了利用数据库中的某些特性保证不同进程的安全。啥意思呢?比如MySQL数据库中有X锁和S锁,同时具有三级封锁协议和四种隔离界别,这些特性基本就能保证MySQL的安全。现在要做的是,A和B两个进程需要对一个共享的变量x进行修改,为了保证同一时刻只能有一个进程对x进行修改,就需要一把锁,竞争到锁的进程可以修改。而如何做这把锁呢?就可以利用MySQL的一些特性比如索引的唯一性来做这把锁,保证了锁的唯一性。

基于数据库的唯一索引特性

做法是这样的:获得锁时向表中插入一条记录,释放锁时删除这条记录。唯一索引可以保证同一条记录只被插入一次,那么就可以用这个记录是否存在来判断是否存于锁定状态。

例如,看下面这张表:

  1. CREATE TABLE `methodLock` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  3. `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',
  4. `desc` varchar(1024) NOT NULL DEFAULT '备注信息',
  5. `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
  6. PRIMARY KEY (`id`),
  7. UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
  8. )
  9. ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

上面的表中,通过UNIQUE KEY将method_name设置为唯一索引,那么在method_name这个索引上,同一条记录只能进行一次插入操作,它就相当于一把唯一的锁。

当要锁住某个方法时,可以在方法中执行以下SQL:

  1. insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)

其他方法检测到method_name字段不为空,说明处于加锁状态。当方法执行完毕之后,想要释放锁的话,需要执行以下sql:

  1. delete from methodLock where method_name ='method_name'

这种方法存在几个问题:

基于数据库的表锁

还可以借助数据库中自带的锁来实现分布式锁。还是利用上面创建的数据库表,基于MySQL的InnoDB引擎,可以使用以下方法来实现加锁操作:

  1. public boolean lock(){
  2. Connection.setAutoCommit(false);
  3. while (true) {
  4. try {
  5. result = select * from MethodLock where methodName = 'xxxx' for update;
  6. if (result != null) {
  7. return true; //表示获取到锁,返回true
  8. }
  9. } catch (Exception e) {
  10. }
  11. //没有获得锁,继续获取
  12. sleep(1000);
  13. }
  14. return false;
  15. }

在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁。

通过以下方法解锁:

  1. public void unlock(){
  2. connection.commit();
  3. }

这种方法在加锁失败时也不会报错,但是还是不可重入锁。

基于数据库版本号实现乐观锁

乐观锁机制其实就是在数据库表中引入一个版本号(version)字段来实现的。

当要从数据库中读取数据的时候,同时把这个version字段也读出来,如果要对读出来的数据进行更新后写回数据库,则需要将version加1,同时将新的数据与新的version更新到数据表中。但是必须必须在更新之前检查目前数据库里version值是不是之前的那个version,如果是,则正常更新。如果不是,则更新失败,说明在这个过程中有其它的进程去更新过数据了。有点类似于CAS算法。

基于缓存的分布式锁

主要介绍几种基于redis实现的分布式锁。

另外,Redisson 是一个基于Redis的分布式锁框架,可以直接用。Maven依赖如下:

  1. <dependency>
  2. <groupId>org.redisson</groupId>
  3. <artifactId>redisson</artifactId>
  4. <version>3.9.0</version>
  5. </dependency>

基于SETNX 指令

使用 SETNX(set if not exist)指令插入一个键值对,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。

SETNX 指令和数据库的唯一索引类似,保证了只存在一个 Key 的键值对,那么可以用一个 Key 的键值对是否存在来判断是否存于锁定状态。

EXPIRE 指令可以为一个键值对设置一个过期时间,从而避免了数据库唯一索引实现方式中释放锁失败的问题。

基于RedLock 算法

使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。

基于zookeeper的分布式锁

使用zookeeper的分布式锁最简单的方法是使用curator开源项目提供的基于zookeeper实现的分布式锁。

Maven依赖如下:

  1. <dependency>
  2. <groupId>org.apache.curator</groupId>
  3. <artifactId>curator-recipes</artifactId>
  4. <version>4.0.0</version>
  5. </dependency>

Zookeeper 抽象模型

Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点的父节点为 /app1。



节点类型

监听器

为一个节点注册监听器,在节点状态发生改变时,会给监听者发送消息。

分布式锁实现

会话超时

如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库的唯一索引实现的分布式锁释放锁失败问题。

羊群效应

一个节点未获得锁,只需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。

小结

几种分布式锁的优缺点:

数据库锁:

缓存锁:

zookeeper锁:

参考

分布式锁、事务和分布式事务概念汇总
浅谈分布式锁
还有人不懂分布式锁的实现就把这篇文章丢给他
基于Zookeeper的分布式锁
为什么分布式要有分布式锁!
再有人问你分布式锁是什么,就把这篇文章发给他
分布式锁的几种实现方式
基于数据库的分布式锁实现
分布式锁方式(一、基于数据库的分布式锁)
分布式锁1 Java常用技术方案

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注