分布式锁
基于哪些服务实现分布式锁?
- memcache
- ZooKeeper
- Redis
- Hazelcast
- google Chubby
if (cache.add("lock:{orderid}", currenttimestamp, expiredtime)) {// 已获得锁,继续try{do something}catch{...}cache.delete("lock.{orderid}")} else {// 或等待锁超时,或重试,或返回}
- C0操作超时了,但它还持有着锁,C1和C2读取lock.{orderid}检查时间戳,先后发现超时了。
- C1 发送delete lock.{orderid},
- C1 发送set lock.{orderid} 且成功。
- C2 发送delete lock.{orderid},
- C2 发送set lock.{orderid} 且成功。
所谓保持独占,就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。通常的做法是把zk上的一个znode看作是一把锁,通过 create znode的方式来实现。所有客户端都去创建 /distributed_lock 节点,最终成功创建的那个客户端也就拥有了这把锁。
控制时序,就是所有试图获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序。做法和上面基本类似,只是这里 /distributed_lock 已经预先存在,客户端在它下面创建临时有序节点(这个可以通过节点的属性控制:CreateMode.EPHEMERAL_SEQUENTIAL来指 定)。zk的父节点(/distributed_lock)维持一份sequence,保证子节点创建的时序性,从而形成了每个客户端的全局时序。
ZooKeeper 里实现分布式锁的基本逻辑:
- 客户端调用create()方法创建名为“_locknode_/guid-lock-”的节点,需要注意的是,这里节点的创建类型需要设置为EPHEMERAL_SEQUENTIAL。
- 客户端调用getChildren(“_locknode_”)方法来获取所有已经创建的子节点,同时在这个节点上注册上子节点变更通知的Watcher。
- 客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点是所有节点中序号最小的,那么就认为这个客户端获得了锁。
- 如果在步骤3中发现自己并非是所有子节点中最小的,说明自己还没有获取到锁,就开始等待,直到下次子节点变更通知的时候,再进行子节点的获取,判断是否获取锁。
释放锁的过程相对比较简单,就是删除自己创建的那个子节点即可。
- C3发送SETNX lock.{orderid} 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0,
- C3发送GET lock.{orderid} 以检查锁是否超时了,如果没超时,则等待或重试。
- 反之,如果已超时,C3通过下面的操作来尝试获得锁:
GETSET lock.{orderid} <current Unix time + lock timeout + 1> - 通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。
- 如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。
-
# get lock
-
lock = 0
-
while lock != 1:
-
timestamp = current Unix time + lock timeout + 1
-
lock = SETNX lock.orderid timestamp
-
if lock == 1 or (now() > (GET lock.orderid) and now() > (GETSET lock.orderid timestamp)):
-
break
-
else:
-
sleep(10ms)
-
-
do_your_job()
-
-
# release lock
-
if now() < GET lock.orderid:
-
DEL lock.orderid
<?php
/***
* Wap组
* Summary:Redis 操作
* Redis 分布式锁
* @author: chengql 2014/11/12
*/
class WapRedisDistributedLock {
//锁的超时时间
const TIMEOUT = 20;
const SLEEP = 100000;
/**
* 当前锁的过期时间
* @var int
*/
protected static $expire;
public static function getRedis()
{
require_once CODE_BASE2 . '/util/redis/RedisClient.class.php';
$RedisClient = new RedisClient( RedisConfig::$GROUP_WEBAPP );
return $RedisClient->getMasterRedis('wapmsc');
}
/**
* Gets a lock or waits for it to become available
* 获得锁,如果锁被占用,阻塞,直到获得锁或者超时
*
* 如果$timeout参数为0,则立即返回锁。
*
* @param string $key
* @param int $timeout Time to wait for the key (seconds)
* @return boolean 成功,true;失败,false
*/
public static function lock($key, $timeout = null){
if(!$key){
return false;
}
$start = time();
$redis = self::getRedis();
do{
self::$expire = self::timeout();
if($acquired = ($redis->setnx("Lock:{$key}", self::$expire))){
break;
}
if($acquired = (self::recover($key))){
break;
}
if($timeout === 0) {
//如果超时时间为0,即为
break;
}
usleep(self::SLEEP);
} while(!is_numeric($timeout) || time() < $start + $timeout);
if(!$acquired){
//超时
return false;
}
return true;
}
/**
* Summary:释放锁
* @param mixed $key Item to lock
*/
public static function release($key){
if(!$key){
return false;
}
$redis = self::getRedis();
if(self::$expire > time()) {
$redis->del("Lock:{$key}");
}
}
/**
* Summary: 超时时间
*/
protected static function timeout(){
return (int) (time() + self::TIMEOUT + 1);
}
/**
* Recover an abandoned lock
* @param mixed $key Item to lock
* @return bool Was the lock acquired?
*/
protected static function recover($key){
$redis = self::getRedis();
if(($lockTimeout = $redis->get("Lock:{$key}")) > time()) {
//锁还没有过期
return false;
}
$timeout = self::timeout();
$currentTimeout = $redis->getset("Lock:{$key}", $timeout);
if($currentTimeout != $lockTimeout) {
return false;
}
self::$expire = $timeout;
return true;
}
}