老板问我分布式锁,结果悲剧了......
公司交给了萌新小猿一个光荣而艰巨的项目,该项目需要使用分布式锁,这可难道了小猿。
只是听说过分布式锁很牛掰,其他就一概不知了,唉,不懂就问呗,遂向老板请教。
老板:我们每天不都在经历分布式锁吗,我来给你回忆回忆。
本文结构:
为什么要使用分布式锁
分布式锁有哪些特点
分布式锁流行算法及其优缺点
总结
为什么要使用分布式锁
①为什么使用锁
共享资源只允许一个客户端操作
共享资源允许多个客户端操作
仅允许一个客户端访问:共享资源的操作不具备幂等性。常见于数据的修改、删除操作。
在上面的例子中:
允许多个客户端操作:主要应用场景是共享资源的操作具有幂等性;如数据的查询。
既然都具有幂等性了,为什么还需要分布式锁呢,通常是为了效率或性能,避免重复操作(尤其是消耗资源的操作)。
例如我们常见的缓存方案:
在上面的例子中:
查询用户信息
查询历史订单
锁+缓存+缓存失效/失效重新获取/缓存定时更新。
②锁为什么需要分布式的?
还是以上面的缓存方案为例,此处略作变化:
在上面的例子中:
分布式锁有哪些特点?
①互斥性
在任意时刻,仅允许有一个客户端获得锁。
②可重入性
客户端 A 获得了锁,只要锁没有过期,客户端 A 可以继续获得该锁。锁在我这里,我还要继续使用,其他人不准抢。
这种特性可以很好的支持【锁续约】功能。例如:客户端 A 获取锁,锁释放时间为 10S,即将到达 10S 时,客户端 A 未完成任务,需要再申请 5S。若锁没有可重入性,客户端 A 将无法续约,导致锁可能被其他客户端抢走。
老板:小猿啊,难得你这么好学,我很欣慰,我们的交流时间延10分钟吧,其他会议延后。
③高性能
获取锁的效率应该足够高;总不能让业务阻塞在获取锁上面吧?
老板:嗯,我已经接受会议邀请了;
小猿:老板你真高效。
④高可用
⑤支持阻塞和非阻塞式锁
获取锁失败,是直接返回失败,还是一直阻塞知道获取成功?不同的业务场景有不同的答案。
⑥解锁权限
客户端仅能释放(解锁)自己加的锁。常见的解决方案是,给锁加随机数(或 ThreadID)。
笼子里的鹦鹉:明白啦,明白啦。
⑦避免死锁
加锁方异常终止无法主动释放锁;常规做法是 加锁时设置超时时间,如果未主动释放锁,则利用 Redis 的自动过期被动释放锁。
⑧异常处理
常见的异常情况有 Redis 宕机、时钟跳跃、网络故障等。
分布式锁流行算法
基本方案 SETNX
①获取锁 SET lock:resource_name random_value NX PX 30000
random_value:通常存储加锁方的唯一标记,如“UUID+ThreadID”。
NX:Key 不存在才设置,即锁未被其他人加锁才能加锁。
PX:锁超时时间。
②释放锁(LUA 脚本)
public class RedisTool {
// 释放锁成功标记
private static final Long RELEASE_LOCK_SUCCESS = 1L;
/**
* 释放分布式锁
*
* @param jedis Redis客户端
* @param lockKey 锁标记
* @param lockValue 加锁方标记
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String lockValue) {
String script = "" +
"if redis.call('get', KEYS[1]) == ARGV[1] then" +
" return redis.call('del', KEYS[1]) " +
"else" +
" return 0 " +
"end";
// Collections.singletonList():用于只有一个元素的场景,减少内存分配
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
if (RELEASE_LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
Redlock 算法
Redlock 算法原理:【核心】大多数节点获取锁成功且锁依旧有效。
Step 1:获取当前时间(毫秒数)。
Step 2:按序想 N 个 Redis 节点获取锁。设置随机字符串 random_value;设置锁过期时间:
Note 1:获取锁需设置超时时间(防止某个节点不可用),且 timeout 应远小于锁有效时间(几十毫秒级)。
Note 2:某节点获取锁失败后,立即向下一个节点获取锁(任何类型失败,包含该节点上的锁已被其他客户端持有)。
Step 3:计算获取锁的总耗时 totalTime。
Step 4:获取锁成功。
获取锁成功:客户端从大多数节点(>=N/2+1)成功获取锁,且 totalTime 不超过锁的有效时间。
重新计算锁有效时间:最初锁有效时间减 3.1 计算的获取锁消耗的时间。
Step 5:获取锁失败。
获取失败后应立即向【所有】客户端发起释放锁(Lua 脚本)。
Step 6:释放锁。
Redlock 算法优点:
可用性高,大多数节点正常即可。
单 Redis 节点的分布式锁在 failover 时锁失效问题不复存在。
Redlock 算法问题点:
Redis 节点崩溃将影响锁安全性:节点崩溃前锁未持久化,节点重启后锁将丢失;Redis 默认 AOF 持久化是每秒刷盘(fsync)一次,最坏情况将丢失 1 秒的数据。
需避免始终跳跃:管理员手动修改时钟;使用[不会跳跃调整系统时钟]的 ntpd(时钟同步)程序,对时钟修改通过多次微调实现。
客户端阻塞导致锁过期,导致共享资源不安全。
如果获取锁消耗时间较长,导致效时间很短,是否应该立即释放锁?多段才算短?
带 fencing token 的实现
神仙之战:Martin Kleppmann 认为 Redis 作者 antirez 提出的 RedLock 算法有安全性问题,双方在网络上多轮探讨交锋。
Martin 指出 RedLock 算法的核心问题点如下:
锁过期或者网络延迟将导致锁冲突:客户端 A 进程 pause→锁过期→客户端 B 持有锁→客户端 A 恢复并向共享资源发起写请求;网络延迟也会产生类似效果。
RedLock 安全性对系统时钟有强依赖。
fencing token 算法原理:
fencing token 是一个单调递增的数字,当客户端成功获取锁时随同锁一起返回给客户端。
客户端访问共享资源时带上 token。
共享资源服务检查 token,拒绝延迟到来的请求。
fencing token 算法问题点:
需要改造共享资源服务。
如果资源服务也是分布式,如何保证 token 在多个资源服务节点递增。
2 个 fencing token 到达资源服务的顺序颠倒,服务检查将异常。
【antirez】既然存在 fencing 机制保持资源互斥访问,为什么还需要分布式锁且要求強安全性呢。
其他分布式锁
数据库排它锁:
获取锁(select for update ,悲观锁)。
处理业务逻辑。
释放锁(connection.commit())。
注意:InnoDB 引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。So 必须给 lock_name 加索引。
ZooKeeper 分布式锁:
客户端创建 znode 节点,创建成功则获取锁成功。
持有锁的客户端访问共享资源完成后删除 znode。
znode 创建成 ephemeral(znode 特性),保证创建 znode 的客户端崩溃后,znode 会被自动删除。
【问题】Zookeeper 基于客户端与 Zookeeper 某台服务器维护 Session,Session 依赖定期心跳(heartbeat)维持。
Zookeeper 长时间收不到客户端心跳,就任务 Session 过期,这个 Session 所创建的所有 ephemeral 类型的 znode 节点都将被删除。
Google 的 Chubby 分布式锁:
sequencer 机制(类似 fencing token)缓解延迟导致的问题。
锁持有者可随时请求一个 sequencer。
客户端操作资源时将 sequencer 传给资源服务器。
资源服务器检查 sequencer 有效性:①调用 Chubby 的 API(CheckSequencer)检查。②对比检查客户端、资源服务器当前观察到的 sequencer(类似 fencing token)。③lock-delay:允许客户端为持有锁指定一个 lock-delay 延迟时间,Chubby 发现客户端失去联系时,在 lock-delay 时间内组织其他客户端获取锁;
总结
我们该使用怎样的分布式锁算法?
技术都是为业务服务的,避免选择“高大上”的炫技;
依托业务场景,尽可能选择最简单的做法;
最简单的分布式锁导致偶发性异常如何处理呢?建议增加额外的机制甚至人工介入保证业务准确性,通常这部分成本低于复杂的分布式锁的开发、运维成本。
分布式锁的另类玩法,“分而治之”经久不衰:
如果共享资源本身可以拆分,那就分开处理吧。
比如电商系统防止超卖,假设有 10000 个口罩将被秒杀,常规做法是一个锁控制所有资源。另类玩法就是将 10000 个口罩交由 20 个锁控制,整体性能瞬间提升几十倍。
PS:此处超卖仅是举例,真实场景下的秒杀超卖有更加复杂的场景,慎重。
编辑:陶家龙
精彩文章推荐: