Redis(小记)

277 阅读11分钟

Redis是一款高性能的key-value数据库

特点

基于内存的key-value数据库 读写性能高 支持丰富的数据类型(string字符串,list链表,set基于hash表的无序集合,Sorted Set有序集合) 单条操作具有原子性,多条操作支持事务 支持发布订阅 单线程 多路 I/O 复用技术 支持持久化

为什么使用单线程

我们通常说的redis是单线程的,指的是Redis 的网络 IO 和键值对读写是由一个线程来完成的
而其他的持久化、异步删除、集群数据同步等功能是由额外的线程完成。

redis使用单线程避免多线程并发下的共享资源访问安全的问题。

为什么快

纯内存操作
IO多路复用:\

网络 IO 操作中,accept() 和 recv()会阻塞线程。

在 socket 模型中,不同操作调用后会返回不同的套接字类型。socket() 方法会返回主动套接
字,然后调用 listen() 方法,将主动套接字转化为监听套接字,此时,可以监听来自客户端的
连接请求。最后,调用 accept() 方法接收到达的客户端连接,并返回已连接套接字

针对监听套接字,我们可以设置非阻塞模式:当 Redis 调用 accept() 但一直未有连接请求到
达时,Redis 线程可以返回处理其他操作,而不用一直等待。但是,你要注意的是,调用 
accept() 时,已经存在监听套接字了。虽然 Redis 线程可以不用继续等待,但是总得有机制继
续在监听套接字上等待后续连接请求,并在有请求时通知 Redis。类似的,我们也可以针对已连接
套接字设置非阻塞模式:Redis 调用 recv() 后,如果已连接套接字上一直没有数据到达,
Redis 线程同样可以返回处理其他操作。我们也需要有机制继续监听该已连接套接字,并在有数据
达到时通知 Redis。

基于多路复用的高性能 I/O :
模型Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 
select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时
存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦
有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
Redis网络框架调用 epoll 机制,让内核监听这些套接字。此时,Redis 线程不会阻塞在某一个特定的
监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,
Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。

为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对
不同事件的发生,调用相应的处理函数。那么,回调机制是怎么工作的呢?其实,select/epoll
一旦监测到 FD 上有请求到达时,就会触发相应的事件。这些事件会被放进一个事件队列,Redis
单线程对该事件队列不断进行处理。这样一来,Redis 无需一直轮询是否有请求实际发生,这就可
以避免造成 CPU 资源浪费。同时,Redis 在对事件队列中的事件进行处理时,会调用相应的处理
函数,这就实现了基于事件的回调。因为 Redis 一直在对事件队列进行处理,所以能及时响应客
户端请求,提升 Redis 的响应性能。

高效的数据结构:hash表和跳表

redis性能的瓶颈

Redis单线程处理IO请求性能瓶颈主要包括2个方面:

1、任意一个请求在server中一旦发生耗时,都会影响整个server的性能,也就是说后面的请求都要等前面这个耗时请求处理完成,自己才能被处理到。耗时的操作包括以下几种:
a、操作bigkey:写入一个bigkey在分配内存时需要消耗更多的时间,同样,删除bigkey释放内存同样会产生耗时;
b、使用复杂度过高的命令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)命令,但是N很大,例如lrange key 0 -1一次查询全量数据;
c、大量key集中过期:Redis的过期机制也是在主线程中执行的,大量key集中过期会导致处理一个请求时,耗时都在删除过期key,耗时变长;
d、淘汰策略:淘汰策略也是在主线程执行的,当内存超过Redis内存上限后,每次写入都需要淘汰一些key,也会造成耗时变长;
e、AOF刷盘开启always机制:每次写入都需要把这个操作刷到磁盘,写磁盘的速度远比写内存慢,会拖慢Redis的性能;
f、主从全量同步生成RDB:虽然采用fork子进程生成数据快照,但fork这一瞬间也是会阻塞整个线程的,实例越大,阻塞时间越久;

2、并发量非常大时,单线程读写客户端IO数据存在性能瓶颈,虽然采用IO多路复用机制,但是读写客户端数据依旧是同步IO,只能单线程依次读取客户端的数据,无法利用到CPU多核。

针对问题1,一方面需要业务人员去规避,一方面Redis在4.0推出了lazy-free机制,把bigkey释放内存的耗时操作放在了异步线程中执行,降低对主线程的影响。

针对问题2,Redis在6.0推出了多线程,可以在高并发场景下利用CPU多核多线程读写客户端数据,进一步提升server性能,当然,只是针对客户端的读写是并行的,每个命令的真正操作依旧是单线程的。

简单介绍下select poll epoll的区别,select和poll本质上没啥区别,就是文件描述符数量的限制,select根据不同的系统,文件描述符限制为1024或者2048,poll没有数量限制。他两都是把文件描述符集合保存在用户态,每次把集合传入内核态,内核态返回ready的文件描述符。 epoll是通过epoll_create和epoll_ctl和epoll_await三个系统调用完成的,每当接入一个文件描述符,通过ctl添加到内核维护的红黑树中,通过事件机制,当数据ready后,从红黑树移动到链表,通过await获取链表中准备好数据的fd,程序去处理。

应用场景

缓存、计数器(浏览器、收藏量)、自增主键生成器 (string)
抽奖(set)
排行榜(sorted set)
共享session
分布式锁(getset key value)若key不存在,赋值翻译1 若存在返回0

IO模型

IO多路复用

为什么 Redis 中要使用 I/O 多路复用这种技术呢?

首先,Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。

先来看一下传统的阻塞 I/O 模型到底是如何工作的:当使用 read 或者 write 对某一个文件描述符(File Descriptor 以下简称 FD)进行读写时,如果当前 FD 不可读或不可写,整个 Redis 服务就不会对其它的操作作出响应,导致整个服务不可用。

在 I/O 多路复用模型中,最重要的函数调用就是 select,该方法的能够同时监控多个文件描述符的可读可写情况,当其中的某些文件描述符可读或者可写时,select 方法就会返回可读以及可写的文件描述符个数。

缓存淘汰

voltile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰

volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰

volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰

allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰

allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰

缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

解决方案:
1、布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
2、如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟

缓存雪崩

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

解决方案:
1、在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件
2、缓存失效后加锁排队,减小数据库的压力

缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。

缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮

解决方案:
1、缓存失效后加锁排队,减小数据库压力,每次请求获取锁后还是先get缓存,第一个请求获取到数据后放入缓存\

问题定位

redis-cli 连入redis info memory查看内存使用量

(1)used_memory:Redis分配器分配的内存总量(单位是字节),包括使用的虚拟内存(即swap);Redis分配器后面会介绍。used_memory_human只是显示更友好。

(2)used_memory_rss:Redis进程占据操作系统的内存(单位是字节),与top及ps命令看到的值是一致的;除了分配器分配的内存之外,used_memory_rss还包括进程运行本身需要的内存、内存碎片等,但是不包括虚拟内存。

因此,used_memory和used_memory_rss,前者是从Redis角度得到的量,后者是从操作系统角度得到的量。二者之所以有所不同,一方面是因为内存碎片和Redis进程运行需要占用内存,使得前者可能比后者小,另一方面虚拟内存的存在,使得前者可能比后者大。

由于在实际应用中,Redis的数据量会比较大,此时进程运行占用的内存与Redis数据量和内存碎片相比,都会小得多;因此used_memory_rss和used_memory的比例,便成了衡量Redis内存碎片率的参数;这个参数就是mem_fragmentation_ratio。

1、mem_fragmentation_ratio:used_memory_rss/used_memory的比值内存碎片比率<1 说明内存不足,使用了虚拟内存

主从复制

内存模型

www.cnblogs.com/kismetv/p/8…

内存占用:数据、缓冲区、redis运行使用、内存碎片

hashtable结构:

默认内存分配器:jemalloc

将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。

Redis高可用

持久化:持久化是最简单的高可用方法(有时甚至不被归为高可用的手段),主要作用是数据备份,即将数据存储在硬盘,保证数据不会因进程退出而丢失。

复制:复制是高可用Redis的基础,哨兵和集群都是在复制基础上实现高可用的。复制主要实现了数据的多机备份,以及对于读操作的负载均衡和简单的故障恢复。缺陷:故障恢复无法自动化;写操作无法负载均衡;存储能力受到单机的限制。

哨兵:在复制的基础上,哨兵实现了自动化的故障恢复。缺陷:写操作无法负载均衡;存储能力受到单机的限制。

集群:通过集群,Redis解决了写操作无法负载均衡,以及存储能力受到单机限制的问题,实现了较为完善的高可用方案

持久化

www.cnblogs.com/kismetv/p/9…

RDB:指定时间间隔存储当前数据快照(例如 save 900 1 900秒发生一次写入就生成快照)

AOF:记录每次写操作

RDB的快照、AOF的重写都需要fork,这是一个重量级操作,会对Redis造成阻塞。因此为了不影响Redis主进程响应,我们需要尽可能降低阻塞。

降低fork的频率,比如可以手动来触发RDB生成快照、与AOF重写;
控制Redis最大使用内存,防止fork耗时过长;
可以使用一个从库定时进行数据备份

比较:
aof相对于rdb更可靠,rdb可能会丢掉备份时间间隔的数据; 数据量较大时,恢复数据时rdb更快一些

主从复制

www.cnblogs.com/kismetv/p/9…

将一台redis服务器的数据,复制到另一台redis服务器,前者为主节点,后者为从节点,只能从主节点复制到从节点。

主从复制实现原理:建立连接、数据同步、命令传输

数据冗余、故障切换、读负载均衡

存在问题:数据不一致、数据过期、复制中断、无法实现写负载

哨兵模式

www.cnblogs.com/kismetv/p/9…

在主从复制的基础上实现了故障自动切换,但是无法实现写负载均衡。

哨兵是特殊的redis,不存储数据。

1)定时发送心跳到主节点判断是否下线

2)若心跳检测到已经下线,判断为主观下线

3)询问其他哨兵,认为主节点已经下线的数量达到配置的值判定为客观下线

4)选举主哨兵进行故障转移

5)主哨兵选择新的主节点,过滤掉不健康的节点,优先配置的优先级高的从节点,若一致选择数据偏移量大的节点,若一致选择runid最小的节点。

6)通过slaveof no one命令,让选出来的从节点成为主节点;并通过slaveof命令让其他节点成为其从节点

集群

www.cnblogs.com/kismetv/p/9…

1、数据分区: 将数据分散到多个节点,扩展了redis的存储能力

2、高可用: 每个主节点都可以提供读写能力,提高响应能力,故障自动转移

搭建集群:

1)启动各个节点,此时各个节点时独立的
2)节点握手,将各个节点连成一个网络
3)分配槽,将16384个槽分配给主节点
4)指定主从关系

数据分区方案:带虚拟节点的一致性Hash

将hash的空间组成一个虚拟的圆环,将糟作为虚拟节点均匀分配到圆环上,每个槽就代表了hash值在一定范围的数据,映射关系为:hash值->槽->实际节点

问题:
不支持分布在多个分区的多个key的批量操作

疑问:

put与get的过程?

redis的IO模型?