Redis分布式锁服务

概述

在多线程环境下,一般会使用锁来保证有且只有一个线程来操做共享资源。好比:git

object obj = new object();
lock (obj) 
{ 
//操做共享资源 
}

利用操做系统提供的锁机制,能够确保多线程或多进程下的并发惟一操做。但若是在多机环境下就不能知足了,当A,B两台机器同时操做C机器的共享资源时,就须要第三方的锁机制来保证在分布式环境下的资源协调,也称分布式锁。github

Redis有三个最基本属性来保证分布式锁的有效实现:redis

  • 安全性: 互斥,在任什么时候候,只有一个客户端能持有锁。
  • 活跃性A:没有死锁,即便客户端在持有锁的时候崩溃,最后也会有其余客户端能得到锁,超时机制。
  • 活跃性B:故障容忍,只有大多数Redis节点时存活的,客户端仍能够得到锁和释放锁。

分布式锁

因为Redis是单线程模型,命令操做原子性,因此利用这个特性能够很容易的实现分布式锁。 得到一个锁算法

SET key uuid NX PX timeout
SET resource_name uniqueVal NX PX 30000

命令中的NX表示若是key不存在就添加,存在则直接返回。PX表示以毫秒为单位设置key的过时时间,这里是30000ms。 设置过时时间是防止得到锁的客户端忽然崩溃掉或其余异常状况,致使redis中的对象锁一直没法释放,形成死锁。 Key的值须要在全部请求锁服务的客户端中,确保是个惟一值。 这是为了保证拿到锁的客户端能安全释放锁,防止这个锁对象被其余客户端删除。 举个例子:安全

  1. A客户端拿到对象锁,但在由于一些缘由被阻塞致使没法及时释放锁。
  2. 由于过时时间已到,Redis中的锁对象被删除。
  3. B客户端请求获取锁成功。
  4. A客户端此时阻塞操做完成,删除key释放锁。
  5. C客户端请求获取锁成功。
  6. 这时B、C都拿到了锁,所以分布式锁失效。

要避免例子中的状况发生,就要保证key的值是惟一的,只有拿到锁的客户端才能进行删除。 基于这个缘由,普通的del命令是不能知足要求的,咱们须要一个能判断客户端传过来的value和锁对象的value是否同样的命令。遗憾的是Redis并无这样的命令,但能够经过Lua脚原本完成:多线程

if redis.call("get",KEYS[1]) == ARGV[1] then 
   return redis.call("del",KEYS[1])
 else 
   return 0
 end

逻辑很简单,获取key中的值和参数中的值相比较,相等删除,不相等返回0。 并发

多实例分布式锁

上面是在单个Redis实例实现分布式锁的,这存在一个问题就是,若是这台实例因某些缘由崩溃掉,那么全部客户端的锁服务所有失效。 Redis自己支持Master-Slave结构,能够一主多从,采用高可用方法,能够保证在master挂的时候自动切换到slave。 可是因为主从之间是异步同步数据的,因此redis并不能彻底的实现锁的安全性。 举个例子来讲:异步

  1. A客户端在master实例上得到一个锁。
  2. 在对象锁key传送到slave以前,master崩溃掉。
  3. 一个slave被选举成master。
  4. B客户端能够获取到同个key的锁,但A也已经拿到锁,致使锁失效。

在多台master状况下实现这个算法,并保证锁的安全性。 步骤以下:分布式

  1. 客户端以毫秒为单位获取当前时间。
  2. 使用一样key和值,循环在多个实例中得到锁。 为了得到锁,客户端应该设置个偏移时间,它小于锁自动释放时间(即key的过时时间)。 举个例子来讲,若是一个锁自动释放时间是10秒,那偏移时间应该设置在5~50毫秒的范围。 防止由于某个实例崩溃掉或其余缘由,致使client在获取锁时耗时过长。
  3. 计算获取全部锁的耗时,即当前时间减去开始时间,获得a值。 用锁自动释放时间减去a值,在减去偏移时间,获得c值,若是获取锁成功的实例数量大于实际的数量一半,而且c大于0,那么锁就被获取成功。
  4. 锁获取成功,锁对象的有效时间是上面的c值。
  5. 如果客户端由于一些缘由获取失败,缘由多是上面的c值为负数或者锁成功的数量小于实例数,以用N/2+1当标准(N为实例数)。 那么会释放全部实例上的锁。

上面描述可能不方便理解,用代码表示以下:ui

//锁自动释放时间
TimeSpan ttl=new TimeSpan(0,0,0,30000)
//获取锁成功的数量
 int n = 0; 
//记录开始时间
 var startTime = DateTime.Now;

  //在每一个实例上获取锁
                for_each_redis(
                    redis =>
                    {
                        if (LockInstance(redis, resource, val, ttl)) n += 1;
                    }
                );

//偏移时间是锁自动释放时间的1%,根据上面10s是5-50毫秒推出。
 var drift = Convert.ToInt32(ttl.TotalMilliseconds * 0.01); 

//锁对象的有效时间=锁自动释放时间-(当前时间-开始时间)-偏移时间
 var validity_time = ttl - (DateTime.Now - startTime) - new TimeSpan(0, 0, 0, 0, drift);

//判断成功的数量和有效时间c值是否大于0 if (n >= (N/2+1) && validity_time.TotalMilliseconds > 0) { }

总结

用Redis作分布式锁相比其余分布式锁(zookeeper)实现更简单,速度更快。 在ServiceStack.Redis客户端组件上是直接支持锁实现的。 或者用stackexchange客户端组件,锁实现及示例代码:https://github.com/kidfashion/redlock-cs。

官方介绍文档:http://redis.io/topics/distlock。

相关文章
相关标签/搜索