惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Spread Privacy
Spread Privacy
K
Kaspersky official blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Forbes - Security
Forbes - Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
The Last Watchdog
The Last Watchdog
SecWiki News
SecWiki News
Attack and Defense Labs
Attack and Defense Labs
Google DeepMind News
Google DeepMind News
Security Archives - TechRepublic
Security Archives - TechRepublic
S
Secure Thoughts
WordPress大学
WordPress大学
Microsoft Security Blog
Microsoft Security Blog
P
Proofpoint News Feed
云风的 BLOG
云风的 BLOG
V
Visual Studio Blog
Security Latest
Security Latest
TaoSecurity Blog
TaoSecurity Blog
Cyberwarzone
Cyberwarzone
S
SegmentFault 最新的问题
Cloudbric
Cloudbric
aimingoo的专栏
aimingoo的专栏
S
Schneier on Security
N
Netflix TechBlog - Medium
MyScale Blog
MyScale Blog
T
The Blog of Author Tim Ferriss
H
Hacker News: Front Page
C
Cybersecurity and Infrastructure Security Agency CISA
小众软件
小众软件
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
AWS News Blog
AWS News Blog
AI
AI
G
GRAHAM CLULEY
IT之家
IT之家
P
Privacy & Cybersecurity Law Blog
L
Lohrmann on Cybersecurity
Last Week in AI
Last Week in AI
D
Docker
Recent Announcements
Recent Announcements
O
OpenAI News
T
Threat Research - Cisco Blogs
GbyAI
GbyAI
S
Security @ Cisco Blogs
T
Troy Hunt's Blog
C
Check Point Blog
博客园 - 三生石上(FineUI控件)
A
About on SuperTechFans
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
N
News and Events Feed by Topic

文章列表 - 她和她的猫

使用小鹤双拼一个月 折腾 Hugo PaperMod 主题 去长鹿旅游休博园 1Password 自动化登录堡垒机 深入 Hyperf:Inject 注解是如何工作的? 译|理解容器镜像层 使用 Rust 实现高性能预写日志(Write Ahead Log) 译|使用 Linux 命名空间隔离系统 深入 Hyperf:HTTP 服务是如何处理请求的? Linux 文件权限学习笔记 译|了解 Bitcask:基于日志结构的 KV 存储引擎 还原 SM2 压缩公钥的几种方法 深入 Hyperf:HTTP 服务启动时发生了什么? 一次 Hyperf 注解失效问题分析 将博客迁移到又拍云 译|PHP 7 新的 Hashtable 实现 排查 ES 查询问题:深入了解 json_encode() 函数 Yar 源码阅读笔记:RPC 服务端 Yar 源码阅读笔记:客户端的并行调用 Yar 源码阅读笔记:客户端的同步调用 Yar 源码阅读笔记:数据传输模块 Yar 源码阅读笔记:消息编码模块 Yar 源码阅读笔记:RPC 通信协议 Yar 源码阅读笔记:开篇 PHP 8 新特性介绍 基于 OpenSSL 实现国密 SM4 加解密 PHP 源码阅读笔记:编译与调试 PHP 什么是惊群问题 大端模式和小端模式 基于 GitHub Actions 定时推送网址到百度站长平台 聊聊五种 I/O 模型 Workerman 源码分析:文件上传 浅入浅出 HTTP 协议 PHP 网络编程:构建 MySQL 蜜罐获取攻击者微信 ID 使用 Workerman 接入 Bilibili 直播弹幕协议 使用 GitHub Actions 自动部署 Hexo 什么是二进制安全 【转载】PHP 程序员进阶之路 优化 Workerman 检查主进程是否存活的逻辑 基于 Redis 实现分布式锁 《PHP 实现 Base64 编码/解码》笔记 PHP 多进程下载必应壁纸 基于 Redis 实现延迟队列 【转载】PHP 实现 Base64 编码/解码 使用 MySQL FIELD() 函数自定义排序规则 Laravel 的 Facades 实现原理 C语言单链表实现约瑟夫环 数据结构学习笔记:顺序栈和链栈(C语言) 使用 QueryList + Redis 下载壁纸 在 Laravel 登录/注册中使用 mews/captcha 扩展包 使用 mews/captcha 扩展包为 Laravel 应用添加图片验证码 给 YOURLS 短网址系统编写插件《批量生成短网址》 给 YOURLS 短网址系统编写插件《Hello World!》 PHP 7.1 使用 json_encode 函数造成浮点类型数据出现精度问题 Laravel 授权策略(Policy)的基本使用 Ubuntu 系统下 WiFi 频繁掉线解决方法 Laravel 应用部署到 Nginx 服务器上的第一个坑 Ubuntu 16.04 LST 安装 Redis 和 Composer PHP 采集逐浪小说章节列表 Nginx 环境 SSL 强制 HTTP 301 跳转到 HTTPS 【工作&生活】第一周 ServiceStack.Redis 与多线程 Count the number of Duplicates C# Base64 加密及解密 ASP.NET MVC 使用动态类型传递数据 Django 使用 order_by() 对数据进行排序操作 ASP.NET + jQuery + AJAX 实现用户登录 ASP.NET 从客户端中检测到有潜在危险的 Request.Form 值 ASP.NET 使用 FormData 实现 AJAX 上传图片 Python 使用 BeautifulSoup 抓取网页 贪心算法之字符串的完美度 一言不和就造轮子之 CookieHelper ASP.NET 上传图片 DBHelper 类 C# 捕获屏幕源码 用 JavaScript 做一些有趣的事情 第一篇文章 好久不见。 如何防止非法调用 PHP 文件 清明小记。 PHP 生成随机字符串 Z-Blog PHP 程序做 301 跳转 PHP 处理数组常用的几个函数 Sublime Text 3 快捷键总结 PHP 使用 file_get_contents() 函数实现采集网页 使用 C# 编写学生信息管理系统 终于到了。 zblog报错: magic_quotes_gpc 和 safe_mode 世事无常。 如何使用 PHP 上传文件 安卓手机编写和运行 PHP 程序的软件 使用 AJAX 刷新、验证 PHP 图片验证码 PHP 生成图片验证码 PHP 之 session 的使用方法 PHP 之 cookie 的使用方法 PHP 实现分页原理详解 Thinkphp 框架中系统常量在 Js 文件中不解析的解决方法
浅析 Redlock 分布式锁实现原理
她和她的猫 · 2022-05-08 · via 文章列表 - 她和她的猫

前言

不知不觉中,这篇文章在我的草稿箱已经躺了半年多了。

起初写这篇文章是准备做一次技术分享,后来因为一些原因将分享的主题换成了什么是惊群问题 ,这篇文章也就一直在草稿箱躺到了现在。

在博客鸽了几个月之后的今天,我又想起来了草稿箱的它…,对一些细节进行修改后,让它从草稿箱走了出来。

在介绍 Redlock 前,我们先来看看在项目中是怎么使用 Redis 实现锁的。

基于单实例 Redis 分布式锁

我们平时使用的 Redis 锁大部分都是单实例的,只有一个 Redis 实例。

为什么叫它分布式锁?因为这里的分布式是指分布式的应用,即多个调用方,而不是说锁本身。

Redis 加锁的操作非常简单,只需要使用 SET 命令并带上相关的选项即可。

  • key: 需要锁定的资源,比如对某个订单加锁,那么 key 就可以是订单号。
  • value: 在平时使用的时候,value 通常是一个无意义的值。
  • NX: 只有当 key 不存在的时候才能设置成功,,作用等同于 SETNX 命令。
  • EX: 60 秒后过期,作用等同于 EXPIRE 命令。

NX 选项的作用是,当有多个客户端同时申请对一个资源进行加锁时候,保证了只会有一个客户端能够加锁成功,其它客户端在锁被释放前都无法获得锁。

EX 选项主要是为了防止出现死锁问题,使用 EX 选项对锁设定了有效时间,当客户端持有锁的时间超过有效时间后,锁会自动被释放(过期)。

避免某个客户端获得锁后突然挂掉或其它原因一直持有锁,导致其它客户端永远无法获得锁。

最后,业务逻辑处理完之后,直接使用 DEL 命令删除指定的 key,就完成了释放锁的操作。

可能出现的问题及解决办法

假设客户端 A 获得锁后开始处理业务,这时候客户端 B 也想要获得锁,但是发现锁已经被客户端 A 占有了。然后为了让自己获得锁,不管三七二十一就使用 DEL 命令把锁给释放了。

导致客户端 A 的正在执行的业务逻辑处于不安全的状态,因为创建的锁被别的客户端释放了。

怎么能避免这个问题呢?

我们可以在 SET 命令中将 value 设置成一个随机字符串,作为持有这个锁的令牌(token)。

客户端在释放锁的时候,需要传入自己的令牌,只有令牌与 value 匹配时才能删除 key,这样就保证了锁只能由持有锁的人才能释放。

示例代码:

function releaseLock($lockKey, $token): bool
{
    if (Redis::get($lockKey) == $token) {
        Redis::del($lockKey);
        return true;
    }
    return false;
}

releaseLock('order-lock:订单号', '随机值');

可以看到,上面的代码中需要分别执行 GET 和 DEL 命令,两个命令执行的过程中可能会有其它命令被执行,导致我们释放锁的操作是非原子性的,同时也耗费了两次网络请求。

非原子性操作会产生什么问题呢?

  • 客户端 A 发出 GET 命令拿到令牌。
  • 这时,Redis 中这个 key 过期被删除了。
  • 客户端 B 发出 SET 命令获得锁。
  • 客户端 B 开始执行业务逻辑。
  • 客户端 A 发出 DEL 命令删除 key。

最后导致客户端 B 的锁被客户端 A 释放了。

对于这个问题,我们可以使用 Lua 脚本,让 Redis 来执行释放锁的逻辑,这样 Redis 在执行释放锁的逻辑时就不会被打断,同时减少了一次网络请求。

function releaseLock($lockKey, $token): bool
{
    $luaScript = <<<LUA
if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end
LUA;
    
    return (bool) Redis::eval($luaScript, [$lockKey, $token], 1);
}

Lua 脚本的逻辑与我们上面的 PHP 版是一样的,区别是验证令牌及释放锁的逻辑是由 Redis 执行的,在执行的时候不会被其它命令(信号)所打断,也就保证了释放锁操作的原子性。

上面说的这些都是以 Redis 服务正常为前提,如果 Redis 服务宕机,就会导致加不了锁,后续的业务逻辑也就无法正常的运行。

虽然可以使用 Redis 的集群提升 Redis 服务的可用性,但是在一些情况下还是会导致我们的锁失去作用。

进程 A 发送 SET 命令对 order1 进行加锁操作后,Master 实例宕机了,但是数据还没来得及同步给 Slave 实例,所以 Slave 也就没有这条数据。进行主从切换后,Slave 升级为 Master 提供服务,然后进程 B 对 order1 也能够加锁成功,这时候就有两个进程同时对同一个资源进行操作,所以锁也就失去了作用。

Redlock(Redis lock)是 Redis 作者设计的一种分布式锁,Redlock 直译过来就是红锁。

Redlock 需要部署 N (N >= 2n+1)个独立的 Redis 实例,且实例之间没有任何的联系。也就是说,只要一半以上的 Redis 实例加锁成功,那么 Redlock 依然可以正常运行。

使用独立实例是为了避免 Redis 异步复制导致锁丢失。

Redlock 加锁过程

假设我们有 5 个 Redis 实例,当我们对 order1 这个订单加锁时,先记录当前时间用于统计加锁过程花费的时间,然后依次让 5 个 Redis 实例执行 SET order1 token NX EX 60 命令,最后统计加锁成功的实例数量以及加锁过程耗费的时间。

当加锁成功的实例数量超过半数(>= 3)并且加锁耗费的时间小于锁的有效时间,我们就认为加锁成功了。

为什么需要计算加锁过程耗费的时间呢?

因为当某些 Redis 实例由于网络延迟或其它原因,导致执行 SET 命令花费的时间比较久,这些 Redis 实例执行命令的时间加起来甚至超过了锁的有效时间。

比如锁的有效时间是 4 秒,但是 5 个 Redis 实例执行命令一共花费了 5 秒,对于这种情况,即使加锁成功的实例数量超过了半数,也是算作是加锁失败的。

所以 Redlock 加锁失败有两种情况:

  • 加锁成功的实例数量未超过半数。
  • 加锁过程花费时间超过锁的有效时间。

Redlock 解锁过程

无论是加锁成功还是加锁失败后都需要去释放锁,及时让出相关资源给其它调用者。

解锁的过程实际上就是让 5 个 Redis 实例依次执行 DEL 命令删除加锁时的 key,为了确保只释放自己的锁,需要用前面提到的 Lua 脚本来代替直接使用 DEL 命令进行解锁操作。

总结

在本文中,先介绍了单实例 Redis 锁的一些问题以及解决方法,然后又介绍了基于多实例的 Redlock 分布式锁的实现,Redlock 在解决了单实例以及集群可能会出现的一些问题。

但是 Redlock 本身还是存在一些问题,比如:

  • 加锁/解锁的效率会随着实例数量增加而降低。
  • 客户端无法感知锁失效:客户端获取到锁后,处理业务逻辑时锁失效了,客户端是无法感知的,可能会被其它客户端再次获取到。
  • 过期时间依赖系统时钟:如果系统发生时钟漂移,那么会影响到 Redis 计算过期时间,导致锁提前失效或持有时间比原来更久。

所以在业务选型的过程中,我们需要结合实际业务情况使用合适的分布式锁组件。

  • 业务比较简单或者对锁的可靠性要求不高时,可以直接使用单实例的 Redis 锁。
  • 对于锁的可靠性有一定要求,又不想引入其它组件时,可以使用 Redlock。
  • 对于锁的可靠性要求比较高时,可以使用 ZooKeeper、Etcd 等组件。

如果你想要更深入的了解 Redlock 或上面提到的问题,推荐你阅读下面这些资料: