一、redis介绍
1、redis是什么
redis是一种非关系型数据库,存放的是非结构化数据(key:value形式),并且数据都存放于内存中,存取速度都快,类似的还有memcache
关系型数据库:例如mysql、oracle、db2等,表与表之间存在关联关系,存放的是结构化的数据
非关系型数据库:例如redis、memcache等,没有表的概念,数据存放形式示key:value,存放的是非结构化的数据
2、redis是用来干什么的?
redis可以取代mysql作为后端数据库使用(微博就是这么干的),但更多时候,在架构中,我们会用redis来做数据库的读缓存,以此来减轻数据库读取压力(而不是写压力),并且提升用户的访问速度,因为redis是把数据存放到内存中。与redis一样,memcache也可以作为数据库的缓存
3、为何一定要引入数据库的读缓存呢
储备知识:数据库的缓存有Query Cache(检查Qcache)与Buffer pool
Q:innodb buffer pool和Qcache的缓存区别?
A:
1、Qcacche缓存的是SQL语句及该SQL查询出的结果集,缓存在内存,最简单的情况是SQL一直不重复,那Qcache的命令率肯定是0;
2、buffer pool中缓存的是整张表中的数据,缓存在内存,SQL再变只要数据都在内存,那么命中率就是100%。
聪明的同学有如下疑问
mysql虽然会把数据都存放到硬盘上,但每次读取数据都会放入buffer bool缓存啊(并且还有Query Cache缓存sql的结果),后续的读取不也是从内存读取数据吗???为何还需要redis、memcache来作为数据库的读缓存呢?
没错,如果数据量比较小的情况下,mysql的buffer pool就足够了
但如果数据量的大的情况下,buffer pool根本救不了你、即便带有buffer pool这种运行机制,mysql这种结构化数据库的性能也会直线下降,为什么呢?看下一小节
补充概念:MySQL缓存之Qcache与buffer pool对比
二、结构化数据库的性能为何会下降
1、数据量小,buffer pool足够容纳,则不需要引入redis缓存
以Mysql为例,我们知道,为了调和CPU和磁盘的速度不匹配,MySQL 用buffer pool来加载磁盘数据到一段连续的内存中,供读写使用。
如果数据量比较小,缓冲池设置的大小能够放下所有数据页,那mysql操作基本不会产生读IO,而关于写IO因为是异步、内存相对数据量来说又很充足,所以写io也不会有性能问题,此时就没有引入redis缓存的必要
====
2、但现实往往是残酷的,但凡上点规模的应用的数据量都会很大,大过你内存大小也都很常见,而buffer pool设置的再大也大不过你的单机内存(设置为物理内存的70%已经算很高了),此时buffer pool根本不可能容纳所有数据页,你请求的数据页没有在buffer pool中就十分常见了
以此为背景,引出3个问题,
- 问题1:如果你请求的数据页没有在buffer pool,mysql会怎么办呢
mysql的读写操作要求数据页必须在buffer pool中才能进行,如果数据页未在buffer pool里,那mysql就必须去硬盘里读取,然后等待io操作结束,磁盘文件被加载到buffer pool后,mysql才能进行后续操作,如果过程中硬盘io没有响应,就会阻塞,必须等待io完成才行 - 问题2:如果遇到热点请求会怎么样?
热点请求指的是并发大量请求同一个数据页,因为buffer pool里没有该数据页,于是mysql必须去磁盘中读取,如果第一个请求该数据页的数据库线程因为硬盘io瓶颈,迟迟没有将物理数据页读入buffer pool, 这个时间区间拖得越长,则造成等待该数据块的用户线程就越多。对高并发的系统来说,将造成大量的等待,数据库的性能随即急剧下降 - 问题3:并发,大量请求的访问行为被阻塞,会造成什么后果?
对于服务来说,大量超时会使服务器处于不可用的状态。该台机器会触发熔断。熔断触发后,该机器的流量会打到其他机器,其他机器发生类似的情况的可能性会提高,极端情况会引起所有服务宕机,曲线掉底。
补充:
上面是由于磁盘IO导致服务异常的分析逻辑,也是我们生产中最常遇到的一种数据库性能异常的场景。
除此之外,还有锁竞争缓存命中率等异常场景也会导致服务异常。
说到这里,你应该知道mysql的buffer pool在数据量大的场景下,根本救不了你,它没办法很好的解决上面的问题
即便说你为了解决单库单表的极限,而引入了分库分表等优化策略,那也只能是缓解,而不会根除
此时,配合着、引入单独的缓存系统就显得十分有必要了
三、细说缓存的类型
单说redis的话,我们在架构中会用其作为数据库的读缓存
但缓存其实是一个很大的话题,除了数据库的缓存之外还有很多其他缓存,目的都只有一个,就是让用户能够以更快的速度拿到想要的数据并减少后端压力
我们统一在这里罗列一下缓存的类型
(1)客户端缓存
离用户最近的web页面缓存、app缓存。web页面因为技术成熟所以问题不是太多,但app因为设备的限制,在使用缓存时要多加注意。
之前经历的某个业务,因为客户端缓存出现问题,发生两次请求订单号串单,导致业务异常。串单呐,猜是因为缓存发生了混乱,至今比较奇怪会发生这种情况,需要对客户端相关加深认识了。
(2)单机缓存
CPU缓存[2]。为了调和CPU和内存之间巨大的速度差异,设置了L1/L2/L3三级缓存,离CPU越近,速度越快。知乎首页已读过滤的缓存架构,其灵感就是来源于此。
(3)数据库本身自带的缓存设置
Query cache即将查询的结果缓存起来,开启后生效。其可以降低查询的执行时间,对需要消耗大量资源的查询效果明显。
(4)分布式缓存(单独的缓存系统,用于缓存数据库的读)
memcached。[5] memcached是一个高效的分布式内存cache,搭建与操作使用都比较简单,整个缓存都是基于内存的,因此响应时间很快,但是没有持久化的能力。
Redis。 Redis以优秀的性能和丰富的数据结构,以及稳定性和数据一致性的支持,被业内越来越普遍的使用。
哪些公司在用redis作缓存?微博算是redis的重度用户,相传redis的新特性好多都是为了微博定制的
Twitter
GitHub
Weibo
StackOverflow
。。。。。。
(5)网页缓存-》CDN
CDN服务器是建立在网络上的内容分发网络。布置在各地的边缘服务器,用户可以经过中央渠道的负载平衡、内容分发、调度等功用模块获取附近所需的内容,减少网络拥塞,提高响应速度和命中率。
Nginx基于Proxy Store实现,使用Nginx的http_proxy模块可以实现类似于squid的缓存功能。当启用缓存时,Nginx会将相应数据保存在磁盘缓存中,只要缓存数据尚未过期,就会使用缓存数据来响应客户端的请求。
补充上图:Ehcache与Guava cache是java的两个缓存框架
四、redis为什么快
Redis是一种基于单线程模型的数据库,其设计主要是为了在并发和多客户端环境中,通过顺序处理命令,以确保数据的一致性并避免竞态条件。每当Redis处理一个命令时,这个命令在完成之前,任何其他命令都不会被执行。这样就保证了Redis的操作是原子的,也就避免了多线程环境中常见的数据竞态冲突问题。
然而,即便是在单线程模型下,Redis也能够通过优化内存管理、数据结构、采用epoll模型处理io,达到了惊人的速度和效率。
综上,总结redis快的原因主要有3点:
- 1、数据结构设计精良,存储速度快
- 2、单线程+io多路复用,单线程避免了线程切换和竞态/锁产生的消耗,io多路复用优化了单线程下的io阻塞问题
- 3、高效的内存管理
4.1 数据结构设计
redis的底层是用c语言来编写的,但是,数据结构确没有直接套用C的结构,而是根据redis的定位自建了一套数据结构,
这些数据结构的核心设计思想总结就是一句话:用空间换时间,用牺牲存储空间和微小的计算代价,来换取数据的快速操作
4.2 单线程+io多路复用
4.2.1 单线程
redis6.x之前,一直在说单线程如何如之何的好。(redis6.x之后诞生了多线程模式,但负载处理用户请求的还是单线程)
关于新版本的多线程模型在后面小节单独说,这里先说单线程。
所谓单线程是指对数据的所有操作都是由一个线程按顺序挨个执行的,使用单线程可以:
- 1、减少切换:
避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU; - 2、没有锁的开销
不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。
然而,使用了单线程的处理方式,就意味着到达服务端的请求不可能被立即处理。
那么怎么来保证单线程的资源利用率和处理效率呢?答案就是epoll网络IO模型的使用,该模型同样也是nginx高并发的核心
4.2.2 IO多路复用和事件驱动
Redis服务端,从整体上来看,其实是一个事件驱动的程序(所有的操作都以事件的方式来进行)
1、什么是事件?
事件,就是一种描述某事已经发生的信号。它可以是用户的动作,例如鼠标的点击、键盘的按压,
也可以是网络消息的接收、系统状态的变化,甚至是定时器的到时。
2、什么是驱动?
驱动,是一种机制,当接收到某种信号或者满足某种条件时,使得系统开始运行或者运行特定的程序。
常见的驱动方式有:时间驱动(例如闹钟到时)、条件驱动(例如温度达到阈值)、数据驱动(例如新邮件到达时提示)等。
3、什么是事件驱动?
事件驱动,是一种编程方式,它的运行流程并不是预先设定,而是由用户交互或者特定事件来决定的。
常见的一些系统,如桌面应用、网页应用,都是采用事件驱动方式。
举个例子
比如你使用的电子邮件客户端(例如Outlook或者Gmail等)。
当你收到一封新的邮件时,邮件客户端会为你提醒,这个提醒(可以是声音、图标闪动等等)就是一个事件。
邮件客户端在收到这个“新邮件到达”的事件后,根据这个事件采取了“提醒用户”的动作,这就是驱动。
因此,我们说这种邮件客户端是事件驱动的。
4、总结
事件驱动就是程序等待事件发生,然后响应事件,执行相应的代码
如图所示,Redis的事件驱动架构由四个部分组成,多个请求到达 Redis 服务器时的处理流程:
- 1、套接字(Socket):当一个客户端尝试连接到 Redis 服务器时,服务器会为这个连接创建一个套接字(socket)。
- 2、IO多路复用模型:通过 IO多路复用模型,Redis 能够用单一的线程来监听和管理多个套接字。这样,当多个套接字上有事件发生时,IO多路复用模型能将哪个套接字已经准备好进行读/写操作的信息通知给文件事件分派器。
- 3、文件事件分派器:文件事件分派器负责将该事件分派给对应的事件处理器(一个处理器是一个函数,用于处理特定的事件)进行处理。
- 4、事件处理器就是一个函数,这个函数会对套接字上发生的事件进行处理
补充:
Redis设计的事件分为两种,文件事件和时间事件
1、文件事件是对套接字操作的抽象,例如
客户端连接请求(AE_READABLE事件)
客户端命令请求(AE_READABLE事件)和事
服务端命令回复(AE_WRITABLE事件)
2、时间事件则是对一些定时操作的抽象
分为定时事件和周期性时间;redis的所有时间事件都存放在一个无序链表中,
当时间事件执行器运行时,需要遍历链表以确保已经到达时间的事件被全部处理。
Redis为每个IO多路复用函数都实现了相同的API,因此,底层实现是可以互换的。
Reids默认的IO多路复用机制是epoll,和select/poll等其他多路复用机制相比,epoll具有诸多优点:
1、epoll:类似与回调,套接字的描述符就绪后会主动通知
2、select:需要定期轮询套接字对象
2、poll:与select类似
并发连接限制 | 内存拷贝 | 活跃连接感知 | |
---|---|---|---|
epoll | 没有最大并发连接的限制 | 共享内存,无需内存拷贝 | 基于event callback方式,只感知活跃连接 |
select | 受fd限制,32位机默认1024个/64位机默认2048个 | 把fd集合从用户态拷贝到内核态 | 只能感知有fd就绪,但无法定位,需要遍历+轮询 |
poll | 采用链表存储fd无最大并发连接数限制 | 同select | 同select,需遍历+轮询 |
综上,Redis整个执行方案是通过高效的I/O多路复用件驱动方式加上单线程内存操作来达到优秀的处理效率和极高的吞吐量。
4.3 高效的内存管理
上面的小节也提到了,redis之所以可以使用单线程来处理,其中的一个原因是,内存操作对资源损耗较小,保证了处理的高效性。
如此宝贵的内存资源,Redis是怎么维护和管理的呢?
Redis的内存管理可以分为以下几部分:
- 内存分配: Redis在存储用户键时会严格按照用户设定的最大内存限制“maxmemory”进行内存分配。您可以配置这个参数来决定Redis可以使用多少系统内存。
- 惰性删除和过期键删除: 一个键可能已经过期了,但直到其在被读取时,Redis才会发现并删除,这被称为惰性删除。此外,Redis也会定期进行键的删除操作,以及时释放内存。
- 命中率统计和LRU时间更新: 在读取一个键后,Redis会进行命中率统计和最近最少使用(LRU)时间的更新。命中率统计用于记录键的访问情况,LRU时间则用于记录键的闲置时间,这将影响键的淘汰策略。
- 数据淘汰策略: 如果key生产的太快,定期删除操作跟不上新生产的速率,而这些key又很少被访问无法触发惰性删除,是否会把内存撑爆?回答是不会,因为redis有数据淘汰策略:
- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。 - allkeys-lru:当内存不足以容纳新写入数据时,,移除最近最少使用的 Key。 - allkeys-random:当内存不足以容纳新写入数据时,随机移除某个 Key。 - volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。 - volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。 - volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。
- 内存优化和监视: 通过Redis提供的内置命令,比如MEMORY USAGE和INFO memory,您可以获取Redis当前的内存使用信息。同时,通过合理配置和调优Redis,您可以根据自身应用的需求更好地优化内存使用。
五、Redis为什么这么靠谱
主要原因有两点,后续我们将详细介绍
1、redis将数据存放于内存,但提供了RDB和AOF两种不同的数据持久化方式
2、提供了高可用集群方案,例如主从模式、sentinel/哨兵模式、集群模式
虽然Redis的核心功能采用了单线程模型,但是一些任务,比如持久化(AOF、RDB)、复制和集群的元数据管理等,是可以在后台单独的进程/线程中完成的。
六、Redis6.x的多线程
Redis6.x之后推出了多线程模型,但这个多线程模型并不是传统意义的多线程并发,看下图
对客户端的任何请求,其实还是主线程在执行,只是把socket对象的解析回写的这部分io操作多线程话,以解决IO上的时间消耗带来的系统瓶颈。
Redis6.x这所以这么搞的原因如下
- redis6.x更新的目标:在保证核心数据访问仍然是单线程、避免数据一致性问题的同时,改进了其网络I/O处理,从而进一步提高了Redis的性能。
- 原因:在Redis 6.0之前,尽管它的内存管理非常高效,单线程模型也能提供惊人的速度,但Redis的瓶颈并非在于内存,而在于网络I/O模块耗费的CPU时间。Redis 6.0引入多线程来处理网络I/O,从而使得CPU资源得到充分利用,减少了网络I/O阻塞所产生的性能损失。
- 工作流程:在多线程模型中,Redis会创建多个线程处理网络I/O。当客户端发送命令到Redis服务器时,这些线程会并行地读取网络套接字中的这些命令。然后,单线程主逻辑将从这些线程中获取已经到达的命令并进行处理。处理完命令后,Redis会将结果发送回客户端。这里的发送结果也是由多个线程并行执行。
Redis的这种多线程模型,特别是在处理大量并发请求时,可以显著提高Redis的性能。在很多场景中,当I/O线程功能开启时,配合适当的线程数配置,Redis的新版本可以在单实例上提供2倍于之前的操作处理能力。
需要注意的是,虽然启用了I/O多线程,但Redis的数据操作仍然是单线程的,不会因为多线程而出现数据不一致的问题。所以,你可以放心地在需要高并发处理的项目中使用新版Redis。