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

推荐订阅源

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() 函数 浅析 Redlock 分布式锁实现原理 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 检查主进程是否存活的逻辑 《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 文件中不解析的解决方法
基于 Redis 实现分布式锁
她和她的猫 · 2020-03-15 · via 文章列表 - 她和她的猫

前言

之前在写支付回调的时候,因为第三方支付的回调机制有问题,存在并发回调的情况。如果对回调的订单不加锁的话,会造成一笔订单重复处理的情况。

在 Laravel 中使用基于 Redis 的锁非常简单,只需要使用 Cache::lock() 就可以创建和管理锁。

更多使用方法: https://learnku.com/docs/laravel/6.x/cache/5160#atomic-locks

use Illuminate\Support\Facades\Cache;

$orderSn = '123456';

// 创建一个锁
$lock = Cache::lock("pay_callback:{$orderSn}", 10);

if (!$lock->get()) {
    // 没有抢到锁
    exit('failed');
}

// 更新订单支付结果

// 释放锁
$lock->release();

如果用 Redis 作为缓存驱动的话,这段代码的锁就是用 Redis 的 SET 命令加上 NX 和 EX 参数实现的。

本文的例子是单机分布式锁,多 Redis 实例的分布式锁参考: https://redis.io/topics/distlock

什么是分布式锁

多个进程(线程)请求共享资源时,为了保证共享资源在任意时间间隔内只有一个进程(线程)可以进行操作。

为了能够有效使用分布锁,应该具备的三个条件:

  • 互斥性: 在任意时间间隔内,只有一个客户端可以持有锁。
  • 无死锁: 即使锁定资源的客户端崩溃或发生其它意外,其它客户端也始终有可能获取锁。
  • 容错: 只要大多数 Redis 节点还在运行,客户端就可以获取和释放锁。

在 Redis 2.6.12 版本以前,需要 SETNXEXPIRE 两个命令实现锁。

  • SETNX 是 SET if Not eXists 的简写,翻译过来就是当 key 不存在时才 set。
  • EXPIRE 的作用就是设置指定 key 的存活时间,单位为秒。

SETNX 命令保证 key 只会被设置成功一次。

127.0.0.1:6379> SETNX lock-key val
(integer) 1
127.0.0.1:6379> SETNX lock-key val
(integer) 0

可以看到第二次设置 lock-key 时返回 0,表示没有被设置成功。

这时 lock-key 的过期时间为 -1,表示不会过期。

127.0.0.1:6379> ttl lock-key
(integer) -1

EXPIRE 命令保证了即使客户端抢到锁后挂掉了,在到达指定的过期时间后依然可以被其它客户端获取,不会造成死锁。

# 设置10秒后过期
127.0.0.1:6379> expire lock-key 10
(integer) 1

虽然基本上实现了锁,但是由于使用了两个命令,需要发出两次网络请求并等待响应,两次请求都有可能被意外情况打断,不是原子操作。

关于原子操作的解释: 所谓原子操作是指不会被进程(线程)调度机制打断的操作;只要开始执行,就不会被打断,直到执行完毕。

举个例子:

  • 客户端告诉 Redis 说我要用 SETNX 设置个 key。
  • Redis 收到请求并设置成功了。
  • 客户端还没来得及发出设置过期时间的请求就挂掉了。
  • 这时候 key 不会过期,导致其它客户端没办法获取到锁,造成了死锁。

为了避免这种情况可以使用 Lua 脚本来代替,只发出一次网络请求,保证了客户端只要发出了请求,Redis 在执行 Lua 脚本的时候运行正常,就一定能设置锁成功。

为了能够更加方便的实现锁,在 Redis 2.6.12 后,SET 命令新增了 NX、EX 等参数,只需要一条命令就能设置 key、value 及过期时间。

  • NX: 只有在 key 不存在时才设置成功,作用等同于 SETNX。
  • EX: key 多少秒后过期,作用等同于 EXPIRE。

释放锁

在过期时间前处理完业务逻辑后,需要提前释放锁,最简单的办法就是使用 DEL 命令删除。但是这种方法太过于粗暴了,而且会产生问题,比如 A 抢到了锁,然后开始处理自己的业务逻辑,这时候如果 B 直接用 DEL 命令将 A 的锁删除了,就会导致 A 访问的共享资源不安全。这样肯定是不行的,谁创建的锁就只能由谁来释放。

所以在设置锁的时候,需要生成一个唯一的字符串作为当前进程(线程)的 token,然后再将 token 设置为锁的值。在释放锁的时候,需要判断当前进程(线程)的 token 是否等于锁的 token,如果一致再使用 DEL 删除 key。

Lua 脚本示例:

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

代码实现

代码比较简单,就是将 Redis 的几个命令封装了一下。

需要安装 Redis 扩展,也可以改用 predis/predis。

class RedisLock
{
    /**
     * @var Redis
     */
    private $redis;

    /**
     * @var string
     */
    private $token;

    /**
     * @var string
     */
    private $prefix = 'redis-lock';

    /**
     * RedisLock constructor.
     * @param Redis $redis
     */
    public function __construct(Redis $redis = null)
    {
        $this->redis = $redis ?: $this->createRedis();
        $this->token = md5(uniqid(spl_object_hash($this), true));
    }

    /**
     * 获取一个锁.
     * @param string $name
     * @param int $seconds
     * @return bool
     */
    public function get(string $name, int $seconds)
    {
        $args = [$this->buildKey($name), $this->token, $seconds];

        // Redis 2.6.12 版本以后支持 SET 命令支持 NX、EX 选项
        // SET key value NX EX seconds
        // $this->redis->set($this->buildKey($name), $this->token, ['NX', 'EX' => $seconds]);

        return (bool) $this->redis->eval(self::getLockLuaScript(), $args, 1);
    }

    /**
     * 释放锁.
     * @param string $name
     * @return bool
     */
    public function release(string $name)
    {
        $args = [$this->buildKey($name), $this->token];

        return (bool) $this->redis->eval(self::getReleaseLuaScript(), $args, 1);
    }

    /**
     * 强制释放锁.
     * @param string $name
     * @return bool
     */
    public function forceRelease(string $name)
    {
        return (bool) $this->redis->del($this->buildKey($name));
    }

    /**
     * 构造缓存 key.
     * @param $name
     * @return string
     */
    protected function buildKey($name)
    {
        return sprintf('%s%s', $this->prefix, $name);
    }

    /**
     * 创建 Redis 实例.
     * @return Redis
     */
    protected function createRedis()
    {
        $redis = new Redis();

        $redis->connect('127.0.0.1', 6379);

        return $redis;
    }

    /**
     * 获取加锁的 Lua 脚本.
     * @return string
     */
    public static function getLockLuaScript()
    {
        return <<<LUA
if redis.call("setnx",KEYS[1],ARGV[1]) > 0
then
    return redis.call('expire',KEYS[1],ARGV[2])
else
    return 0;
end
LUA;
    }

    /**
     * 获取释放锁的 Lua 脚本.
     * @return string
     */
    public static function getReleaseLuaScript()
    {
        return <<<LUA
if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end
LUA;
    }
}

测试代码

PHP 脚本测试

$lock = new RedisLock();
$lock2 = new RedisLock();

$lock_name = 'order';

echo "锁的名称:".$lock_name.PHP_EOL;
echo "lock1 第1次获取锁:".json_encode($lock->get($lock_name, 10)).PHP_EOL;
echo "lock1 第2次获取锁:".json_encode($lock->get($lock_name, 10)).PHP_EOL;
echo "lock2 第1次获取锁:".json_encode($lock2->release($lock_name)).PHP_EOL;
echo "lock2 尝试释放取锁:".json_encode($lock2->release($lock_name)).PHP_EOL;
echo "lock1 尝试释放取锁:".json_encode($lock->release($lock_name)).PHP_EOL;
echo "lock2 第2次获取锁:".json_encode($lock2->get($lock_name, 10)).PHP_EOL;

运行结果:

锁的名称:order
lock1 第1次获取锁:true
lock1 第2次获取锁:false
lock2 第1次获取锁:false
lock2 尝试释放取锁:false
lock1 尝试释放取锁:true
lock2 第2次获取锁:true

使用 siege 压测

index.php:

require_once 'RedisLock.php';

$lock = new RedisLock();
// 持有锁 1 秒
$result = $lock->get('siege-test', 1);
// 抢到锁返回状态码 200,否则返回 500
http_response_code($result ? 200 : 500);
// 延迟 0.9 秒,方便看效果
usleep(900000);

压测命令:

siege -c 100 http://127.0.0.1:8081

压测结果:

WX20200315-145923@2x.png