Redis的IO多路复用和多线程特性会破坏分布式锁的原子性吗?(上)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: Redis的IO多路复用和多线程特性会破坏分布式锁的原子性吗?

1 为什么使用分布式锁?

当有多个客户端并发访问某个共享资源时,比如要修改DB某条记录,为避免记录修改冲突,可将所有客户端从Redis获取分布式锁,拿到锁的客户端才能操作共享资源。


分布式锁实现的关键就是保证加锁、解锁都是原子操作,才能保证多个客户端访问时锁的正确性。而Redis能通过事件驱动框架同时捕获多个客户端的可读事件(命令请求)。在Redis 6.x,还会有多个I/O线程并发读取或写回数据。


那事到如今,分布式锁的原子性,还能被保证吗?


那就得研究一条命令在Redis Server的执行过程,同时看看有I/O多路复用和多I/O线程情况下,分布式锁的原子性是否会被影响。

2 实现分布式锁

分布式锁的加锁操作使用 Redis的SET命令,其提供如下可选参数:

  1. NX
  • 当操作的K不存在时,Redis会直接创建
  • 当操作的K已存在,则返回NULL,Redis对K也不会做任何修改
  1. EX:设置K的过期时间


可让客户端发送如下命令执行加锁:

  • lockKey,锁的名称
  • uid,客户端用于唯一标记自己的ID(优化后的雪花算法)
  • expireTime,该K所代表的锁的过期时间,当这过期时间到达后,该K会被删除,相当于释放锁,这就避免锁一直无法释放问题(当客户端所在机器宕机时)。
SET lockKey uid EX expireTime NX

加锁

而若还没客户端创建过锁,假设客户端A发送了这个SET命令给Redis:

SET stockLock 1033 EX 30 NX

Redis就会创建对应K=stockLock,V=客户端的ID 1033。此时,假设另一客户端B也发了SET,要把K=stockLock对应的V改为客户端B的ID 2033,即加锁。

SET stockLock 2033 EX 30 NX

由于NX参数,若stockLock的K已存在,客户端B就无法对其进行修改,即无法获得锁,这就实现了加锁效果。

解锁

使用Lua脚本完成,会以EVAL命令形式在Redis Server执行。客户端会使用GET命令读取锁对应K的V,并判断V是否等于客户端自身ID:

  • 若相等,表明当前客户端正拿着锁
    此时可执行DEL命令删除K,即释放锁
  • 若value不等于客户端自身ID
    则该脚本会直接返回。
if redis.call("get",lockKey) == uid then
   return redis.call("del",lockKey)
else
   return 0
end

这样客户端就不会误删除别的客户端获得的锁,保证了锁的安全性。


无论是加锁的SET命令,还是解锁的Lua脚本和EVAL命令,在I/O多路复用下会被同时执行吗?或者当使用多I/O线程后,会被多个线程同时执行吗?即I/O多路复用引入的多个并发客户端及多I/O线程是否会破坏命令的原子性。


这就和Redis中命令的执行过程有关。

3 一条命令在Redis是如何完成执行的?

Redis Server一旦和某一客户端建立连接后,就会在事件驱动框架中注册可读事件,对应客户端的命令请求。整个命令处理的过程可分为如下阶段:

  • 命令解析,对应processInputBufferAndReplicate
  • 命令执行,对应processCommand
  • 结果返回,对应addReply

3.1 命令读取阶段:readQueryFromClient函数

会从客户端连接的socket中,读取最大为readlen长度的数据,readlen大小为宏定义PROTO_IOBUF_LEN,默认16KB。

image.png

image.png

接着根据读取数据的情况,进行异常处理,如:

  • 数据读取失败
  • 或客户端连接关闭等


若当前客户端是主从复制中的主节点,readQueryFromClient会把读取的数据,追加到用于主从节点命令同步的缓冲区中。

最后,调用processInputBuffer,进入命令解析阶段。

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
   ...
   readlen = PROTO_IOBUF_LEN;  //从客户端socket中读取的数据长度,默认为16KB
   ...
   c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);  //给缓冲区分配空间
   nread = read(fd, c->querybuf+qblen, readlen);  //调用read从描述符为fd的客户端socket中读取数据
    ...
    processInputBufferAndReplicate(c);  //调用processInputBufferAndReplicate进一步处理读取内容
}

该函数的基本流程:

1.png

3.2 命令解析:processInputBuffer函数

根据当前客户端是否有CLIENT_MASTER标记,执行如下分支:

  • Case1

对应客户端无CLIENT_MASTER标记,即当前客户端不属于主从复制中的Master。那processInputBufferAndReplicate函数会直接调用processInputBuffer(在networking.c文件中)函数,对客户端输入缓冲区中的命令和参数进行解析。所以在这里,实际执行命令解析的函数就是processInputBuffer函数。我们一会儿来具体看下这个函数。

  • Case2

对应客户端有CLIENT_MASTER标记,即当前客户端属于主从复制中的Master。processInputBufferAndReplicate除了会调用processInputBuffer函数,解析客户端命令,还会调用replicationFeedSlavesFromMasterStream函数,将主节点接收到的命令同步给从节点。

1.png

最终命令解析实际是在processInputBuffer执行的

首先,processInputBuffer函数会执行一个while循环,不断地从客户端的输入缓冲区中读取数据。然后,它会判断读取到的命令格式,是否以“*”开头

  • 若命令以*开头,表明该命令是 PROTO_REQ_MULTIBULK 类型的请求,即符合RESP协议(Redis客户端与服务器端的标准通信协议)的请求。processInputBuffer会进一步调用processMultibulkBuffer解析读取到的命令
  • 不是以*开头,说明该命令是PROTO_REQ_INLINE类型的请求,并非RESP协议请求。这类命令也被称为管道命令,命令和命令间用换行符\r\n分隔的。如使用Telnet发给Redis的命令就属该类型命令。此时,processInputBuffer会调用processInlineBuffer解析命令。

image.png

当命令解析完成后,processInputBuffer就会调用processCommand,开始进入命令处理的第三个阶段,也就是命令执行阶段。

processInputBuffer函数的基本执行流程:

1.jpg

好,那么下面,我们接着来看第三个阶段,也就是命令执行阶段的processCommand函数的基本处理流程。

目录
相关文章
|
3月前
|
存储 负载均衡 NoSQL
【赵渝强老师】Redis Cluster分布式集群
Redis Cluster是Redis的分布式存储解决方案,通过哈希槽(slot)实现数据分片,支持水平扩展,具备高可用性和负载均衡能力,适用于大规模数据场景。
321 2
|
3月前
|
存储 缓存 NoSQL
【📕分布式锁通关指南 12】源码剖析redisson如何利用Redis数据结构实现Semaphore和CountDownLatch
本文解析 Redisson 如何通过 Redis 实现分布式信号量(RSemaphore)与倒数闩(RCountDownLatch),利用 Lua 脚本与原子操作保障分布式环境下的同步控制,帮助开发者更好地理解其原理与应用。
255 6
|
4月前
|
存储 缓存 NoSQL
Redis核心数据结构与分布式锁实现详解
Redis 是高性能键值数据库,支持多种数据结构,如字符串、列表、集合、哈希、有序集合等,广泛用于缓存、消息队列和实时数据处理。本文详解其核心数据结构及分布式锁实现,帮助开发者提升系统性能与并发控制能力。
|
2月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
206 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
2月前
|
缓存 NoSQL 关系型数据库
Redis缓存和分布式锁
Redis 是一种高性能的键值存储系统,广泛用于缓存、消息队列和内存数据库。其典型应用包括缓解关系型数据库压力,通过缓存热点数据提高查询效率,支持高并发访问。此外,Redis 还可用于实现分布式锁,解决分布式系统中的资源竞争问题。文章还探讨了缓存的更新策略、缓存穿透与雪崩的解决方案,以及 Redlock 算法等关键技术。
|
4月前
|
NoSQL Redis
Lua脚本协助Redis分布式锁实现命令的原子性
利用Lua脚本确保Redis操作的原子性是分布式锁安全性的关键所在,可以大幅减少由于网络分区、客户端故障等导致的锁无法正确释放的情况,从而在分布式系统中保证数据操作的安全性和一致性。在将这些概念应用于生产环境前,建议深入理解Redis事务与Lua脚本的工作原理以及分布式锁的可能问题和解决方案。
203 8
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
1月前
|
Java Unix Go
【Java】(8)Stream流、文件File相关操作,IO的含义与运用
Java 为 I/O 提供了强大的而灵活的支持,使其更广泛地应用到文件传输和网络编程中。!但本节讲述最基本的和流与 I/O 相关的功能。我们将通过一个个例子来学习这些功能。
164 1
|
Java 大数据
解析Java中的NIO与传统IO的区别与应用
解析Java中的NIO与传统IO的区别与应用
|
Java 大数据 API
Java 流(Stream)、文件(File)和IO的区别
Java中的流(Stream)、文件(File)和输入/输出(I/O)是处理数据的关键概念。`File`类用于基本文件操作,如创建、删除和检查文件;流则提供了数据读写的抽象机制,适用于文件、内存和网络等多种数据源;I/O涵盖更广泛的输入输出操作,包括文件I/O、网络通信等,并支持异常处理和缓冲等功能。实际开发中,这三者常结合使用,以实现高效的数据处理。例如,`File`用于管理文件路径,`Stream`用于读写数据,I/O则处理复杂的输入输出需求。
694 12

热门文章

最新文章