百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

面试官问我redis锁怎么实现?我一口气和他说了3种方法!

mhr18 2024-10-22 12:36 27 浏览 0 评论

原创文章首发微信公众号「后端技术学堂」转载请先与我联系,点文末链接「了解更多

今天就来说说高并发编程中redis分布式锁实现,这里罗列出3种redis实现的分布式锁,并分别对比说明各自特点。


Redis单实例分布式锁

实现一:SETNX实现的分布式锁

setnx用法参考redis官方文档

语法

SETNX key value

将key设置值为value,如果key不存在,这种情况下等同SET命令。当key存在时,什么也不做。SETNX是”SET if Not eXists”的简写。

返回值:

  • 1 设置key成功
  • 0 设置key失败

加锁步骤

  1. SETNX lock.foo <current Unix time + lock timeout + 1>如果客户端获得锁,SETNX返回1,加锁成功。如果SETNX返回0,那么该键已经被其他的客户端锁定。
  2. 接上一步,SETNX返回0加锁失败,此时,调用GET lock.foo获取时间戳检查该锁是否已经过期:
  3. 若旧的时间戳已过期,则表示加锁成功。
  4. 若旧的时间戳还未过期(说明被其他客户端抢去并设置了时间戳),代表加锁失败,需要等待重试。
  5. 如果没有过期,则休眠一会重试。
  6. 如果已经过期,则可以获取该锁。具体的:调用GETSET lock.foo <current Unix timestamp + lock timeout + 1>基于当前时间设置新的过期时间。注意: 这里设置的时候因为在SETNX与GETSET之间有个窗口期,在这期间锁可能已被其他客户端抢去,所以这里需要判断GETSET的返回值,它的返回值是SET之前旧的时间戳:

解锁步骤

解锁相对简单,只需GET lock.foo时间戳,判断是否过期,过期就调用删除DEL lock.foo


实现二:SET实现的分布式锁

set用法参考官方文档

语法

SET key value [EX seconds|PX milliseconds] [NX|XX]

将键key设定为指定的“字符串”值。如果 key 已经保存了一个值,那么这个操作会直接覆盖原来的值,并且忽略原始类型。当set命令执行成功之后,之前设置的过期时间都将失效。

从2.6.12版本开始,redis为SET命令增加了一系列选项:

  • EX seconds – Set the specified expire time, in seconds.
  • PX milliseconds – Set the specified expire time, in milliseconds.
  • NX – Only set the key if it does not already exist.
  • XX – Only set the key if it already exist.
  • EX seconds – 设置键key的过期时间,单位时秒
  • PX milliseconds – 设置键key的过期时间,单位是毫秒
  • NX – 只有键key不存在的时候才会设置key的值
  • XX – 只有键key存在的时候才会设置key的值

版本>= 6.0

  • KEEPTTL -- 保持 key 之前的有效时间TTL

加锁步骤

一条命令即可加锁: SET resource_name my_random_value NX PX 30000

The command will set the key only if it does not already exist (NX option), with an expire of 30000 milliseconds (PX option). The key is set to a value “myrandomvalue”. This value must be unique across all clients and all lock requests.

这个命令只有当key 对应的键不存在resource_name时(NX选项的作用)才生效,同时设置30000毫秒的超时,成功设置其值为my_random_value,这是个在所有redis客户端加锁请求中全局唯一的随机值。

解锁步骤

解锁时需要确保my_random_value和加锁的时候一致。下面的Lua脚本可以完成

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

这段Lua脚本在执行的时候要把前面的my_random_value作为ARGV[1]的值传进去,把resource_name作为KEYS[1]的值传进去。释放锁其实包含三步操作:’GET’、判断和’DEL’,用Lua脚本来实现能保证这三步的原子性。



Redis集群分布式锁


实现三:Redlock

前面两种分布式锁的实现都是针对单redis master实例,既不是有互为备份的slave节点也不是多master集群,如果是redis集群,每个redis master节点都是独立存储,这种场景用前面两种加锁策略有锁的安全性问题。

比如下面这种场景:

客户端1从Master获取了锁。

Master宕机了,存储锁的key还没有来得及同步到Slave上。

Slave升级为Master。

客户端2从新的Master获取到了对应同一个资源的锁。

于是,客户端1和客户端2同时持有了同一个资源的锁。锁的安全性被打破。

针对这种多redis服务实例的场景,redis作者antirez设计了Redlock (Distributed locks with Redis)算法,就是我们接下来介绍的。

加锁步骤

集群加锁的总体思想是尝试锁住所有节点,当有一半以上节点被锁住就代表加锁成功。集群部署你的数据可能保存在任何一个redis服务节点上,一旦加锁必须确保集群内任意节点被锁住,否则也就失去了加锁的意义。

具体的:

  1. 获取当前时间(毫秒数)。
  2. 按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。
  3. 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。
  4. 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。
  5. 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的Redis Lua脚本)。



我们接着讲

解锁步骤

客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。

算法实现

上面描述的算法已经有现成的实现,各种语言版本。

  • Redlock-rb (Ruby implementation). There is also a fork of Redlock-rb that adds a gem for easy distribution and perhaps more.
  • Redlock-py (Python implementation).
  • Aioredlock (Asyncio Python implementation).
  • Redlock-php (PHP implementation).
  • PHPRedisMutex (further PHP implementation)
  • cheprasov/php-redis-lock (PHP library for locks)
  • Redsync (Go implementation).
  • Redisson (Java implementation).
  • Redis::DistLock (Perl implementation).
  • Redlock-cpp (C++ implementation).
  • Redlock-cs (C#/.NET implementation).
  • RedLock.net (C#/.NET implementation). Includes async and lock extension support.
  • ScarletLock (C# .NET implementation with configurable datastore)
  • Redlock4Net (C# .NET implementation)
  • node-redlock (NodeJS implementation). Includes support for lock extension.

比如我用的C++实现

源码在这 https://github.com/jacket-code/redlock-cpp

创建分布式锁管理类CRedLock

CRedLock * dlm = new CRedLock();
dlm->AddServerUrl("127.0.0.1", 5005);
dlm->AddServerUrl("127.0.0.1", 5006);
dlm->AddServerUrl("127.0.0.1", 5007);

加锁并设置超时时间

CLock my_lock;
bool flag = dlm->Lock("my_resource_name", 1000, my_lock);

加锁并保持直到释放

CLock my_lock;
bool flag = dlm->ContinueLock("my_resource_name", 1000, my_lock);

my_resource_name是加锁标识;1000是锁的有效期,单位毫秒。

加锁失败返回false, 加锁成功返回Lock结构如下

class CLock {
public:
   int m_validityTime; => 9897.3020019531 // 当前锁可以存活的时间, 毫秒
   sds m_resource; => my_resource_name // 要锁住的资源名称
   sds m_val; => 53771bfa1e775 // 锁住资源的进程随机名字
};

解锁

dlm->Unlock(my_lock);


总结

综上所述,三种实现方式。

  • 单redis实例场景,分布式锁实现一和实现二都可以,实现二更简洁推荐用实现二,用实现三也可以,但是实现三对单实例场景有点复杂略显笨重。
  • 多redis实例场景推荐用实现三最安全,不过实现三也不是完美无瑕,也有针对这种算法缺陷的讨论(节点宕机同步时延、时间同步假设),大家还需要根据自身业务场景灵活选择或定制实现自己的分布式锁。

参考

https://redis.io/topics/distlock

https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

http://zhangtielei.com/posts/blog-redlock-reasoning.html

创作不易,点赞关注支持一下吧

我会持续分享软件编程和程序员那些事,欢迎关注。若你对编程感兴趣,我整理了这些年学习编程的各种资源,关注公众号「后端技术学堂」发送「资源」分享给你,点下方「了解更多」链接。

相关推荐

如何通过 Redis 日志排查连接超时问题

Redis是一种高性能的内存数据存储服务,但在高并发或误配置情况下,可能会出现连接超时问题。借助Redis日志,可以快速定位并解决连接超时的根本原因。以下是具体的排查和解决步骤:1.什么是R...

给你1亿的Redis key,如何高效统计?

前言有些小伙伴在工作中,可能遇到过这样的场景:老板突然要求统计Redis中所有key的数量,你随手执行了KEYS*命令,下一秒监控告警疯狂闪烁——整个Redis集群彻底卡死,线上服务大面积瘫痪。今天...

Redis分布式锁的安全性分析与实践指南

一、Redis分布式锁的核心原理Redis分布式锁通过SETNX(SetifNotExists)和EXPIRE(Expire)指令实现原子性操作,结合UUID生成唯一标识符,确保锁的互斥性和安全...

高可用Redis分布式锁:秒杀系统中的锁战

引言在分布式系统中,“程序猿的终极武器是并发控制”。当多个服务实例同时访问共享资源时,如何避免数据不一致和重复操作?答案是分布式锁。Redis凭借其高性能和原子性操作,成为实现分布式锁的首选方案。...

Redis分布式锁(redis分布式锁解决超卖)

场景描述简单模拟一个高并发库存扣减场景,商品库存加载到Redis缓存,如:127.0.0.1:6379>setproduct:stock:101200无锁状态操作从缓存中获取对应商品的库存...

Redis 分布式锁和 ZooKeeper分布式锁

Redis分布式锁和ZooKeeper(简称zk)分布式锁都是用来解决在分布式系统中多个节点之间竞争资源的问题。它们各自有不同的特点和适用场景。Redis分布式锁Redis实现分布式锁主要是...

Redis vs ZooKeeper锁:高并发下的生死对决,谁才是最终赢家?

在分布式系统中,锁是控制资源访问的重要机制。Redis和ZooKeeper作为两种主流的分布式锁实现方案,各有优劣。本文将从原理、性能、代码实现三个维度进行硬核对比,助你做出最佳技术选型。一、原理对比...

说说Redis的大key(redis key大小限制)

一句话总结Redis大key指存储超大值(如字符串过大、集合元素过多)的键。主要成因包括:1.设计不合理,未拆分数据结构;2.业务需求(如缓存整页数据);3.数据持续积累未清理;4.使用不当的集合类型...

PHP Laravel框架底层机制(php框架的底层原理)

当然可以,Laravel是最受欢迎的PHP框架之一,以优雅的语法和丰富的生态而闻名。尽管开发体验非常“高端”,它的底层其实是由一系列结构清晰、职责分明的组件构成的。下面我从整体架构、核心流程、...

PHP性能全面优化-值得收藏(php优化网站性能)

PHP项目卡顿频发,老技巧失灵?隐藏漏洞竟在代码循环里。上周公司服务器突然开始卡顿,测试发现用户请求响应时间翻倍。我们先按以前学的方法做了基准测试,用AB工具压测时发现2000并发就有5%错误,换成S...

PHP+UniApp:低成本打造外卖系统横扫App+小程序+H5全平台

在餐饮行业数字化转型中,外卖系统开发常面临两大痛点:高昂的开发成本(需独立开发App、小程序、H5)和多端维护的复杂性。PHP+UniApp的组合通过技术复用与跨平台能力,为中小商家和开发者提供了“降...

从需求到上线:PHP+Uniapp校园圈子系统源码的架构设计与性能优化

一、需求分析与架构设计1.核心功能需求用户体系:支持手机号/微信登录、多角色权限(学生、教师、管理员)。圈子管理:支持创建/加入兴趣圈子(如学术、电竞)、标签分类、动态发布与审核。实时互动:点赞、评...

PHP 8.0性能翻3倍?四年亲测:这些项目升了哭晕!

2020年那个感恩节,当PHP8.0带着“性能翻倍”的豪言横空出世时,无数程序员连夜备份代码准备升级。四年过去了,那些宣称“性能提升3倍”的项目,真的跑出火箭速度了吗?还记得当时铺天盖地的宣传吗?“...

我把 Mac mini 托管到机房了:一套打败云服务器的终极方案

本内容来源于@什么值得买APP,观点仅代表作者本人|作者:薯仔不爱吃薯仔我把我积灰的Macmini托管到机房了,有图有真相。虽然画质又渣又昏暗,但是!这就是实锤。作为开发者,谁不想拥有个自己的服...

从phpstudy到Docker:我用一个下午让开发效率翻倍的实战指南

一、为什么放弃phpstudy?上周三下午,我花了3小时将本地开发环境从phpstudy迁移到Docker,没想到第二天团队反馈:环境部署时间从2小时压缩到5分钟,跨设备协作bug减少70%。作为一个...

取消回复欢迎 发表评论: