Redis
谈谈Redis的认识
Redis是一个高性能非关系型(NoSQL)的键值对数据库。Redis可以存储键和五种不同数据类型的值之间的映射。键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、哈希、有序集合。与传统的数据库不同的是,Redis的数据是存在内存中的,所以读写速度非常快,因此Redis被广泛应用于缓存方向。
-
优点:
读写性能优异;支持数据持久化;支持事物,操作都是原子性的;数据结构丰富;支持主从复制,可以进行读写分离 -
缺点:
数据库容量受物理内存的限制,不能用作海量数据的高性能读写;Redis不具备自动容错和恢复功能;主机宕机,宕机前有部分数据未能及时同步到从机,会引入数据不一致的问题,降低了系统的可用性;Redis较难支持在线扩容
Redis基础
Redis中的数据类型有哪些
Redis 有 5 种基础数据类型,分别为:string (字符串)、list (列表)、set (集合)、hash (哈希) 和 zset (有序集合)。
-
Redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n),这点让人非常意外。 当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。
-
Redis 的哈希相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同 Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。
-
Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值NULL。 当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。
-
zset 类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓文字恐惧症的可以看下面的对比表↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
类型 | 底层实现描述 |
---|---|
String(字符串) | 最基础的数据类型。value除了是字符串还可以是数字。底层是char类型的数组 。 |
List(列表) | 相当于Java语言里面的LinkedList,注意它是链表而不是数组。这意味着list的插入和删除操作非常快,时间复杂度为O(1),但是索引定位速度非常慢,时间复杂度为O(n)。当列表弹出最后一个元素后,改数据结构自动被删除,内存被回收。底层是双向链表。 |
Hash(哈希) | 相当于Java语言里面的HashMap,它是无序字典。内部实现结构上同Java的HashMap是一致的,同样的数组+链表的二维机构。第一维Hash的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。 底层是散列表结构,使用拉链法解决Hash冲突。 |
Set(集合) | 相当于Java语言里面的HashSet,它内部的键值对是无序的唯一的。它的内部相当于一个特殊的字典,字典中所有的value都是一个值NULL。当集合中最后有个元素移除后,数据结构自动删除,内存被回收。底层是ZipList(压缩列表)、HashTable(通过挂链解决冲突问题)。 |
ZSet(有序集合) | 相当于Java语言里面的SortedSet和HashMap的结合体,一方面它是一个set,保证内部value的唯一性,另一方面它可以给每个value赋予一个score,代表这个value的排序权重。在Set的基础上加了一个分值(score)。ZSet可以实现有序性操作,从而实现排行榜等功能。 |
Redis中的数据结构能详细说下吗
String(字符串)
Redis是使用C语言开发,但是C中并没有字符串类型,只能使用指针或符数组的形式表示一个字符串,所以Redis设计了一种简单动态字符串(SDS[Simple Dynamic String])作为底实现:
定义SDS对象,此对象中包含三个属性:
- len buf中已经占有的长度(表示此字符串的实际长度)
- free buf中未使用的缓冲区长度
- buf[]实际保存字符串数据的地方
所以取字符串长度的时间复杂度为O(1),另外,buf[]中依然采用了C语言的以\0结尾,所以可以直接使用C语言的部分标准C字符串库函数。
控件分配原则:当len小于1MB(1024*1024)时,增加字符串分配控件大小为原来的2倍,当len大于等于1M时每次分配额外多分配1M空间。
由此可以得出以下特性:
- Redis为字符串分配空间的次数是小于等于字符串长度N,而原C语言中的分配则必须为N。降低了分配次数提高了追加速度,代价就是多占用一些内存控件,且这些空间不会自动释放。
- 二进制安全的
- 高效的计算字符串长度(时间复杂度为O(1))
- 高效的追加字符串操作
List(列表)
Redis对键-表结构的支持,使得它在键值储存的世界中独树一帆,一个列表结构可以有序的存储多个字符串。拥有例如:Ipush、Ipop、rpush、rpop等等操作命令。
在3.2版本前,列表使用zipList和linkedList实现,在这些老版本中,当列表对象同时满足两个条件时,列表对象使用zipList编码:
- 列表对象保存的所有字符串元素的长度都小于64字节
- 列表对象保存的元素数量小于512个
当有任意条件 不满足 时将会进行一次转码,使用linkedList。
在3.2版本后,重新引入一个quickList的数据结构,列表底层都是由quickList实现,它结合了zipList和linkedList的优点。按照原文的翻译这种数据结构式【A Double linked list of ziplists】意思是一个由ziplist组成的双向链表。
zipList和linkedList两种数据结构是怎样结合的?
- zipList的结构
由表头和N个entry节点和压缩列表尾部标识符zlend组成的一个连续的内存块。通过一系列的编码规则,提高内存的利用率,主要用于存储正数和比较短的字符串。可以看出在插入和删除元素的时候,都需要对内存进行一次扩展和缩减,还要进行部分数据的移动操作,这样会造成更新效率低下的情况。- linkedList的结构
意思为一个双向链表,和普通的链表定义相同,每个entry包含向前向后的指针,当插入或删除元素的时候,只需要对此元素前后指针操作即可。所以插入和删除元素的效率都很高。但查询的效率却很低O(n)[n为元素的个数]了解了这两种数据结构,在来看看ziplist组成的双向链表是什么意思?实际上,它整体宏观上就是一个链表接个够,只不过每个节点都是压缩表zipList的结构保存着数据,而每个zipList又可以包含多个entry。也可以说一个quickList节点保存的是一片数据,而不是一个数据。
总结
- 整体上quickList就是一个双向列表结构,和普通的链表操作一样,插入和删除效率很高O(1),但查询效率很低O(n)。不过这样的链表访问两端的元素的时间复杂度却是O(1)。所以,对list的操作多数都是poll和push
- 每个quickList节点就是一个zipList,具备压缩列表的特性
Set(集合)
Redis的集合和列表都可以存储多个字符串,它们之间的不同在于,列表可以存储多个相同的字符串,而集合则通过使用散列表(hashTable)来保证自己存储的每个字符串都是各不相同的(这些三列表只有键,但没有与键相关联的值),Redis中的集合是无序的。还可能存在另一种集合,那就是intset,他是用于存储正数的有序集合,里面存放同一类型的正数,共有3中正数:int16_t、int32_t、int64_t。查找的时间复杂度为O(logN),但是插入的时候,会涉及到升级(比如:原来是int16_t的集合,当插入int32_t的整数的时候就会为每个元素升级为int32_t)这时候会对内存重新分配,所以此时的时间复杂度就是O(N)级别的了。注意:intset只支持升级不支持降级操作。
intset在redis.config中也有一个配置参数set-max-intset-entries默认值为512.表示如果entry的个数小于此值,则可以编码成REDIS_ENCODING_INTSET类型存储,节约内存,否则采用dict的形式存储。
Hash(哈希)
Hash底层的数据结构实现有两种:
-
一种是zipList,上面已经提到过。当存储的额数据超过配置的阈值时就会专用hashTable的结构。这种转换比较消耗性能,所以应该尽量避免这种转换操作。同时满足一下两个条件才会使用这种结构:
条件一:当键的个数小于hash-max-ziplist-entries(默认512)
条件二:当所有值都小于hash-max-ziplist-value(默认64) -
另一种是hashtable。这种结构的时间复杂度为O(1),但是会消耗比较多的内存控件。
ZSet(有序集合)
有序集合和三列一样,都用于存储键值对:
有序集合的键被称为成员(memeber),每个成员都是各不相同的。有序集合的值则被称为分值(score), 分值必须为浮点数。有序集合是Redis里面唯一一个既可以根据成员访问元素(这一点和散列一样),又可以根据分值及分值的排列顺序访问元素的结构。它的储存方式也有两种:
-
一种是ziplist:
与上面hash的ziplist类似,member和score顺序存放并按score顺序排列
-
另一种是skiplist与dict集合
skiplist是一种跳跃表结构,用于有序集合中快速查找,大多数情况下它的效率与平衡树差不多,但是比平衡树实现简单。Redis的作者对普通的跳跃表进行了修改,报错添加span\tail\backward指针、score的值可以重复这些设计,从而实现排序功能和反向遍历功能。
一般跳跃表的实现,主要包含以下几个部分:
- 表头(head):指向头结点
- 表尾(tail):指向尾结点
- 结点(node):实际保存的元素结点,每个节点可以有多层,层数是在创建此节点的时候随机生成的一个数值,而且每一层都是一个执行后面某个节点的指针。
- 层(level):目标表内节点的最大层数
- 长度(length):节点的数量
跳跃表的遍历总是从高层开始,然后随着元素值范围的缩小,慢慢减低到底层
跳跃表的实现原理可以参考:https://blog.csdn.net/Acceptedxukai/article/details/17333673
前面也说了,有序列表是使用skiplist和dict结合实现的,skiplist用来保障有序性和访问查找性能,dict就用来存储元素信息,并且dict的访问时间复杂度为O(1)。
Redis为什么这么快
- 绝大部分请求是纯粹的内存操作
- 采用单线程,避免了不必要的上下文切换和竞争条件
- 非阻塞IO - IO多路复用
那么Redis的单进程单线程模型的具体细节是怎样的?答案:IO多路复用技术
首先说一下,什么是IO多路复用技术。
比如,现在我们模拟一个tcp服务器处理30个客户的socket,如何快速的处理掉这30个请求呢?
在不了解原理的情况下,我们类比一个实例:在课堂上让全班30个人同时做作业,做完后老师检查,30个学生的作业都检查完成才能下课。如何在有限的资源下,以最快的速度下课呢?
第一种:安排一个老师,按顺序逐个检查。先检查A,然后是B,之后是C、D。。。这中间如果有一个学生卡住,全班都会被耽误。这种模式就好比,你用循环挨个处理socket,根本不具有并发能力。这种方式只需要一个老师,但是耗时时间会比较长。
第二种:安排30个老师,每个老师检查一个学生的作业。 这种类似于为每一个socket创建一个进程或者线程处理连接。这种方式需要30个老师(最消耗资源),但是速度最快。
第三种:安排一个老师,站在讲台上,谁解答完谁举手。这时C、D举手,表示他们作业做完了,老师下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A。这种方式可以在最小的资源消耗的情况下,最快的处理完任务。
第三种就是IO复用模型(Linux下的select、poll和epoll就是干这个的。将用户socket对应的fd注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。这样,整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor模式。)
参考地址
Redis的持久化机制有哪些,优缺点及使用建议
Redis的持久化机制
RDB 持久化
RDB持久化即通过创建快照(压缩二进制文件)的方式进行持久化,保存某个时间点的全量数据。RDB持久化是Redis默认的持久化方式。RDB持久化的触发包括手动触发和自动触发两种方式。
AOF 持久化
AOF(Append-Only-File)持久化即记录所有变更数据库状态的指令,以append的形式追加保存到AOF文件中,在服务器下次启动时,就可以通过载入和执行AOF文件中保存的命令,来还原服务器关闭前的数据库状态度。
RDB、AOF混合持久化
Redis从4.0开始支持RDB和AOF混合持久化方案。首先有RDB定期完成内存快照的备份,然后再由AOF完成两次RDB之间的数据备份,由这两部分共同构成持久化文件。该方案的有点事充分利用RDB加载快、备份文件小及AOF尽可能不丢失的特性。缺点是兼容性差,一旦开启了混合持久化,在4.0之前的版本都不识别改持久化文件,同时由于前部分是RDB格式,阅读性较低。
Redis持久化方案的建议
- 如果Redis只是用来做缓存服务器,比如数据库查询数据后缓存,那可以不用考虑持久化,因为缓存服务时效还能再从数据库获取恢复。
- 如果想要提供很高的数据保障性,那么建议同时使用两种持久化方式,如果可以接受灾难带来的几分钟数据丢失,那么可以仅使用RDB持久化方式
- 通常的设计思路是,利用主从复制机制来弥补持久化时性能上的影响。即Master上的RDB、AOF都不做,保证Master的读写性能,二Slave上则同时开启EDB和AOF(或4.0以上版本得混合持久化方式)来进行持久化,保证数据的安全性。
Redis持久化方案的优缺点
RDB 持久化
- 优点:
RDB文件结构紧凑、体积小、网络传输快、适合全量复制;
与AOF对比,恢复速度快很多,对性能的影响相对较小 - 缺点:
RDB文件的致命缺点,在于其数据快照的持久化方式必然做不到实时持久化,而哎数据越老越重要的禁用,数据的大量丢失很多时候是无法接受的,因此AOF持久化成为主流。此外,RDB文件需要满足特定格式,兼容性差(如老版本的Redis不兼容新版本的RDB文件)
AOF持久化
- 优点:
与RDB持久化相对应,AOF的优点在于支持秒级持久化、兼容性好 - 缺点:
文件大、恢复速度慢、对性能影响大
Redis 缓存穿透、缓存击穿、缓存雪崩解决方案
缓存穿透
指查询数据库和缓存都没有的数据,可能导致DB挂掉。
解决方案:
方案 | 代码复杂度 | 效果 | 描述 |
---|---|---|---|
缓存空对象 | 代码简单 | 效果不好 | 缓存中会出现很多垃圾数据 |
布隆过滤器 | 代码复杂 | 效果很好 | 将所有可能存在的数据hash到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免对DB的查询 |
缓存击穿
对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把DB压垮。
解决方案:
- 使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时在进行load db 的操作并回设缓存,否则重试get缓存的方法
- 使用分布式锁:控制访问数据的锁定和开锁(不使用synchronized,因为会造成方法的锁定,导致所有人都无法使用此方法)
- 永远不过期:物理不过期,但逻辑过期(后台异步线程去刷新)
缓存雪崩
设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB, DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多 key,击穿是某一个key 缓存。
解决方案:
- 搭建Redis高可用集群(cluster)
- 降级 熔断
- 将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值
Redis 的集群模式有什么了解
主从复制
当数据库启动时,会向主数据库发送sync命令,主数据库接收到sync后开始在后台保存快照RDB,在保存快照期间,收到的命令缓存起来,当快照完成时,主数据库会将快照和缓存的命令一起发送给从。复制初始化结束之后,主每收到1个命令就会同步发送给从。当出现断开重连情况后,2.8之后的版本会将断线期间的命令传给从数据库(增量复制)
主从复制是乐观复制,当客户端发送写执行给主,主执行完立即将结果返回客户端,并异步的把命令发送给从,从而不影响性能。也可以设置至少同步给多少个从主才可写。 无硬盘复制:如果硬盘效率低将会影响复制性能,2.8之后可以设置无硬盘复制,repl-diskless-sync yes
哨兵模式
哨兵的作用:
- 监控Redis主、从数据库是否正常运行
- 主出现故障自动将从数据库转换为主数据库
哨兵的核心知识:
- 哨兵至少需要三个实例,来保证自己的健壮性
- 哨兵 + Redis 主从的部署架构,是不保证数据零丢失的,只能保证Redis集群的高可用性
- 哨兵 + Redis主从这种复杂的部署架构,尽量在测试环境和 生产环境,都惊醒充足的测试和演练
- 配置哨兵监控一个系统时,只需要配置起监控主数据库即可,哨兵会自动发现所有复制该主数据库的从数据库
Redis 分布式锁是怎么肥事
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
如果在setnx之后执行expire之前进程意外crash或者要重启维护了,这个锁就永远得不到释放了,使用set指令把setnx和expire合成一条指令来用
红锁
用Redis中的多个master实例,来获取锁,只有大多数实例获取到了锁,才算是获取成功。具体的红锁算法分为以下五步:
- step1. 获取当前的时间(单位是毫秒)。
- step2.使用相同的key和随机值在N个节点上请求锁。这里获取锁的尝试时间要远远小于锁的超时时间,防止某个 masterDown了,我们还在不断的获取锁,而被阻塞过长的时间。
- step3.只有在大多数节点上获取到了锁,而且总的获取时间小于锁的超时时间的情况下,认为锁获取成功了。
- step4.如果锁获取成功了,锁的超时时间就是最初的锁超时时间进去获取锁的总耗时时间。
- step5.如果锁获取失败了,不管是因为获取成功的节点的数目没有过半,还是因为获取锁的耗时超过了锁的释放时间,都会将已经设置了key的master上的key删除。
Redis 的淘汰机制你了解吗?
Redis 内存淘汰机制有以下几个:
- noeviction::当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。
Redis 缓存和Mysql数据不一致怎么肥事,怎么解决
咋肥事
- 第一个删Redis。如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
- 第二个删Redis。如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
解决方案:采用延时双删策略
- step1. 先淘汰缓存
- step1. 再写数据库(这两步和原来一样)
- step1. 休眠800ms,再次淘汰缓存
这么做,可以将800ms内所造成的缓存脏数据,再次删除。
Redis 的性能问题你怎么看,有什么好的解决方案
- Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件;(Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照;AOF文件过大会影响Master重启的恢复速度)
- 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次
- 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内
- 尽量避免在压力很大的主库上增加从库
- 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3…;这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变。
Redis 里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将它们全部找出来?
使用keys指令可以扫出指定模式的key列表。对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。
Redis事务有没有用过
采用的是Redis Cluster集群架构,不同的key是有可能分配在不同的Redis节点上的,在这种情况下Redis的事务机制是不生效的。其次,Redis事务不支持回滚操作,所以基本不用!
其他
布隆过滤器
Redis 跳表原理
一致性hash算法
不负韶华,只能朝夕