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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
云解析 DNS,旗舰版 1个月
简介: 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函数的基本处理流程。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
5天前
|
NoSQL 网络协议 Unix
1)Redis 属于单线程还是多线程?不同版本之间有什么区别?
1)Redis 属于单线程还是多线程?不同版本之间有什么区别?
17 1
|
9天前
|
消息中间件 NoSQL Java
面试官:谈谈你对IO多路复用的理解?
面试官:谈谈你对IO多路复用的理解?
23 0
面试官:谈谈你对IO多路复用的理解?
|
11天前
|
网络协议 Java Linux
高并发编程必备知识IO多路复用技术select,poll讲解
高并发编程必备知识IO多路复用技术select,poll讲解
|
2月前
|
NoSQL Redis 数据库
Redis AOF重写问题之同一数据产生两次磁盘IO如何解决
Redis AOF重写问题之同一数据产生两次磁盘IO如何解决
Redis AOF重写问题之同一数据产生两次磁盘IO如何解决
|
3月前
|
消息中间件 缓存 NoSQL
Redis快速度特性及为什么支持多线程及应用场景
Redis快速度特性及为什么支持多线程及应用场景
77 11
|
3月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
64 1
|
3月前
|
存储 Java Unix
(八)Java网络编程之IO模型篇-内核Select、Poll、Epoll多路复用函数源码深度历险!
select/poll、epoll这些词汇相信诸位都不陌生,因为在Redis/Nginx/Netty等一些高性能技术栈的底层原理中,大家应该都见过它们的身影,接下来重点讲解这块内容。
|
3月前
|
缓存 Java 编译器
多线程内存模型问题之保证Java中的原子性,如何解决
多线程内存模型问题之保证Java中的原子性,如何解决
|
3月前
|
数据采集 算法 数据处理
Python中的并发编程:异步IO与多线程对比分析
传统的多线程编程在Python中因为全局解释器锁(GIL)的存在受到限制,导致多线程并不能充分利用多核处理器的优势。本文将探讨Python中的异步IO编程与多线程编程的差异与优劣,并分析适合的应用场景。
|
3月前
|
设计模式 存储 缓存
Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
45 0
下一篇
无影云桌面