分布式锁的实现

1 分布式锁的疑问

谈到分布式锁,有不少实现方式,如数据库、redis、ZooKeeper等。提个问题:mysql

  • 实现分布式锁须要知足哪些条件呢?

2 数据库实现分布式锁

2.1 实现案例

如使用数据库事务中的锁如record lock来实现,以下所示redis

1 获取锁sql

public void lock(){
	connection.setAutoCommit(false)
	int count = 0;
	while(count < 4){
		try{
			select * from lock where lock_name=xxx for update;
			if(结果不为空){
				//表明获取到锁
				return;
			}
		}catch(Exception e){

		}
		//为空或者抛异常的话都表示没有获取到锁
		sleep(1000);
		count++;
	}
	throw new LockException();
}

2 释放锁数据库

public void release(){
	connection.commit();
}

数据库的lock表,lock_name是主键,经过for update操做,数据库就会对该行记录加上record lock,从而阻塞其余人对该记录的操做。服务器

一旦获取到了锁,就能够开始执行业务逻辑,最后经过connection.commit()操做来释放锁。微信

其余没有获取到锁的就会阻塞在上述select语句上,可能的结果有2种,在超时以前获取到了锁,在超时以前仍未获取到锁(这时候会抛出超时异常,而后进行重试)并发

数据库固然还有其余方式,如插入一个有惟一约束的数据。成功插入则表示获取到了锁,释放锁就是删除该记录。该方案也有不少问题要解决框架

2.2 存在的问题

首先性能不是特别高。异步

经过数据库的锁来实现多进程之间的互斥,可是这貌似也有一个问题:就是sql超时异常的问题分布式

jdbc超时具体有3种超时,具体见深刻理解JDBC的超时设置

  • 框架层的事务超时
  • jdbc的查询超时
  • Socket的读超时

这里只涉及到后2种的超时,jdbc的查询超时还好(mysql的jdbc驱动会向服务器发送kill query命令来取消查询),若是一旦出现Socket的读超时,对于若是是同步通讯的Socket链接来讲(底层实现Connection的多是同步通讯也多是异步通讯),该链接基本上不能使用了,须要关闭该链接,重新换用新的链接,由于会出现请求和响应错乱的状况,好比jedis出现的类型转换异常,详见Jedis的类型转换异常深究

3 redis实现分布式锁

而redis一般可使用setnx来实现分布式锁

3.1 基本版

1 获取锁

public void lock(){
	for(){
		ret = setnx lock_ley (current_time + lock_timeout)
		if(ret){
			//获取到了锁
			break;
		}
		//没有获取到锁
		sleep(100);
	}
}

2 释放锁

public void release(){
	del lock_ley
}

setnx来建立一个key,若是key不存在则建立成功返回1,若是key已经存在则返回0。依照上述来断定是否获取到了锁

获取到锁的执行业务逻辑,完毕后删除lock_key,来实现释放锁

其余未获取到锁的则进行不断重试,直到本身获取到了锁

3.2 改进版

上述逻辑在正常状况下是OK的,可是一旦获取到锁的客户端挂了,没有执行上述释放锁的操做,则其余客户端就没法获取到锁了,因此在这种状况下有2种方式来解决:

  • 为lock_key设置一个过时时间
  • 对lock_key的value进行判断是否过时

以第一种为例,在set键值的时候带上过时时间,即便挂了,也会在过时时间以后,其余客户端可以从新竞争获取锁

public void lock(){
	while(true){
		ret = set lock_key identify_value nx ex lock_timeout
		if(ret){
			//获取到了锁
			return;
		}
		sleep(100);
	}
}

public void release(){
	value = get lock_key
	if(identify_value == value){
		del lock_key
	}
}

以第二种为例,一旦发现lock_key的值已经小于当前时间了,说明该key过时了,而后对该key进行getset设置,一旦getset返回值是原来的过时值,说明当前客户端是第一个来操做的,表明获取到了锁,一旦getset返回值不是原来过时时间则说明前面已经有人修改了,则表明没有获取到锁,详细见用Redis实现分布式锁,改正以下:

# get lock
lock = 0
while lock != 1:
    timestamp = current_unix_time + lock_timeout
    lock = SETNX lock.foo timestamp
    if lock == 1 or (now() > (GET lock.foo) and now() > (GETSET lock.foo timestamp)):
        break;
    else:
        sleep(10ms)
 
# do your job
do_job()
 
# release
if now() < GET lock.foo:
    DEL lock.foo

这里看来第二种其实没有第一种比较好。

3.3 问题依旧

问题1: lock timeout的存在也使得失去了锁的意义,即存在并发的现象。一旦出现锁的租约时间,就意味着获取到锁的客户端必须在租约以内执行完毕业务逻辑,一旦业务逻辑执行时间过长,租约到期,就会引起并发问题。因此有lock timeout的可靠性并非那么的高。

问题2: 上述方式仅仅是redis单机状况下,还存在redis单点故障的问题。若是为了解决单点故障而使用redis的sentinel或者cluster方案,则更加复杂,引入的问题更多。

4 ZooKeeper实现分布式锁

4.1 案例

这也是ZooKeeper客户端curator的分布式锁实现。

1 获取锁

public void lock(){
	path = 在父节点下建立临时顺序节点
	while(true){
		children = 获取父节点的全部节点
		if(path是children中的最小的){
			表明获取了节点
			return;
		}else{
			添加监控前一个节点是否存在的watcher
			wait();
		}
	}
}

watcher中的内容{
	notifyAll();
}

2 释放锁

public void release(){
	删除上述建立的节点
}

4.2 总结

ZooKeeper版本的分布式锁问题相对比较来讲少。

  • 锁的占用时间限制:redis就有占用时间限制,而ZooKeeper则没有,最主要的缘由是redis目前没有办法知道已经获取锁的客户端的状态,是已经挂了呢仍是正在执行耗时较长的业务逻辑。而ZooKeeper经过临时节点就能清晰知道,若是临时节点存在说明还在执行业务逻辑,若是临时节点不存在说明已经执行完毕释放锁或者是挂了。由此看来redis若是能像ZooKeeper同样添加一些与客户端绑定的临时键,也是一大好事。

  • 是否单点故障:redis自己有不少中玩法,如客户端一致性hash,服务器端sentinel方案或者cluster方案,很难作到一种分布式锁方式能应对全部这些方案。而ZooKeeper只有一种玩法,多台机器的节点数据是一致的,没有redis的那么多的麻烦因素要考虑。

整体上来讲ZooKeeper实现分布式锁更加的简单,可靠性更高。

5 分布式锁实现原理总结

从上面咱们经历了3种实现方式,能够从中总结下,该怎么去回答最初提出的问题。

5.1 分布式锁的实现

在我本身看来有以下3个方面:

  • 怎么获取锁
  • 怎么释放锁
  • 怎么得知锁被释放了

5.1.1 怎么获取锁

可以提供一种方式,多个客户端并发操做,只能有一个客户端能知足相应的要求

如数据库的for update的sql语句、或者插入一个含有惟一约束的数据等

如redis的setnx等

如ZooKeeper的求最小节点的方式

这些均可以保证只能有一个客户端获取到了锁

5.1.2 怎么释放锁

场景通常有2种状况:

  • 1 正常状况下的释放锁

  • 2 异常状况下如何释放锁(即释放锁的操做没有被执行,如挂掉、没执行成功等缘由)

如redis正常状况下释放锁是删除lock_key,异常状况下,只能经过lock_key的超时时间了

如ZooKeeper正常状况下释放锁是删除临时节点,异常状况下,服务器也会主动删除临时节点(这种机制就简单多了)

5.1.3 怎么得知锁被释放了

实现方式通常有2种状况:

  • 1 没有获取到锁的客户端不断尝试获取锁
  • 2 服务器端通知客户端锁被释放了

固然第二种状况是最优的(客户端所作的无用功最少),如ZooKeeper经过注册watcher来获得锁释放的通知。而数据库、redis没有办法来通知客户端锁释放了,那客户端就只能傻傻的不断尝试获取锁了。

欢迎来拍砖,相互讨论,我相信会越辩越清晰。

欢迎关注微信公众号:乒乓狂魔

微信公众号

相关文章
相关标签/搜索