使用过 Redis 分布式锁么,它是什么回事?
wptr33 2025-04-09 21:25 12 浏览
我使用过 Redis 分布式锁。让我来详细解释一下它是什么回事。
首先,什么是分布式锁?
在单体应用中,我们通常使用编程语言提供的锁机制(例如 Java 中的 synchronized 关键字或 ReentrantLock)来控制多线程对共享资源的并发访问,防止数据错乱或资源竞争。
但是,在分布式系统中,应用被部署在多台服务器上,同一个应用的不同实例可能同时运行并尝试访问同一个共享资源(例如数据库中的同一行数据、共享文件、队列等)。 单机锁无法跨进程、跨机器工作,因此我们需要一种分布式锁来协调不同服务器上的进程对共享资源的访问。
Redis 分布式锁的原理和实现
Redis 分布式锁的核心思想是利用 Redis 的原子操作来实现锁的获取和释放。 通常使用以下几个关键 Redis 命令和特性:
- SETNX (SET if Not eXists) 命令:
- 这是实现 Redis 分布式锁最常用的命令。
- SETNX key value 命令的作用是:当且仅当 key 不存在时,将 key 的值设置为 value,并返回 1;如果 key 已经存在,则不做任何操作,并返回 0。
- 这个原子性操作是锁的关键:多个客户端同时尝试 SETNX 同一个 key,只有一个客户端能够成功(返回 1),表示它成功获取了锁。其他客户端会失败(返回 0),表示锁已被占用。
- 设置锁的过期时间 (EXPIRE 命令):
- 为了防止死锁,我们需要为锁设置一个过期时间。
- 如果持有锁的客户端在执行业务逻辑过程中发生崩溃或者网络问题,未能及时释放锁,锁会因为过期时间到期而被 Redis 自动删除,从而避免其他客户端永远无法获取锁的情况。
- 使用 EXPIRE key seconds 命令可以为指定的 key 设置过期时间,单位是秒。
- 释放锁 (DEL 命令 或 Lua 脚本):
- 当持有锁的客户端完成业务逻辑后,需要释放锁,以便其他客户端可以获取锁。
- 最简单的释放锁的方式是使用 DEL key 命令删除锁的 key。
一个简单的 Redis 分布式锁实现示例 (伪代码):
python复制代码import redis
import uuid
import time
class RedisDistributedLock:
def __init__(self, redis_client, lock_name, expire_time=30):
self.redis_client = redis_client
self.lock_name = lock_name
self.expire_time = expire_time
self.lock_key = "lock:" + lock_name
self.lock_value = str(uuid.uuid4()) # 使用 UUID 作为锁的值,确保唯一性
def acquire_lock(self):
while True:
acquired = self.redis_client.setnx(self.lock_key, self.lock_value)
if acquired:
self.redis_client.expire(self.lock_key, self.expire_time)
return True # 获取锁成功
else:
# 锁已被占用,等待一段时间后重试
time.sleep(0.1)
return False # 理论上不会执行到这里
def release_lock(self):
# 释放锁时,需要验证锁是否是自己持有的,防止误删其他客户端的锁
if self.redis_client.get(self.lock_key) == self.lock_value.encode(): # 注意 get 返回的是 bytes
self.redis_client.delete(self.lock_key)
return True
return False
使用示例:
python复制代码redis_client = redis.Redis(host='localhost', port=6379, db=0)
lock = RedisDistributedLock(redis_client, "my_resource_lock")
if lock.acquire_lock():
try:
print("成功获取锁,执行业务逻辑...")
time.sleep(5) # 模拟业务逻辑执行
finally:
if lock.release_lock():
print("成功释放锁")
else:
print("释放锁失败")
else:
print("获取锁失败,资源已被占用")
Redis 分布式锁的优点:
- 高性能: Redis 是内存数据库,读写速度非常快,因此分布式锁的获取和释放操作非常高效。
- 简单易用: 基于 Redis 的命令实现,代码相对简单,容易理解和实现。
- 可靠性: Redis 本身具有持久化和复制机制,可以保证一定的可靠性(但需要注意单点故障问题,后面会提到)。
Redis 分布式锁的挑战和需要注意的问题:
- 误删锁问题:
- 上面的简单示例中,释放锁时只是简单地 DEL key。 如果持有锁的客户端执行业务逻辑的时间超过了锁的过期时间,锁会被 Redis 自动释放。
- 此时,如果另一个客户端获取了锁并开始执行业务逻辑,而之前的客户端仍然在执行,并且在执行完成后尝试释放锁,它可能会错误地删除新客户端持有的锁,导致并发问题。
- 解决方案:释放锁时验证锁的持有者: 在 SETNX 时,我们将一个唯一的 value (例如 UUID) 写入锁的 key。 释放锁时,先 GET 锁的 key 的值,判断是否是自己设置的 value,如果是才执行 DEL 操作。 上面的示例代码中已经加入了这个验证。使用 Lua 脚本原子性地验证和删除: 为了保证验证和删除操作的原子性,可以使用 Lua 脚本将 GET 和 DEL 操作放在一个原子操作中执行。 这是更推荐的做法,可以避免在并发情况下出现问题。
- lua复制代码
- -- Lua 脚本,用于原子性地验证和删除锁 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
- 在 Python 中使用 Lua 脚本释放锁:
- python复制代码
- def release_lock(self): release_script = """ if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end """ result = self.redis_client.eval(release_script, 1, self.lock_key, self.lock_value) return result == 1
- 锁的续期 (Watchdog):
- 如果业务逻辑执行时间无法预估,或者可能超过锁的过期时间,为了防止锁被意外释放,可以实现锁的续期机制(也称为 Watchdog)。
- 客户端在持有锁期间,启动一个后台线程或定时任务,定期检查锁的剩余过期时间,如果剩余时间过短,则自动延长锁的过期时间。
- 很多 Redis 分布式锁的客户端库(例如 Redisson)都提供了 Watchdog 机制。
- Redis 单点故障问题:
- 如果 Redis 服务本身发生故障(例如宕机),那么所有的锁都会失效,可能会导致并发问题。
- 解决方案:Redis Sentinel 或 Redis Cluster: 使用 Redis Sentinel 或 Redis Cluster 集群模式,提高 Redis 的可用性和容错性。 当主节点故障时,Sentinel 或 Cluster 会自动进行故障转移,将从节点提升为主节点,保证 Redis 服务的持续可用。Redlock (红锁): Redlock 是一种更复杂的分布式锁算法,它使用多个独立的 Redis 实例来获取锁,只有当在大多数 Redis 实例上都成功获取锁时,才认为获取锁成功。 Redlock 可以提高分布式锁的可靠性,但实现和维护也更复杂,性能也会有所下降。 通常情况下,使用 Redis Sentinel 或 Cluster 已经可以满足大部分场景的需求,Redlock 一般在对数据一致性要求极高的场景下才考虑使用。
- 时钟漂移问题:
- 在分布式系统中,不同服务器的时钟可能存在轻微的偏差(时钟漂移)。 如果时钟漂移严重,可能会影响锁的过期时间判断,导致锁提前过期或延迟过期。
- 解决方案:尽量保证服务器时钟的同步,可以使用 NTP 服务进行时钟同步。在设置锁的过期时间时,可以适当留一些余量,避免因为轻微的时钟漂移导致锁提前过期。
- 网络分区问题:
- 在分布式系统中,网络分区是不可避免的。 如果发生网络分区,导致客户端与 Redis 集群的一部分节点断开连接,可能会出现 “脑裂” 情况,导致多个客户端同时认为自己持有锁。
- Redlock 在一定程度上可以缓解网络分区问题,但并不能完全解决。 在设计分布式系统时,需要考虑网络分区的影响,并根据业务场景选择合适的分布式锁方案和容错机制。
总结
Redis 分布式锁是一种常用的实现分布式系统互斥访问共享资源的方案。它基于 Redis 的原子操作和过期时间机制,具有高性能、简单易用的优点。 但在使用 Redis 分布式锁时,需要注意误删锁、锁续期、Redis 单点故障、时钟漂移和网络分区等问题,并根据实际场景选择合适的解决方案。
在实际项目中,为了简化开发和提高可靠性,通常建议使用成熟的 Redis 分布式锁客户端库,例如:
- Redisson (Java): 功能非常强大,提供了各种分布式锁的实现,包括可重入锁、公平锁、读写锁、Redlock 等,并内置了 Watchdog 机制。
- Lettuce (Java): 高性能的 Redis 客户端,也提供了分布式锁的实现。
- redis-py (Python): Python 的 Redis 客户端,可以结合 Lua 脚本实现分布式锁。
- ioredis (Node.js): Node.js 的 Redis 客户端,也提供了分布式锁的实现。
这些客户端库通常会处理一些细节问题,例如锁的续期、重试机制、错误处理等,可以减少开发工作量,并提高分布式锁的可靠性。
相关推荐
- 每天一个编程技巧!掌握这7个神技,代码效率飙升200%
-
“同事6点下班,你却为改BUG加班到凌晨?不是你不努力,而是没掌握‘偷懒’的艺术!本文揭秘谷歌工程师私藏的7个编程神技,每天1分钟,让你的代码从‘能用’变‘逆天’。文末附《Python高效代码模板》,...
- Git重置到某个历史节点(Sourcetree工具)
-
前言Sourcetree回滚提交和重置当前分支到此次提交的区别?回滚提交是指将改动的代码提交到本地仓库,但未推送到远端仓库的时候。...
- git工作区、暂存区、本地仓库、远程仓库的区别和联系
-
很多程序员天天写代码,提交代码,拉取代码,对git操作非常熟练,但是对git的原理并不甚了解,借助豆包AI,写个文章总结一下。Git的四个核心区域(工作区、暂存区、本地仓库、远程仓库)是版本控制的核...
- 解锁人生新剧本的密钥:学会让往事退场
-
开篇:敦煌莫高窟的千年启示在莫高窟321窟的《降魔变》壁画前,讲解员指着斑驳色彩说:"画师刻意保留了历代修补痕迹,因为真正的传承不是定格,而是流动。"就像我们的人生剧本,精彩章节永远...
- Reset local repository branch to be just like remote repository HEAD
-
技术背景在使用Git进行版本控制时,有时会遇到本地分支与远程分支不一致的情况。可能是因为误操作、多人协作时远程分支被更新等原因。这时就需要将本地分支重置为与远程分支的...
- Git恢复至之前版本(git恢复到pull之前的版本)
-
让程序回到提交前的样子:两种解决方法:回退(reset)、反做(revert)方法一:gitreset...
- 如何将文件重置或回退到特定版本(怎么让文件回到初始状态)
-
技术背景在使用Git进行版本控制时,经常会遇到需要将文件回退到特定版本的情况。可能是因为当前版本出现了错误,或者想要恢复到之前某个稳定的版本。Git提供了多种方式来实现这一需求。...
- git如何正确回滚代码(git命令回滚代码)
-
方法一,删除远程分支再提交①首先两步保证当前工作区是干净的,并且和远程分支代码一致$gitcocurrentBranch$gitpullorigincurrentBranch$gi...
- [git]撤销的相关命令:reset、revert、checkout
-
基本概念如果不清晰上面的四个概念,请查看廖老师的git教程这里我多说几句:最开始我使用git的时候,我并不明白我为什么写完代码要用git的一些列指令把我的修改存起来。后来用多了,也就明白了为什么。gi...
- 利用shell脚本将Mysql错误日志保存到数据库中
-
说明:利用shell脚本将MYSQL的错误日志提取并保存到数据库中步骤:1)创建数据库,创建表CreatedatabaseMysqlCenter;UseMysqlCenter;CREATET...
- MySQL 9.3 引入增强的JavaScript支持
-
MySQL,这一广泛采用的开源关系型数据库管理系统(RDBMS),发布了其9.x系列的第三个更新版本——9.3版,带来了多项新功能。...
- python 连接 mysql 数据库(python连接MySQL数据库案例)
-
用PyMySQL包来连接Python和MySQL。在使用前需要先通过pip来安装PyMySQL包:在windows系统中打开cmd,输入pipinstallPyMySQL ...
- mysql导入导出命令(mysql 导入命令)
-
mysql导入导出命令mysqldump命令的输入是在bin目录下.1.导出整个数据库 mysqldump-u用户名-p数据库名>导出的文件名 mysqldump-uw...
- MySQL-SQL介绍(mysql sqlyog)
-
介绍结构化查询语言是高级的非过程化编程语言,允许用户在高层数据结构上工作。它不要求用户指定对数据的存放方法,也不需要用户了解具体的数据存放方式,所以具有完全不同底层结构的不同数据库系统,可以使用相同...
- MySQL 误删除数据恢复全攻略:基于 Binlog 的实战指南
-
在MySQL的世界里,二进制日志(Binlog)就是我们的"时光机"。它默默记录着数据库的每一个重要变更,就像一位忠实的史官,为我们在数据灾难中提供最后的救命稻草。本文将带您深入掌握如...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
因果推断Matching方式实现代码 因果推断模型
-
git pull命令使用实例 git pull--rebase
-
面试官:git pull是哪两个指令的组合?
-
git 执行pull错误如何撤销 git pull fail
-
git pull 和git fetch 命令分别有什么作用?二者有什么区别?
-
git fetch 和git pull 的异同 git中fetch和pull的区别
-
git pull 之后本地代码被覆盖 解决方案
-
还可以这样玩?Git基本原理及各种骚操作,涨知识了
-
git命令之pull git.pull
-
- 最近发表
-
- 每天一个编程技巧!掌握这7个神技,代码效率飙升200%
- Git重置到某个历史节点(Sourcetree工具)
- git工作区、暂存区、本地仓库、远程仓库的区别和联系
- 解锁人生新剧本的密钥:学会让往事退场
- Reset local repository branch to be just like remote repository HEAD
- Git恢复至之前版本(git恢复到pull之前的版本)
- 如何将文件重置或回退到特定版本(怎么让文件回到初始状态)
- git如何正确回滚代码(git命令回滚代码)
- [git]撤销的相关命令:reset、revert、checkout
- 利用shell脚本将Mysql错误日志保存到数据库中
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)