1.概述
现在大部分公司的服务通过分布式部署实现了高可用,但是问题来了:如果有一段逻辑只希望一个应用中的一个线程进行执行,如何能够保证用户的一次操作,同时只受理一次呢?以前单机应用,可以通过java的synchronize和Lock来实现。现在则需要使用分布式锁。
分布式锁的实现主要有redis、zookeeper、db等方式。本文主要说明用redis实现方式。
2.方案1
因为redis是单线程处理的,并且支持cas操作。不难想到的一个实现是:
lock:
setnx key value
unlock:
del key
其中,setnx语义为:若给定的 key 已经存在,则 SETNX 不做任何动作。 成功设置返回1、设置失败返回0。
根据setnx的返回值判断是否加锁成功。
问题:
如果一个服务lock完之后,崩溃了或者重启了,那么将永远不会unlock,对于需要继续执行的场景来说,是无法接受的。
3.方案2
针对方案2的一些问题,不难想到对key设置一个超时就ok了。那么方案2来了:
lock:
set key value px timeout_millisecond nx
unlock:
del key
其中set方法支持同时设置超时和nx,这种方案是否ok?
问题:
如果最早拿到锁的服务器1执行结束,调用unlock失败(实际redis删除成功),另外一个服务器2加锁成功。某种补偿导致服务器1重复调用unlock。这时候问题出现了,服务器2加的锁被服务器1释放了。
4.方案3
针对上面的问题:一个全备的分布式锁要求是:
key 一致
value 包含了持有者信息、加锁的次数(可重入)
对于redis来说,基于lua脚本实现。代码如下(参考redisson):
lock:
if (redis.call('exists', key) == 0) then
redis.call('hset', key, holder_info, 1);
redis.call('pexpire', key, timeout);
return nil;
end;
if (redis.call('hexists', key, holder_info) == 1) then
redis.call('hincrby', key, holder_info, 1);
redis.call('pexpire', key, timeout);
return nil;
end;
return redis.call('pttl', key);
unlock:
if (redis.call('exists', key) == 0) then
return 1;
end;
if (redis.call('hexists', key, holder_info) == 0) then
return nil; //非本持有者加的锁
end;
local counter = redis.call('hincrby', key, holder_info, -1);
if (counter > 0) then
redis.call('pexpire', key, timeout);
return 0;
else
redis.call('del', key);
return 1;
end;
return nil;
5.总结
那么真正对于分布式锁的要求是什么,有以下几点:
1. 不同服务器之间操作串行化(锁的含义)
2.可重入(根据业务场景、代码来决定)
3.一个服务器加的锁,不可被另外的服务器删除(误删除)
4.具有应用崩溃恢复机制
6.展望
希望以下的能够引起读者的思考:
1.如果一个应用没有获取到锁,并且业务需求是需要进行重试加锁,那么只能够轮询。是否还有别的办法?
2.目前讨论的都是基于redis单点且服务正常的情况下。 如果redis崩溃,那么如何处理?
3.redis、zookeeper、db实现的分布式锁的优缺点各是什么?