在分布式系统当中, Redis锁是一个很常用的工具. 举个很常见的例子就是: 某个接口需要去查询数据库的数据, 但是请求量却又很大, 所以我们一般会加一层缓存, 并且设定过期时间. 但是这里存在一个问题就是当并发量很大的情况下, 在缓存过期的瞬间, 会有大量的请求穿透去数据库请求数据, 造成缓存雪崩效应. 这时候如果有锁的机制, 那么就可以控制单个请求去更新缓存.
其实对于Redis锁的看法, 网上已经有很多了, 只是大部分都是基于Java来实现的, 这里给出一个PHP实现的版本. 这里考虑的只是单机部署Redis的情况, 相对会简单好理解, 而且也更加的实用. 如果有分布式Redis部署的情况, 可以参考下Redlock算法的实现.
实现一个分布式锁定, 我们至少要考虑它能满足一下的这些需求:
我们这里使用的是Predis这个这个PHP的客户端, 其他客户端也是同理. 先来看看代码:
class RedisTool {
const LOCK_SUCCESS = 'OK';
const IF_NOT_EXIST = 'NX';
const MILLISECONDS_EXPIRE_TIME = 'PX';
const RELEASE_SUCCESS = 1;
/**
* 尝试获取锁
* @param \Predis\Client $redis redis客户端
* @param String $key 锁
* @param String $requestId 请求id
* @param int $expireTime 过期时间
* @return bool 是否获取成功
*/
public static function tryGetLock(\Predis\Client $redis, String $key, String $requestId, int $expireTime) {
$result = $redis->set($key, $requestId, self::MILLISECONDS_EXPIRE_TIME, $expireTime, self::IF_NOT_EXIST);
return self::LOCK_SUCCESS === (string)$result;
}
}
定义一些Redis的操作符作为常量, 加锁的代码其实很简单, 一行代码即可. 简单解释下这个set方法的五个参数:
PS. 请求的唯一性ID生成方式很多, 可以参考下这个chronos, 该库是Java版本的, 下回给出一个简单的PHP实现.
简单解释下上面的那段代码, 设置NX保证了只能有一个客户端获取到锁, 满足互斥性; 加入了过期时间, 保证在客户端崩溃后不会造成死锁; 请求ID的作用是用来标识客户端, 这样客户端在解锁的时候可以进行校验是否同一个客户端.
当锁拥有的客户端完成了对共享资源的操作后, 释放锁需要用到Lua脚本, 也很简单:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
PHP代码实现:
class RedisTool {
const RELEASE_SUCCESS = 1;
public static function releaseLock(\Predis\Client $redis, String $key, String $requestId) {
$lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
$result = $redis->eval($lua, 1, $key, $requestId);
return self::RELEASE_SUCCESS === $result;
}
}
没想到一个简单的解锁操作也要用到Lua脚本, 待会会说说常见的几种错误解锁的方式. 其实为什么要用Lua脚本来实现, 主要是为了保证原子性. Redis的eval可以保证原子性, 主要还是源于Redis的特性, 可以看看官网的介绍
错误加锁1
public static function wrong1(\Predis\Client $redis, String $key, String $requestId, int $expireTime) {
$result = $redis->setnx($key, $requestId);
if ($result == 1) {
// 这里程序挂了或者expire操作失败,则无法设置过期时间,将发生死锁
$redis->expire($key, $expireTime);
}
}
这是比较常见的一种错误实现, 先通过setnx加锁, 然后在通过expire设置过期时间. 这样乍一看和上面的不都一样吗? 其实不然, 这是两条Redis命令, 不具有原子性, 如果在setnx之后程序挂了, 会使得锁没有设置过期时间, 这样就会发生死锁定.
错误加锁2
public static function wrong2(\Predis\Client $redis, String $key, int $expireTime) {
$expires = floor(microtime(true) * 1000) + $expireTime;
// 如果当前锁不存在,返回加锁成功
if ($redis->setnx($key, $expires) == 1) {
return true;
}
// 如果锁存在,获取锁的过期时间
$currentValue = floor($redis->get($key));
if ($currentValue != null && $currentValue < floor(microtime(true) * 1000)) {
// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
$oldValue = floor($redis->getSet($key, $expires));
if ($oldValue != null && $oldValue === $currentValue) {
// 考虑并发的情况,只有设置值和当前值相同,它才有权利加锁
return true;
}
}
// 其他情况,一律返回加锁失败
return false;
}
这个例子实现原理是使用setnx来加锁, 如果锁已经存在的话则获取锁的过期时间并且与当前的时间比较, 过期则设置新的时间, 并且返回加锁成功. 虽然这样也可以加锁, 但是会存在几个问题:
错误解锁1
public static function wrongRelease1(\Predis\Client $redis, String $key) {
$redis->del([$key]);
}
这是最典型的错误了, 这样的做法没判断锁的拥有者, 会使得任何一个客户端都可以解锁, 甚至会把别人的锁给解除了.
错误解锁2
public static function wrongRelease2(\Predis\Client $redis, String $key, String $requestId) {
// 判断加锁与解锁是不是同一个客户端
if ($requestId === $redis->get($key)) {
// 若在此时,这把锁突然不是这个客户端的,则会误解锁
$redis->del([$key]);
}
上面的解锁也是没有保证原子性, 注释说的很明白了, 有这样的场景来复现:
客户端A加锁成功后一段时间再来解锁, 在执行删除del操作的时候锁过期了, 而且这时候又有其他客户端B来加锁(这时候加锁是肯定成功的, 因为客户端A的锁过期了), 这是客户端A再执行删除del操作, 会把客户端B的锁给清了.
这样就基本上实现了一个简单的基于Redis的分布式锁. 其实分布式锁的实现远比想象的复杂, 特别是在多机部署Redis的情况下. 当然实现的方式也不仅仅包括Redis, 还可以用Zookeeper来实现. 随着对分布式系统的深入理解, 可以再来慢慢地思考这个问题.
微信与订阅号,欢迎关注
[](http://summerwind.qiniudn.com/qrcode2.png)
[
](http://summerwind.qiniudn.com/qrcode2.png)
Original url: Access
Created at: 2018-10-10 17:23:24
Category: default
Tags: none
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
java windows火焰图_mob64ca12ec8020的技术博客_51CTO博客 - 在windows下不可行,不知道作者是怎样搞的 监听SpringBoot 服务启动成功事件并打印信息_监听springboot启动完毕-CSDN博客 SpringBoot中就绪探针和存活探针_management.endpoint.health.probes.enabled-CSDN博客 u2u转换板 - 嘉立创EDA开源硬件平台 Spring Boot 项目的轻量级 HTTP 客户端 retrofit 框架,快来试试它!_Java精选-CSDN博客 手把手教你打造一套最牛的知识笔记管理系统! - 知乎 - 想法有重合-理论可参考 安宇雨 闲鱼 机械键盘 客制化 开贴记录 文本 linux 使用find命令查找包含某字符串的文件_beijihukk的博客-CSDN博客_find 查找字符串 ---- mac 也适用 安宇雨 打字音 记录集合 B站 bilibili 自行搭建 开坑 真正的客制化 安宇雨 黑苹果开坑 查找工具包maven pom 引用地 工具网站 Dantelis 介绍的玩轴入坑攻略 --- 关于轴的一些说法 --- 非官方 ---- 心得而已 --- 长期开坑更新 [本人问题][新开坑位]关于自动化测试的工具与平台应用 机械键盘 开团 网站记录 -- 能做一个收集的程序就好了 不过现在没时间 -- 信息大多是在群里发的 - 你要让垃圾佬 都去一个地方看难度也是很大的 精神支柱 [超级前台]sprinbboot maven superdesk-app 记录 [信息有用] [环境准备] [基本完成] [sebp/elk] 给已创建的Docker容器增加新的端口映射 - qq_30599553的博客 - CSDN博客 [正在研究] Elasticsearch, Logstash, Kibana (ELK) Docker image documentation elasticsearch centos 安装记录 及 启动手记 正式服务器 39 elasticsearch 问题合集 不断更新 6.1.1 | 6.5.1 两个版本 博客程序 - 测试 - bug记录 等等问题 laravel的启动过程解析 - lpfuture - 博客园 OAuth2 Server PHP 用 Laravel 搭建带 OAuth2 验证的 RESTful 服务 | Laravel China 社区 - 高品质的 Laravel 和 PHP 开发者社区 利用Laravel 搭建oauth2 API接口 附 Unauthenticated 解决办法 - 煮茶的博客 - SegmentFault 思否 使用 OAuth2-Server-php 搭建 OAuth2 Server - 午时的海 - 博客园 基于PHP构建OAuth 2.0 服务端 认证平台 - Endv - 博客园 Laravel 的 Artisan 命令行工具 Laravel 的文件系统和云存储功能集成 浅谈Chromium中的设计模式--终--Observer模式 浅谈Chromium中的设计模式--二--pre/post和Delegate模式 浅谈Chromium中的设计模式--一--Chromium中模块分层和进程模型 DeepMind 4 Hacking Yourself README.md update 20211011
Laravel China 简书 知乎 博客园 CSDN博客 开源中国 Go Further Ryan是菜鸟 | LNMP技术栈笔记 云栖社区-阿里云 Netflix技术博客 Techie Delight Linkedin技术博客 Dropbox技术博客 Facebook技术博客 淘宝中间件团队 美团技术博客 360技术博客 古巷博客 - 一个专注于分享的不正常博客 软件测试知识传播 - 测试窝 有赞技术团队 阮一峰 语雀 静觅丨崔庆才的个人博客 软件测试从业者综合能力提升 - isTester IBM Java 开发 使用开放 Java 生态系统开发现代应用程序 pengdai 一个强大的博主 HTML5资源教程 | 分享HTML5开发资源和开发教程 蘑菇博客 - 专注于技术分享的博客平台 个人博客-leapMie 流星007 CSDN博客 - 舍其小伙伴 稀土掘金 Go 技术论坛 | Golang / Go 语言中国知识社区
最新评论