本篇文章为你解答一下问题:
  • 0:redis单线程的实现流程是怎样的?
  • 1:redis哪些地方用到了多线程,哪些地方是单线程?
  • 2:redis多线程是怎么实现的?
  • 3:redis多线程是怎么做到无锁的?
 
0:redis单线程的实现流程是怎样的?
Redis一开始是单线程模型,在一个线程中要同时处理两种事件:文件事件和时间事件
文件事件主要是网络I/O的读写,请求的接收和回复
时间事件就是单次/多次执行的定时器,如主从复制、定时删除过期数据、字典rehash等
redis所有核心功能都是跑在主线程中的,像aof文件落盘操作是在子线程中执行的,那么在高并发情况下它是怎么做到高性能的呢?
由于这两种事件在同一个线程中执行,就会出现互相影响的问题,如时间事件到了还在等待/执行文件事件,或者文件事件已经就绪却在执行时间事件,这就是单线程的缺点,所以在实现上要将这些影响降到最低。那么redis是怎么实现的呢?
 
定时执行的时间事件保存在一个链表中,由于链表中任务没有按照执行时间排序,所以每次需要扫描单链表,找到最近需要执行的任务,时间复杂度是O(N),redis敢这么实现就是因为这个链表很短,大部分定时任务都是在serverCron方法中被调用。从现在开始到最近需要执行的任务的开始时间,时长定位T,这段时间就是属于文件事件的处理时间,以epoll为例,执行epoll_wait最多等待的时长为T,如果有就绪任务epoll会返回所有就绪的网络任务,存在一个数组中,这时我们知道了所有就绪的socket和对应的事件(读、写、错误、挂断),然后就可以接收数据,解析,执行对应的命令函数。
如果最近要执行的定时任务时间已经过了,那么epoll就不会阻塞,直接返回已经就绪的网络事件,即不等待。
总之单线程,定时事件和网络事件还是会互相影响的,正在处理定时事件网络任务来了,正在处理网络事件定时任务的时间到了。所以redis必须保证每个任务的处理时间不能太长。
 
redis处理流程如下:
1:服务启动,开始网络端口监听,等待客户端请求
2:客户端想服务端发起连接请求,创建客户端连接对象,完成连接
3:将socket信息注册到epoll,设置超时时间为时间事件的周期时长,等待客户端发起请求
4:客户端发起操作数据库请求(如GET)
5:epoll收到客户端的请求,可能多个,按照顺序处理请求
6:接收请求参数,接收完成后解析请求协议,得到请求命令
7:执行请求命令,即操作redis数据库
8:将结果返回给客户端
 
1:redis哪些地方用到了多线程,哪些地方是单线程?
Redis多线程和单线程模型对比如下图:
 
从上图中可以看出只有以下3个地方用的是多线程,其他地方都是单线程:
1:接收请求参数
2:解析请求参数
3:请求响应,即将结果返回给client
很明显以上3点各个请求都是互相独立互不影响的,很适合用多线程,特别是请求体/响应体很大的时候,更能体现多线程的威力。而操作数据库是请求之间共享的,如果使用多线程的话适合读写锁。而操作数据库本身是很快的(就是对map的增删改查),单线程不一定就比多线程慢,当然也有可能是作者偷懒,懒得实现罢了,但这次的多线程模型还是值得我们学习一下的。
 
2:redis多线程是怎么实现的?
先大致说一下多线程的流程:
1:服务器启动时启动一定数量线程,服务启动的时候可以指定线程数,每个线程对应一个队列(list *io_threads_list[128]),最多128个线程。
2:服务器收到的每个请求都会放入全局读队列clients_pending_read,同时将队列中的元素分发到每个线程对应的队列io_threads_list中,这些工作都是在主线程中执行的。
3:每个线程(包括主线程和子线程)接收请求参数并做解析,完事后在client中设置一个标记CLIENT_PENDING_READ,标识参数解析完成,可以操作数据库了。(主线程和子线程都会执行这个步骤)
4:主线程遍历队列clients_pending_read,发现设有CLIENT_PENDING_READ标记的,就操作数据库
5:操作完数据库就是响应client了,响应是一组函数addReplyXXX,在client中设置标记CLIENT_PENDING_WRITE,同时将client加入全局写队列clients_pending_write
6:主线程将全局队列clients_pending_write以轮训的方式将任务分发到每个线程对应的队列io_threads_list
7:所有线程将遍历自己的队列io_threads_list,将结果发送给client
 
3:redis多线程是怎么做到无锁的?
上面说了多线程的地方都是互相独立互不影响的。但是每个线程的队列就存在两个两个线程访问的情况:主线程向队列中写数据,子线程消费,redis的实现有点反直觉。按正常思路来说,主线程在往队列中写数据的时候加锁;子线程复制队列&并将队列清空,这个两个动作是加锁的,子线程消费复制后的队列,这个过程是不需要加锁的,按理来说主线程和子线程的加锁动作都是非常快的。但是redis并没有这么实现,那么他是怎么实现的呢?
 
redis多线程的模型是主线程负责搜集任务,放入全局读队列clients_pending_read和全局写队列clients_pending_write,主线程在将队列中的任务以轮训的方式分发到每个线程对应的队列(list *io_threads_list[128])
1:一开始子线程的队列都是空,主线程将全对队列中的任务分发到每个线程的队列,并设置一个队列有数据的标记(_Atomic unsigned long io_threads_pending[128]),io_threads_pending[1]=5表示第一个线程的队列中有5个元素
2:子线程死循环轮训检查io_threads_pending[index] > 0,有数据就开始处理,处理完成之后将io_threads_pending[index] = 0,没数据继续检查
3:主线程将任务分发到子线程的队列中,自己处理自己队列中的任务,处理完成后,等待所有子线程处理完所有任务,继续收集任务到全局队列,在将任务分发给子线程,这样就避免了主线程和子线程同时访问队列的情况,就必须加锁了。因为任务是平均分配到每个队列的,所以每个队列的处理时间是接近的,等待的时间会很短。 
 
4:源码执行流程
为了方便你看源码,这里加上一些代码的执行流程
启动socket监听,注册连接处理函数,连接成功后创建连接对象connection,创建client对象,通过aeCreateFileEvent注册client的读事件
main -> initServer -> acceptTcpHandler -> anetTcpAccept -> anetGenericAccept -> accept(获取到socket连接句柄)
connCreateAcceptedSocket -> connCreateSocket -> 创建一个connection对象
acceptCommonHandler -> createClient创建client连接对象 -> connSetReadHandler -> aeCreateFileEvent -> readQueryFromClient main -> aeMain -> aeProcessEvents -> aeApiPoll(获取可读写的socket) -> readQueryFromClient(如果可读) -> processInputBuffer -> processCommandAndResetClient(多线程下这个方法在当前流程下不会执行,而由主线程执行)
在多线程模式下,readQueryFromClient会将client信息加入server.clients_pending_read队列,listAddNodeHead(server.clients_pending_read,c);
 
主线程会将server.clients_pending_read中的数据分发到子线程的队列(io_threads_list)中,子线程会调用readQueryFromClient就行参数解析,主线程分发完任务后,会执行具体的操作数据库的命令,这块是单线程
如果参数解析完成会在client->flags中加一个标记CLIENT_PENDING_COMMAND,在主线程中先判断client->flags & CLIENT_PENDING_COMMAND > 0,说明参数解析完成,才会调用processCommandAndResetClient,之前还担心如果子线程还在做参数解析,主线程就开始执行命令难道不会有问题吗?现在一切都清楚了
main -> aeMain -> aeProcessEvents -> beforeSleep -> handleClientsWithPendingReadsUsingThreads -> processCommandAndResetClient -> processCommand -> call
读是多次读:socket读缓冲区有数据,epoll就会一直触发读事件,所以读可能是多次的
写是一次写:往socket写数据是在子线程中执行的,直接循环直到数据写完位置,就算某个线程阻塞了,也不会像单线程那样导致所有任务都阻塞
执行完相关命令后,就是将结果返回给client,回复client是一组函数,我们以addReply为例,说一下执行流程,执行addReply还是单线程的,将client信息插入全局队列server.clients_pending_write。
addReply -> prepareClientToWrite -> clientInstallWriteHandler -> listAddNodeHead(server.clients_pending_write,c) 在主线程中将server.clients_pending_write中的数据以轮训的方式分发到多个子线程中
beforeSleep -> handleClientsWithPendingWritesUsingThreads -> 将server.clients_pending_write中的数据以轮训的方式分发到多个线程的队列中io_threads_list
list *io_threads_list[IO_THREADS_MAX_NUM];是数组双向链表,一个线程对应其中一个队列 子线程将client中的数据发给客户端,所以是多线程
server.c -> main -> initThreadedIO(启动一定数量的线程) -> IOThreadMain(线程执行的方法) -> writeToClient -> connWrite -> connSocketWrite

网络操作对应的一些方法,所有connection对象的type字段都是指向CT_Socket

ConnectionType CT_Socket = {
.ae_handler = connSocketEventHandler,
.close = connSocketClose,
.write = connSocketWrite,
.read = connSocketRead,
.accept = connSocketAccept,
.connect = connSocketConnect,
.set_write_handler = connSocketSetWriteHandler,
.set_read_handler = connSocketSetReadHandler,
.get_last_error = connSocketGetLastError,
.blocking_connect = connSocketBlockingConnect,
.sync_write = connSocketSyncWrite,
.sync_read = connSocketSyncRead,
.sync_readline = connSocketSyncReadLine
};
 
 

Redis多线程原理详解的更多相关文章

  1. Redis哨兵原理详解

    一.概述 Redis哨兵(以下称哨兵)是为Redis提供一个高可靠解决方案,对一定程序上的错误,可以不需要人工干预自行解决. 哨兵功能还有监视.事件通知.配置功能.以下是哨兵的功能列表: 监控:不间断 ...

  2. Java中多线程原理详解

    Java是少数的集中支持多线程的语言之一,大多数的语言智能运行单独的一个程序块,无法同时运行不同的多个程序块,Java的多线程机制弥补了这个缺憾,它可以让不同的程序块一起运行,这样可以让程序运行更加顺 ...

  3. Redis快照原理详解

    本文对Redis快照的实现过程进行介绍,了解Redis快照实现过程对Redis管理很有帮助. Redis默认会将快照文件存储在Redis当前进程的工作目录中的dump.rdb文件中,可以通过配置dir ...

  4. Redis原理详解

    Redis原理详解 数据类型 Redis最为常用的数据类型主要有以下五种: String Hash List Set Sorted set 在具体描述这几种数据类型之前,我们先通过一张图了解下Redi ...

  5. 锁之“轻量级锁”原理详解(Lightweight Locking)

    大家知道,Java的多线程安全是基于Lock机制实现的,而Lock的性能往往不如人意. 原因是,monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是JVM依赖 ...

  6. (转)MySQL备份原理详解

    MySQL备份原理详解 原文:http://www.cnblogs.com/cchust/p/5452557.html 备份是数据安全的最后一道防线,对于任何数据丢失的场景,备份虽然不一定能恢复百分之 ...

  7. Redis AOF 持久化详解

    Redis 是一种内存数据库,将数据保存在内存中,读写效率要比传统的将数据保存在磁盘上的数据库要快很多.但是一旦进程退出,Redis 的数据就会丢失. 为了解决这个问题,Redis 提供了 RDB 和 ...

  8. Redis主从复制机制详解

    Redis主从复制机制详解 Redis有两种不同的持久化方式,Redis服务器通过持久化,把Redis内存中持久化到硬盘当中,当Redis宕机时,我们重启Redis服务器时,可以由RDB文件或AOF文 ...

  9. [转]Reids配置文件redis.conf中文详解

    转自: Reids配置文件redis.conf中文详解 redis的各种配置都是在redis.conf文件中进行配置的. 有关其每项配置的中文详细解释如下: 对应的中文版解释redis.conf # ...

随机推荐

  1. SAP ABAP: 把内表数据以excel或csv格式,通过前台或者后台的方式上传至FTP服务器

    今天接到一个FTP的需求,就是每天晚上把当天某个报表的数据自动保存excel上传到FTP服务器. SAP已经有现成的FTP函数使用,可以通过函数的方式来实现,实现前先准备一些数据: User:登录FT ...

  2. 脑桥Brain-Pons

    date: 2014-02-01 15:30:11 updated: 2014-02-01 15:30:11 [一] "2025.7.3.Brain-Pons?Expeiment?Under ...

  3. js 值类型与引用类型

    说明之前先提一个提问题,看一下你是怎么理解的 1. 值类型 简单的数据类型,存放在栈中 var num = 100; var num2 = num; num += 100; console.log(n ...

  4. CNN作为denoiser的优势总结

    图像恢复的MAP推理公式: $\hat{x}\text{}=\text{}$arg min$_{x}\frac{1}{2}||\textbf{y}\text{}-\text{}\textbf{H}x| ...

  5. 二进制获取List中缺少的对象

    今天,我需要做一个大数据页面,其中分了5类 大概就像这样:1类,2类,3类,4类,5类...... 但是由于数据没有足够多,所以现在统计出来的数据缺少了一到两类,或者更多,所以现在要找出在List在总 ...

  6. 你不知道的那些js调试命令

    通常情况下,我们在调试js程序的时候一般都使用console.log()来进行程序的调试,打印自己所需要的内容什么的. 那么js的调试命令可不止一个console.log() 分类输出 console ...

  7. SpringBoot整合Logback日志框架配置全解析

    目录 本篇要点 一.Logback日志框架介绍 二.SpringBoot与Logback 1.默认日志格式 2.控制台输出 3.文件输出 4.日志级别 5.日志组 6.自定义log配置 三.logba ...

  8. MongoDB下载安装与配置

    一.下载与安装 1.进入mongodb官网,try->download->community找到下载.或者直接在地址栏输入:https://www.mongodb.com/try/down ...

  9. 利用CNN进行中文文本分类(数据集是复旦中文语料)

    利用TfidfVectorizer进行中文文本分类(数据集是复旦中文语料) 利用RNN进行中文文本分类(数据集是复旦中文语料) 上一节我们利用了RNN(GRU)对中文文本进行了分类,本节我们将继续使用 ...

  10. NOIP 2018 D1 解题报告(Day_1)

    总分   205分 T1 100分 T2  95分 T3  10分 T1: 题目描述 春春是一名道路工程师,负责铺设一条长度为 nn 的道路. 铺设道路的主要工作是填平下陷的地表.整段道路可以看作是  ...