概述
在多线程环境下, 通常会使用锁来保证有且只有一个线程来操作共享资源. 比如:
- object obj = new object();
- lock (obj)
- {
- // 操作共享资源
- }
利用操作系统提供的锁机制, 可以确保多线程或多进程下的并发唯一操作. 但如果在多机环境下就不能满足了, 当 A,B 两台机器同时操作 C 机器的共享资源时, 就需要第三方的锁机制来保证在分布式环境下的资源协调, 也称分布式锁.
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 的值需要在所有请求锁服务的客户端中, 确保是个唯一值. 这是为了保证拿到锁的客户端能安全释放锁, 防止这个锁对象被其他客户端删除.
举个例子:
A 客户端拿到对象锁, 但在因为一些原因被阻塞导致无法及时释放锁.
因为过期时间已到, Redis 中的锁对象被删除.
B 客户端请求获取锁成功.
A 客户端此时阻塞操作完成, 删除 key 释放锁.
C 客户端请求获取锁成功.
这时 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 并不能完全的实现锁的安全性. 举个例子来说:
A 客户端在 master 实例上获得一个锁.
在对象锁 key 传送到 slave 之前, master 崩溃掉.
一个 slave 被选举成 master.
B 客户端可以获取到同个 key 的锁, 但 A 也已经拿到锁, 导致锁失效.
在多台 master 情况下实现这个算法, 并保证锁的安全性. 步骤如下:
客户端以毫秒为单位获取当前时间.
使用同样 key 和值, 循环在多个实例中获得锁. 为了获得锁, 客户端应该设置个偏移时间, 它小于锁自动释放时间(即 key 的过期时间). 举个例子来说, 如果一个锁自动释放时间是 10 秒, 那偏移时间应该设置在 5~50 毫秒的范围. 防止因为某个实例崩溃掉或其他原因, 导致 client 在获取锁时耗时过长.
计算获取所有锁的耗时, 即当前时间减去开始时间, 得到 a 值. 用锁自动释放时间减去 a 值, 在减去偏移时间, 得到 c 值, 如果获取锁成功的实例数量大于实际的数量一半, 并且 c 大于 0, 那么锁就被获取成功.
锁获取成功, 锁对象的有效时间是上面的 c 值.
若是客户端因为一些原因获取失败, 原因可能是上面的 c 值为负数或者锁成功的数量小于实例数, 以用 N/2+1 当标准(N 为实例数). 那么会释放所有实例上的锁.
上面描述可能不方便理解, 用代码表示如下:
- // 锁自动释放时间
- 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.
本文的重点是你有没有收获与成长, 其余的都不重要, 希望读者们能谨记这一点. 同时我经过多年的收藏目前也算收集到了一套完整的学习资料, 包括但不限于: 分布式架构, 高可扩展, 高性能, 高并发, Jvm 性能调优, Spring,MyBatis,Nginx 源码分析, Redis,ActiveMQ,,Mycat,Netty,Kafka,MySQL,Zookeeper,Tomcat,Docker,Dubbo,Nginx 等多个知识点高级进阶干货, 希望对想成为架构师的朋友有一定的参考和帮助
喜欢这篇文章的朋友可以点个喜欢, 也可以关注一下我的个人专题: Java 成长之路
需要更详细思维导图和以下资料的可以加一下技术交流分享群:"708 701 457" 免费获取
来源: http://www.jianshu.com/p/07462c95089a