# Redis ## 概述 ### 什么是Redis - Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的(BSD许可)高性能非关系型(NoSQL)的键值对数据库。 - Redis 可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。 - 与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。另外,Redis 也经常用来做分布式锁。除此之外,Redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。 ### Redis有哪些优缺点 #### 优点 - 读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。 - 支持数据持久化,支持AOF和RDB两种持久化方式。 - 支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。 - 数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。 - 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。 #### 缺点 - 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。 - Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。 - 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。 - Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。 ### 为什么要用 Redis / 为什么要用缓存 主要从“高性能”和“高并发”这两点来看待这个问题。 **高性能:** 假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可! **高并发:** 直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。 ### 为什么要用 Redis 而不用 map/guava 做缓存? 缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 Hashmap 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。 使用 redis 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 服务的高可用,整个程序架构上较为复杂。 ### Redis为什么单线程还这么快 1. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。 2. 数据存在内存中,构建了类似于 HashMap的结构查找和操作的时间复杂度都是O(1); 3. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗; 4. 使用多路 I/O 复用模型,非阻塞 IO; ### Redis为什么是单线程 - redis是基于内存的操作,cpu不是瓶颈,而单线程下有很多优点,就用单线程了 - 不需要各种锁消耗性能 - 避免了cpu上下文切换和竞争条件的小号 - 在单机多开起几个redis实例可以发挥多核性能 ## 数据类型 ### Redis有哪些数据类型 Redis主要有5种数据类型,包括String,List,Set,Zset,Hash | 数据类型 | 可存储的值 | 操作 | 应用场景 | | :------: | :--------------------: | :----------------------------------------------------------: | :----------------------------: | | String | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作<br/>对整数和浮点数执行自增或者自减操作 | 做简单的键值对缓存 | | List | 列表 | 从两端压入或者弹出元素<br/>对单个或者多个元素进行修剪,<br/>只保留一个范围内的元素 | 做异步队列,秒杀 | | Set | 无序集合 | 添加、获取、移除单个元素<br/>检查一个元素是否存在于集合中<br/>计算交集、并集、差集<br/>从集合里面随机获取元素 | 用在一些去重的场景里 | | Zset | 有序集合 | 添加、获取、删除元素<br/>根据分值范围或者成员来获取元素<br/>计算一个键的排名 | 去重排序,用在各类热门排序场景 | | Hash | 包含键值对的无序散列表 | 添加、获取、移除单个键值对<br/>获取所有键值对<br/>检查某个键是否存在 | 储存结构化的数据,比如一个对象 | ## 数据类型底层结构 <img src="https://mmbiz.qpic.cn/mmbiz_png/J0g14CUwaZc0xXMUofrwAVS3mR0DxcWChjiaqeTofEHm1yC4rBn5hUqPxKqSPNoxFemRgaHBiaWPO5uU27sdYTwA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片" style="zoom:67%;" /> ### 键值对全景图 ![图片](https://mmbiz.qpic.cn/mmbiz_png/J0g14CUwaZc0xXMUofrwAVS3mR0DxcWC8l11icBJ2RhAChyV0x4wJvmUse9L2br11KdltVdIosib1WjwAhUVRq4g/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) - redisDb结构:表示Redis数据库的结构,结构体里存放了指向dict结构的指针 - dict结构:存放了两个哈希表,正常情况用哈希表1,哈希表2只有在rehash时使用 - ditctht结构:表示哈希表的结构,数组中的每个元素都指向一个哈希表节点(dictEntry)的指针 - dictEntry结构:表示哈希表节点的结构里面存放了key和value指针,指向的都是redisObject对象 - redisObject对象有三个成员: - type(标识对象是什么类型的), - encoding(标识该对象使用了那种底层数据结构) - ptr(指向底层数据结构的指针) ### SDS 最大512M #### 具体结构 - len:**记录字符串的长度** - alloc:**分配给字符串数组的空间长度**,修改字符串是,可以直接通过alloc-len计算出剩余的空间大小,方便执行扩容 - flags:**用来表示不同类型的SDS**,sdshdr 5-64 - buf[]:**字符数组,用来保存实际数据**,不仅可以保存字符串,还可以保存二进制数据 ### 双向链表 #### 具体结构 - listNode head:**指向链表头节点** - listNode tail:**指向链表尾节点** - len:**链表节点数量** - 三个函数:节点复制、释放、比较的函数 其中listNode结构为:prev前置节点,next后置节点,value节点的值 因为链表每个节点的内存都不连续,无法很好利用cpu缓存,而且每保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大 后序版本废弃 ### 压缩列表 最大的特点,就是设计成为了一种**内存紧凑型**的数据结构,占用一块连续的内存空间,可以有效的节省内存开销 但是缺点也是有的,①:不能保存过多的元素,否则查询效率会降低。②:新增或修改某个元素时,ziplist占用的内存空间需要重新分配,甚至可能引发连锁更新问题 #### 具体结构 由连续的内存块组成的顺序型数据结构,类似于数组 ![图片](https://mmbiz.qpic.cn/mmbiz_png/J0g14CUwaZc0xXMUofrwAVS3mR0DxcWCGAib78WAoB6Oa4udfiawxjeoLnico76ib5dn5AF9exN0J5O2oCzxMyh9GQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) - zlbytes:记录整个压缩列表占用的内存字节数 - zltail:记录压缩列表尾部节点距离起始地址多少字节 - zllen:记录压缩列表包含的节点数量 - entry:节点 - prevlen,记录的前一个节点的长度 - 如果前一个节点的长度小于254字节,那么需要一字节的空间来保存这个长度值 - 如果前一个节点的长度大于等于254,那么需要5个字节来保存这个长度 - encoding,记录的当前节点实际数据的类型以及长度 - 当前节点的数据是整数,会使用1字节来编码 - 当前节点的数据是字符串,会根据长度大小,使用1/2/5字节的空间 - data,记录的当前节点的实际数据 - zlent:标记压缩列表的结束点,固定值0xff 想查找头和尾元素,时间复杂度O(1),而其他元素就O(n)了,因此不适合保存过多的元素 #### 连续更新 假设列表中有多个连续的,长度在250~253之间的节点,当前一个节点插入大于254字节时,后一个节点需要扩容到5字节,引发连锁效应,所有prevlen都需要扩展到5字节 ### 哈希表 #### 具体结构 哈希表结构 - table:哈希表数组节点指针 - size:哈希表大小 - sizemask:哈希表大小掩码,用于计算索引值 - used:已有的节点数量 哈希表节点 - key:键 - v:键值对中的值,该值是一个联合体 - val:指针,指向实际值 - u64,s64,d:如果值是64位整数或浮点数时,可以直接将数据内嵌再结构里,无需用指针指向实际值 - next:指向下一个节点的指针:这个指针可以将多个哈希值相同的键值对连接起来,链式哈希结构 #### rehash 哈希结构解决哈希冲突时,采用链式哈希的方法,但数据过大时,触发了rehash操作 1. 给hashtable2分配比hashtable1大两倍的空间 2. 将hashtable1的数据迁移到hashtable2中 3. 完成后释放hashtable1,将2设为1,并在现有的hashtable2新建一个空白的表,为下次做准备 为了防止hashtable1数据量过大,拷贝时造成阻塞,redis采用了渐进式rehash的方法 1. 给hashtable2分配空间 2. 在rehash执行期间,每次增删改查时,除了会执行对应的操作,还会顺序的将hashtable1中索引位置上所有的键值对迁移到hashtable2上 3. 随着客户端发起的操作请求数量越多,最终执行完毕 在渐进式的期间,删改查会在这两个表同时执行,如果ht1没找到,回到ht2找,新增直接在ht2中 触发条件:根据负载因子触发:负载因子 = 哈希表已保存节点数量 / 哈希表大小(已保存的节点可以包括hash冲突的节点,所以可以大于表大小) - 当负载因子大于等于1时,如果没有进行RDB快照或者AOF重写时,就会执行rehash - 当大于等于5时,会强制执行rehash ### 整数集合 当集合元素范围都是在(-2^63 到 2^63)内的整数并且个数不超过 512 时,Set对象会使用这个数据结构作为底层实现, #### 具体结构 - encoding:编码方式 - length:元素数量 - contents:保存元素的数组 contents数组保存的元素于encoding声明的编码方式相同 int8_t:char int16_t: short int32_t: int int64_t: long #### 升级操作 假如一个intset集合为int16_t,而新加入元素的类型为int32_t时,便会出发升级操作,按照int32_t来扩展contents数组空间的大小, 假如原有3个int16_t类型的数据,需要添加一个int32_t类型的数据,需要扩容4x32-3x16=80位,讲原有的3个类型依次扩容后,插入新数据 这样可以避免内存浪费,节省资源,而且不会降级 ### 跳表 跳表是在链表基础上改进过来的,实现了**“多层”有序的链表**,查询时间复杂度为O(logN) ![图片](https://mmbiz.qpic.cn/mmbiz_png/J0g14CUwaZc0xXMUofrwAVS3mR0DxcWCVu9fLYSAtpib8llIX5FS4mn1HZbu0FyiaLy4S6BnYn3WwZeOLNfFAhjQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) #### 具体结构 跳表:zskiplist - 跳表的头尾节点,便于在O(1)时间复杂度内访问跳表的头节点和尾节点 - 跳表的长度,便于获取跳表节点的数量 - 最大层数,便于直接获取层高最大的节点的层数量 跳表节点:zskiplistNode - ele:zset对象的具体值 - score:元素的权重值 - backward:向前指针,方便倒查询 - level [ ] :数组中存储zskiplistLevel对象,保存当前节点每层上的数据level[0]表示第一层 - 向后指针foeward:用来指向下一个节点 - 跨度span:用来记录两个节点之间的距离,如果下个是Null,记为0 跨度实际是为了计算这个**节点在跳表中的排位**,计算某个节点排位时,从头节点到该节点的查询路径上,将沿途的跨度累加起来,得到的结果就是目标节点在跳表中的排位 #### 跳表节点查询过程 在查询跳表节点的过程中,跳表会从头节点的**最高层**开始,逐一遍历每一层。在遍历某一层的跳表节点时,会用节点中的SDS类型的元素和元素的权重来判断 - 当节点的权重**小于**要查找的权重时,就会访问该层上的下一个节点 - 当节点的权重**等于**要查找的权重时,并且当前节点的SDS类型数据**小于**要查找的数据,就会访问该该层的下一个节点 - 当节点的权重**大于**要查找的权重时,或者下一个节点为空时,就会使用当前节点level数组的下一层指针,然后沿着下一层继续查找 #### 跳表节点层数设置 相邻两层的节点数量节点比例2:1时间复杂度可以降到O(logN) 为了保证2:1,而且在增删时不需要增加额外的开销,会**随机生成每个节点的层数** 创建节点时,会生成范围为0到1的随机数如果**小于0.25,层数就加1层**,直到随即数大于0.25,累加的层数就是最终的层数,这种方法相当于每层增加的概率不超过25%,最大层高64 ### QuickList 其实就是**双向链表+压缩列表**组合,一个QuickList就是一个链表,而链表的每个元素都是压缩列表 quickList通过控制每个链表节点中的压缩列表大小或者元素个数,来降低连续更新的影响,一个链表对应一个列表 #### 具体结构 ![图片](https://mmbiz.qpic.cn/mmbiz_png/J0g14CUwaZc0xXMUofrwAVS3mR0DxcWCgia3tRMJFSJ9WbBsc0gt9MbsfOorcKe2Ofr96iavwaJJFdeIX3Jubu5A/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) quickList结构同双向链表类似,区别在于内部节点 quickListNode包含5部分:指向前一个和后一个节点的指针,指向具体压缩列表的指针,压缩列表的字节大小,和压缩列表的元素个数 添加时不会直接新建一个链表节点,而是先检查插入位置的压缩列表是否能容纳,默认值16,如果不能会新建一个quickListNode。 ### ListPack 用来代替zipList,listpack每个节点不再包含前一个节点的长度,只记录当前节点的长度,插入时不影响其他节点的变化,避免了连锁更新的隐患 #### 具体结构 ![图片](https://mmbiz.qpic.cn/mmbiz_png/J0g14CUwaZc0xXMUofrwAVS3mR0DxcWCmuXHXl0sZuRyd28U5J89Nd0Z1G9LymamHCJRaVtwRI6Rj7CsDInDHw/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) - listpack:总字节数+元素数量 + 末尾的结束标识 - 节点内容: - encoding:元素编码类型,同ziplist的encoding - data:实际存放的数据 - len:encoding+data的总长度, ## 持久化 ### 什么是Redis持久化? 持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失,宕机后再通过持久化文件将数据恢复。 ### Redis 的持久化机制是什么?各自的优缺点? Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制: #### RDB 是Redis DataBase的缩写,是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。 rdb文件都是二进制,很小。比如内存数据有10gb,rdb文件可能就1gb ![img](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80MDU1NjY2LWMwNzAyMjIzMTUxODUyMjkucG5n?x-oss-process=image/format,png) ##### 优点: 1、只有一个.rdb文件,方便持久化。 2、容灾性好,一个文件可以保存到安全的磁盘。 3、性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能 4、他的主进程处理命令(bgsave时)的效率相对于aof要高 ##### 缺点: 1、数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候 2、没aof执行速度快,因为rdb每次全量,aof每次只追加 ##### RDB持久化的两种方法 - save - 同步、阻塞 - 持久化的时候redis服务阻塞(准确的说会阻塞当前执行save命令的线程,但是redis是单线程的,所以整个服务会阻塞),不能继对外提供请求 - bgsave - 异步、非阻塞 (fork() + copyonwrite) - 可以一边进行持久化,一边对外提供读写服务,互不影响,新写的数据对我持久化不会造成数据影响,持久化的过程中报错或者耗时太久都对当前对外提供请求的服务不会产生任何影响。持久化完会将新的rdb文件覆盖之前的。 ###### fork() **fork有什么用** fork()是操作系统的一个api,用于创建一个子进程,其进程共享其父类的内存数据。但仅仅是共享fork出子进程的那一刻的内存数据,后期修改的数据对子进程不可见,同理,子进程修改的数据对主进程也不可见。子进程如果挂了,不影响主进程,但主进程挂了,子进程也会挂。 **redis中的fork** 当redis的bgsave执行时,Redis主进程会判断当前是否有fork()出来的子进程,若有则忽略,若没有则会fork()出一个子进程来执行rdb文件持久化的工作,子进程与Redis主进程共享同一份内存空间,所以子进程可以做他的rdb文件持久化工作,主进程又能继续他的对外提供服务,二者互不影响。但为什么修改内存的数据彼此不可见,明明是一块内存,这里就采取了CopyOnWrite 技术来实现。 ###### CopyOnWrite 如字面意思,写入时复制 fork()之后,会把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入一个中断例程。中断例程中,会把**触发的异常的页复制一份**,于是父子进程各自持有独立的一份。 COW技术可**减少**分配和复制大量资源时带来的**瞬间延时**但是如果在fork()之后,父子进程都还需要继续进行写操作,那么会产生大量的**分页错误**(页异常中断page-fault),这样就得不偿失 在 Redis 服务中,子进程只会读取共享内存中的数据,它并不会执行任何写操作,只有主进程会在写入时才会触发这一机制,而对于大多数的 Redis 服务或者数据库,写请求往往都是远小于读请求的,所以使用fork()加上写时拷贝这一机制能够带来非常好的性能,也让BGSAVE这一操作的实现变得很简单。 #### AOF AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。 每次都在aof文件后面追加命令。他与主进程收到请求、**处理请求是串行化的,而非异步并行的**。所以aof的频率高的话会对Redis带来性能影响。Redis每次都是先将命令放到缓冲区,然后根据具体策略(每秒/每条指令/缓冲区满)进行刷盘操作。如果配置的always,那么就是典型阻塞,如果是sec,每秒的话,那么会开一个同步线程去每秒进行刷盘操作,对主线程影响稍小。 Redis每次在写入AOF缓冲区之前,他都会检测aof持久化策略的配置**appendfsync**:判断是否需要将AOF缓冲区的内容写入和同步到AOF文件中。这个决策是由配置文件的三个策略来控制的: - no:表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快,但是不太安全; - always:表示每次写入都执行fsync,以保证数据同步到磁盘,效率很低; - everysec:表示每秒执行一次fsync,可能会导致丢失这1s数据。通常选择 everysec ,兼顾安全性和效率。 **rewrite** 用于去除重复的命令,保留最新的命令。 **4.0前** 压缩前 set k1 123 --- del k1 ---- set k1 789 压缩后 set k1 789 **4.0后** 4.0开始的rewrite支持混合模式(也是就是rdb和aof一起用),直接用RDB持久化的方式来操作,将二进制内容覆盖到AOF文件中(rdb是二进制,所以很小),然后再有写入的话还是继续append追加到文件原始命令,等下次文件过大的时候再次rewrite(还是按照rdb持久化的方式将内容覆盖到aof中)。但是这种模式也是配置的,默认是开,也可以关闭。 ##### 优点 - 持久化的速度快,因为每次都只是追加,rdb每次都全量持久化 - 数据相对更可靠,丢失少,可以配置每秒持久化、每个命令执行完就持久化 ##### 缺点 - 灾难性恢复的时候过慢,因为aof每次都只追加原命令,导致aof文件过大,虽然后面会rewrite,但是相对于rdb也是慢的。 - 会对主进程对外提供请求的效率造成影响,接收请求、处理请求、写aof文件这三步是串行原子执行的。而非异步多线程执行的。而Redis是单线程! ### 合适的持久化方式 - 一般来说, 如果想达到较高的数据安全性,应该同时使用两种持久化功能,即RDB-AOF混合持久化(4.0版本后支持)。在这种情况下,当 Redis 重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。 - 如果可以承受数分钟以内的数据丢失,那么可以只使用RDB持久化,但并不推荐只使用AOF持久化,因为定时生成RDB快照,便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快。 - 如果只希望数据在服务器运行的时候存在,也可以不使用任何持久化方式。 ## 过期键删除策略 expier设置过期时间,persist设置永久生效 Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略常有以下三种: - 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。 - 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。 - 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。) Redis所有的键都可以设置过期属性,内部保存在过期字典中。由于进程内保存了大量的键,维护每个键精准的过期删除机制会导致消耗大量的CPU,对于单线程的Redis来说成本过高,因此Redis采用**惰性删除**和**定时任务删除**机制实现过期键的内存回收。 - **惰性删除**:惰性删除用于当客户端读取带有超时属性的键时,如果已经超过键设置的过期时间,会执行删除操作并返回空,这种策略是出于节省CPU成本考虑,不需要单独维护TTL链表来处理过期键的删除。但是单独用这种方式存在内存泄露的问题,当过期键一直没有访问将无法得到及时删除,从而导致内存不能及时释放。正因为如此,Redis还提供另一种定时任务删除机制作为惰性删除的补充。 - **定时任务删除**:Redis内部维护一个定时任务,默认每秒运行10次(通过配置hz控制)。定时任务中删除过期键逻辑采用了自适应算法,根据键的过期比例,使用快慢两种速率模式回收键。 ## 内存相关 ### 如何保证redis中的数据都是热点数据 MySQL里有2000w数据,redis中只存20w的数据,当redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。 ### Redis的内存淘汰策略有哪些 淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。 **全局的键空间选择性移除** - noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。 - allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(常用) - allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。 **设置过期时间的键空间选择性移除** - volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。 - volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。 - volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。 内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。 ### Redis的内存用完了会发生什么? 如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。 ### Redis如何做内存优化? 可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面 ## 线程模型 ### Redis线程模型 Redis基于反应器设计模式(Reactor)模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。 - 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。 - 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。 虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。 ## 事务 ### 什么是事务? 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。 ### Redis事务的概念 Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。 总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。 ### Redis事务的三个阶段 1. 事务开始 MULTI 2. 命令入队 3. 事务执行 EXEC 事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队 ### Redis事务相关命令 Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的 Redis会将一个事务中的所有命令序列化,然后按顺序执行。 1. **redis 不支持回滚**,“Redis 在**事务失败**时不进行回滚,而是**继续执行**余下的命令”, 所以 Redis 的内部可以保持简单且快速。 2. 如果在一个事务中的**命令**出现错误,那么**所有的命令**都不会执行; 3. 如果在一个事务中出现**运行错误**,那么**正确的命令**会被执行。 - WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。 - MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。 - EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。 - 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。 - UNWATCH命令可以取消watch对所有key的监控。 ### 事务管理(ACID)概述 **Redis的事务总是具有ACID中的一致性和隔离性**,其他特性是不支持的。当服务器运行在*AOF*持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。 #### Redis事务支持隔离性吗 Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。 #### Redis事务保证原子性吗,支持回滚吗 Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。 ## 集群方案 ### 哨兵模式 <img src="https://img-blog.csdnimg.cn/20200115174006561.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly90aGlua3dvbi5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70" alt="img" style="zoom:67%;" /> #### 哨兵的介绍 sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能: - 集群监控:负责监控 redis master 和 slave 进程是否正常工作。 - 消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。 - 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。 - 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。 **哨兵用于实现 redis 集群的高可用**,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。 - 故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。 - 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。 **哨兵的核心知识** - 哨兵至少需要 3 个实例,来保证自己的健壮性。 - 哨兵 + redis 主从的部署架构,是**不保证数据零丢失**的,只能保证 redis 集群的高可用性。 - 对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。 #### 官方Redis Cluster 方案 > redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗? **简介** Redis Cluster是一种服务端Sharding(把一个数据库切分成多个部分放到不同的数据库server上,从而缓解单一数据库的性能问题)技术,3.0版本开始正式提供。Redis Cluster并没有使用一致性hash,而是采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行。Redis Cluster要求至少需要3个master才能组成一个集群,同时每个master至少需要有一个slave节点。 <img src="https://pic1.zhimg.com/80/v2-7be734194f5f92b6067761ada15c3e28_720w.jpg" alt="img" style="zoom:80%;" /> **方案说明** 1. 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值)区间的数据,默认分配了2^14(16384)个槽位 2. 每份数据分片会存储在多个互为主从的多节点上 3. 数据先写入主节点,再同步到从节点 4. 同一分片多个节点间的数据不保持一致性 5. 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点。 6. 扩容时需要把旧节点的数据迁移一部分到新节点。 在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。 **节点间的内部通信机制** 集群元数据的维护有两种方式:集中式、Gossip 协议。redis cluster 节点间采用 gossip 协议进行通信。 **说说Redis哈希槽的概念?** Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。 **Redis集群会有写操作丢失吗?为什么?** Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。 **Redis集群之间是如何复制的?** 异步复制 **Redis集群最大节点个数是多少?** 16384个 **Redis集群如何选择数据库?** Redis集群目前无法做数据库选择,默认在0数据库。 ## 分区 ### Redis是单线程的,如何提高多核CPU的利用率? 可以在同一个服务器部署多个Redis的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果你想使用多个CPU,你可以考虑一下分片(shard)。 ### 为什么要做Redis分区? 分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。 ### 你知道有哪些Redis分区实现方案? - 客户端分区:就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。 - 代理分区:意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。redis和memcached的一种代理实现就是Twemproxy - 查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。 ### Redis分区有什么缺点? - 涉及多个key的操作通常不会被支持。例如你不能对两个集合直接使用交集指令,因为他们可能被存储到不同的Redis实例 - 同时操作多个key,则不能使用Redis事务. - 分区使用的粒度是key,不能使用一个非常长的排序key存储一个数据集 - 当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的Redis实例和主机同时收集RDB / AOF文件。 ## 分布式问题 ### Redis实现分布式锁 Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。 SETNX:当且仅当 key 不存在,将 key 的值设为 value。 若给定的 key 已经存在,则 SETNX 不做任何动作,SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。**设置成功,返回 1 。设置失败,返回 0 **。 为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间 ### 如何解决 Redis 的并发竞争 Key 问题 分布式锁,如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能 ### 什么是 RedLock Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫 *Redlock*,此种方式比原先的单节点的方法更安全。它可以保证以下特性: 1. 安全特性:互斥访问,即永远只有一个 client 能拿到锁 2. 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区 3. 容错性:只要大部分 Redis 节点存活就可以正常提供服务 ## 缓存异常 ### 缓存雪崩 **缓存雪崩**是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。 **解决方案** 1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。 2. 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。 3. 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。 ### 缓存穿透 **缓存穿透**是指缓存和数据库中**都没有数据**,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。 **解决方案** 1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截; 2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击 3. 采用布隆过滤器(redis底层采用bitmap),将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力 ### 缓存击穿 **缓存击穿**是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。 **解决方案** 1. 设置热点数据永远不过期。 2. 加互斥锁,互斥锁 ### 缓存预热 **缓存预热**就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据! **解决方案** 1. 直接写个缓存刷新页面,上线时手工操作一下; 2. 数据量不大,可以在项目启动的时候自动进行加载; 3. 定时刷新缓存; ### 缓存降级 缓存降级是指当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,即使是有损部分其他服务,仍然需要保证主服务可用。可以将其他次要服务的数据进行缓存降级,从而提升主服务的稳定性。 降级的目的是保证核心服务可用,即使是有损的。如双十一的时候淘宝购物车无法修改地址只能使用默认地址,这个服务就是被降级了,这里阿里保证了订单可以正常提交和付款,但修改地址的服务可以在服务器压力降低,并发量相对减少的时候再恢复。 ### 热点数据和冷数据 热点数据,缓存才有价值,数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。 ### 缓存热点key 缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期,一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。 **解决方案** 对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询 ## 相关问题 ### 如何保证缓存与数据库双写时的数据一致性? **延迟双删策略** 先清除缓存,然后再写入数据库。有可能存在删除缓存以后,另一个线程读取数据,发现没有数据,就去数据读取数据,然后写入缓存中,此时缓存中的数据为脏数据; 解决办法: 1. 先删除缓存 2. 再写入数据库 3. 休眠一定时间(根据统计线程读取数据和写缓存的时间) 4. 删除缓存 ### Redis常见性能问题和解决方案? 1. Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化。 2. 如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。 3. 为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。 4. 尽量避免在压力较大的主库上增加从库 5. 为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现Slave对Master的替换,也即,如果Master挂了,可以立马启用Slave1做Master,其他不变。 ### 一个字符串类型的值能存储最大容量是多少? 512M ### Redis如何做大量数据插入? 采用pipe mode模式 ### 假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来? 使用keys指令可以扫出指定模式的key列表。 redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用**scan指令**(用于迭代数据库中的数据库键),scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。 ### 使用Redis做过异步队列吗,是如何实现的 使用list类型保存数据信息,rpush生产(插入)消息,lpop消费(使用)消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。(redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。) ### Redis如何实现延时队列 使用zset,使用时间戳做排序, 消息内容作为key,调用zadd来生产消息,消费者使用zrangbyscore(返回zset中指定分数区间的成员列表)获取n秒之前的数据做轮询处理。 ### Redis回收进程如何工作的? 1. 删除到达时间的键对象。 2. 内存使用达到maxmemory(最大内存)上限时触发内存溢出控制策略。 ##### 内存溢出控制策略 当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。具体策略受maxmemory-policy参数控制,Redis支持6种策略,如下所示: 1. noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。 2. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。推荐使用,目前项目在用这种。 3. allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。应该也没人用吧,你不删最少使用 Key,去随机删。 4. volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。不推荐。 5. volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。依然不推荐。 6. volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。不推荐。如果没有对应的键,则回退到noeviction策略。 ### Redis回收使用的是什么算法? 一种近似的 LRU 算法的实现 LRU是一种缓存置换算法。即在缓存有限的情况下,如果有新的数据需要加载进缓存,则需要将最不可能被继续访问的缓存剔除掉。因为缓存是否可能被访问到没法做预测,所以基于已有访问的次数实现该算法 4.0后采用LFU算法 淘汰一段时间内使用次数最少的