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

简介: 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函数的基本处理流程。

目录
相关文章
|
5月前
|
存储 负载均衡 NoSQL
【赵渝强老师】Redis Cluster分布式集群
Redis Cluster是Redis的分布式存储解决方案,通过哈希槽(slot)实现数据分片,支持水平扩展,具备高可用性和负载均衡能力,适用于大规模数据场景。
407 2
|
5月前
|
存储 缓存 NoSQL
【📕分布式锁通关指南 12】源码剖析redisson如何利用Redis数据结构实现Semaphore和CountDownLatch
本文解析 Redisson 如何通过 Redis 实现分布式信号量(RSemaphore)与倒数闩(RCountDownLatch),利用 Lua 脚本与原子操作保障分布式环境下的同步控制,帮助开发者更好地理解其原理与应用。
350 6
|
6月前
|
存储 缓存 NoSQL
Redis核心数据结构与分布式锁实现详解
Redis 是高性能键值数据库,支持多种数据结构,如字符串、列表、集合、哈希、有序集合等,广泛用于缓存、消息队列和实时数据处理。本文详解其核心数据结构及分布式锁实现,帮助开发者提升系统性能与并发控制能力。
|
4月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
297 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
4月前
|
缓存 NoSQL 关系型数据库
Redis缓存和分布式锁
Redis 是一种高性能的键值存储系统,广泛用于缓存、消息队列和内存数据库。其典型应用包括缓解关系型数据库压力,通过缓存热点数据提高查询效率,支持高并发访问。此外,Redis 还可用于实现分布式锁,解决分布式系统中的资源竞争问题。文章还探讨了缓存的更新策略、缓存穿透与雪崩的解决方案,以及 Redlock 算法等关键技术。
|
6月前
|
NoSQL Redis
Lua脚本协助Redis分布式锁实现命令的原子性
利用Lua脚本确保Redis操作的原子性是分布式锁安全性的关键所在,可以大幅减少由于网络分区、客户端故障等导致的锁无法正确释放的情况,从而在分布式系统中保证数据操作的安全性和一致性。在将这些概念应用于生产环境前,建议深入理解Redis事务与Lua脚本的工作原理以及分布式锁的可能问题和解决方案。
235 8
|
7月前
|
缓存 NoSQL 算法
高并发秒杀系统实战(Redis+Lua分布式锁防超卖与库存扣减优化)
秒杀系统面临瞬时高并发、资源竞争和数据一致性挑战。传统方案如数据库锁或应用层锁存在性能瓶颈或分布式问题,而基于Redis的分布式锁与Lua脚本原子操作成为高效解决方案。通过Redis的`SETNX`实现分布式锁,结合Lua脚本完成库存扣减,确保操作原子性并大幅提升性能(QPS从120提升至8,200)。此外,分段库存策略、多级限流及服务降级机制进一步优化系统稳定性。最佳实践包括分层防控、黄金扣减法则与容灾设计,强调根据业务特性灵活组合技术手段以应对高并发场景。
1957 7
|
8月前
|
NoSQL 算法 安全
redis分布式锁在高并发场景下的方案设计与性能提升
本文探讨了Redis分布式锁在主从架构下失效的问题及其解决方案。首先通过CAP理论分析,Redis遵循AP原则,导致锁可能失效。针对此问题,提出两种解决方案:Zookeeper分布式锁(追求CP一致性)和Redlock算法(基于多个Redis实例提升可靠性)。文章还讨论了可能遇到的“坑”,如加从节点引发超卖问题、建议Redis节点数为奇数以及持久化策略对锁的影响。最后,从性能优化角度出发,介绍了减少锁粒度和分段锁的策略,并结合实际场景(如下单重复提交、支付与取消订单冲突)展示了分布式锁的应用方法。
614 3
|
3月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
188 6
|
6月前
|
Java API 微服务
为什么虚拟线程将改变Java并发编程?
为什么虚拟线程将改变Java并发编程?
326 83