这个我的理解大致是这样的面试官!!
Redis有5种基本数据类型它们分别是String、List、Hash、Set、ZSet;此外还有三种特殊数据类型Bitmaps、Geospatial、HyperLogLog
| 数据类型 | 简单描述 | 使用场景 |
| — | — | — |
| String | string(字符串)是Redis最简单也是使用最广泛的数据结构,它的内部是一个字符数组。String(字符串)是动态字符串,允许修改;它在结构上的实现类似于Java中的ArrayList(默认构造一个大小为10的初始数组),这是冗余分配内存的思想,也称为预分配;这种思想可以减少扩容带来的性能消耗。当string(字符串)的大小达到扩容阈值时,将会对string(字符串)进行扩容,string(字符串)的扩容主要有三种情况:1.长度小于1MB,扩容后为原先的两倍; length = length * 2 2.长度大于1MB,扩容后增加1MB; length = length + 1MB 3. 字符串的长度最大值为 512MB | 缓存、计数器、分布式锁等。 |
| List | Redis的列表相当于Java语言中的LinkedList,它是一个双向链表数据结构(但是这个结构设计比较巧妙,后面会介绍),支持前后顺序遍历。链表结构插入和删除操作快,时间复杂度O(1),查询慢,时间复杂度O(n)。Redis的list(列表)不是一个简单。LinkedList,而是quicklist ——“快速列表”,quicklist是多个ziplist(压缩列表)组成的双向列表; | 链表、异步队列、微博关注人时间轴列表…… |
| Hash | Redis的hash(字典)相当于Java语言中的HashM
【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
ap,它是根据散列值分布的无序字典,内部的元素是通过键值对的方式存储。hash(字典)的实现与Java中的HashMap(JDK1.7)的结构也是一致的,它的数据结构也是数组+链表组成的二维结构,节点元素散列在数组上,如果发生hash碰撞则使用链表串联在数组节点上。Redis中的hash(字典)存储的value只能是字符串值,此外扩容与Java中的HashMap也不同。Java中的HashMap在扩容的时候是一次性完成的,而Redis考虑到其核心存取是单线程的性能问题,为了追求高性能,因而采取了渐进式rehash策略。渐进式rehash指的是并非一次性完成,它是多次完成的,因此需要保留旧的hash结构,所以Redis中的hash(字典)会存在新旧两个hash结构,在rehash结束后也就是旧hash的值全部搬迁到新hash之后,新的hash在功能上才会完全替代以前的hash。 | 用户信息、Hash 表…… |
| Set | Redis的set(集合)相当于Java语言里的HashSet,它内部的键值对是无序的、唯一的。它的内部实现了一个所有value为null的特殊字典。集合中的最后一个元素被移除之后,数据结构被自动删除,内存被回收。 | 去重功能、赞、踩、共同好友…… |
| ZSet | zset(有序集合)是Redis中最常问的数据结构。它类似于Java语言中的SortedSet和HashMap的结合体,它一方面通过set来保证内部value值的唯一性,另一方面通过value的score(权重)来进行排序。这个排序的功能是通过Skip List(跳跃列表)来实现的。zset(有序集合)的最后一个元素value被移除后,数据结构被自动删除,内存被回收。 | 粉丝列表、学生成绩排序、访问量排行榜、点击量排行榜…… |
| Bitmaps | Bitmaps 称为位图,严格来说它不是一种数据类型。Bitmaps底层就是字符串(key-value)byte数组。我们可以使用普通的get/set直接获取和设值位图的内容,也可以通过Redis提供的位图操作getbit/setbit等将byte数组看成“位数组”来处理。Bitmaps 的“位数组”每个单元格只能存储0和1,数组的下标在Bitmaps中称为偏移量。Bitmaps设置时key不存在会自动生成一个新的字符串,如果设置的偏移量超出了现有内容的范围,就会自动将位数组进行零扩充 | 员工打卡…… |
| Geospatial | Geospatial是Redis在3.2版本以后增加的地理位置GEO模块 | 微信附近的人,在线点餐“附近的餐馆”…… |
| HyperLogLog | HyperLogLog是用来做基数统计的算法,它提供不精确的去重计数方案(这个不精确并不是非常不精确),标准误差是0.81%,对于UV这种统计来说这样的误差范围是被允许的。HyperLogLog的优点在于,输入元素的数量或者体积非常大时,基数计算的存储空间是固定的。在Redis中,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^64个不同的基数。但是HyperLogLog只能统计基数的大小(也就是数据集的大小,集合的个数),他不能存储元素的本身,不能向set集合那样存储元素本身,也就是说无法返回元素。 | 基数统计比如UV等 |
这个我的理解大致是这样的面试官!!
Redis的数据结构均可以通过EXPIRE key seconds 的方式设置key的过期时间(TTL)。我们也习惯的认为Redis的key过期时间到了,就会自动删除,显然这种想法并不正确。Redis的设计考虑到性能/内存等综合因素,设计了一套过期策略。
主动删除(惰性删除)
被动删除(定期策略)
主动删除(惰性删除)指的是当key被访问的时候,先校验key是否过期,如果过期了则主动删除。 被动删除(定期策略)指的是Redis服务器定时随机的测试key的过期时间,如果过期了则被动删除。被动删除的存在必不可少,因为存在一些过期且永久不在访问的key,如果都依赖主动删除,那么它们将会永久占用内存。 Redis为了保证提供高性能服务,被动删除过期的key,采用了贪心策略/概率算法,默认每隔10秒扫描一次,具体策略如下:
从过期字典(设置了过期时间的key的集合)中随机选择20个key,检查其是否过期
删除其中已经过期的key
如果删除的过期key数量大于25%,则重复步骤1
此外开发在设计Redis缓存架构时,一定要注意要尽可能的避免(禁止)将大量的key设置为同一过期时间,因为结合被动删除可知,Redis被动删除过期key时,会导致服务短暂的不可用;如果存在大量key同时过期,这会导致被动删除key的三个步骤循环多次,从而导致Redis服务出现卡顿情况,这种情况在大型流量项目是无法接收的。 因此为了避免这种情况出现,一定要将一些允许过期时间不需要非常精确的key,设置较为随机的过期时间,这样就可以将卡顿时间缩小。
这个我的理解大致是这样的面试官!!
在分布式场景中我们常见的分布式锁解决方案有(如果自己都会可以把其他两种也在这带出来,如果不会那就别把自己坑了呀!):
基于数据库锁机制实现的分布式锁
基于Zookeeper实现的分布式锁
基于Redis实现的分布式锁
而关于Redis实现分布式锁的方案是这样的。 如果Redis是在单机环境中我们可以通过,Redis提供的原子指令来实现分布式锁
set key value [EX seconds] [PX milliseconds] [NX|XX]
为了防止A加的锁,被B删除了,可以加锁时传入客户端加锁标记,只有当客户端传入的标记和锁标记相同时才允许解锁,不过Redis并未提供这样的功能,我们只能通过Lua脚本来处理,因为Lua脚本可以保证多个指令的原子性执行。最后我们还要考虑锁的超时问题,如果客户端一直不释放锁肯定也是不行的,因此锁只能保证在指定的超时时间范围内不被其他客户端解锁,超时之后就自动释放了,这种情况很难我们可以这样优化:
尽可能不要在Redis分布式锁中执行较长的任务,尽可能的缩小锁区间内执行代码,就像单JVM锁中的synchronized优化一样,我们可以考虑优化锁的区间
多做压力测试和线上真实场景的模拟测试,估算一个合适的锁超时时间
做好Redis分布式锁超时任务未执行完的问题发生后,数据恢复手段的准备
如果是在分布式环境中,会增加一个新的问题,比如sentinel+一主多从环境中,可能存在客户端在主节点上申请了锁,但是同步未完成,主节点宕机了,此时新选举的主节点上锁是失效的。 对于这种情况的处理应该是这么考虑的,首先Redis主从同步直接无论如何都无法解决数据会有丢失的情况。所以我们考虑把像一个Redis申请锁,变成像多个单机Redis申请锁,只有大部分申请成功就行。这种思想就是RedLock(红锁)。 RedLock通过使用多个Redis实例,各个实例之间没有主从关系,相互独立;加锁的时候,客户端向所有的节点发送加锁指令,如果过半的节点set成功,就加锁成功。释放锁时,需要向所有的节点发送del指令来释放锁。 红锁虽然解决了主从同步的问题,但是带来新的复杂问题:
第一个问题是时钟漂移
第二个问题是客户端像不同的Redis服务端申请锁成功的时间是不同的
因此在RedLock中需要计算申请的锁的最小有效时长。假设客户端申请锁成功,第一个key设置成功的时间为TF,最后一个key设置成功的时间为TL,锁的超时时间为TTL,不同进程之间的时钟差异为CLOCK_DIFF,则锁的最小有效时长是:
TIME = TTL - (TF- TL) - CLOCK_DIFF
采用Redis来实现分布式锁,离不开服务器宕机等不可用问题,这里RedLock红锁也一样,即使是多台服务器申请锁,我们也要考虑服务器宕机后的处理,官方建议采用AOF持久化处理。 但是AOF持久化只对正常SHUTDOWN这种指令能做到重启恢复,但是如果是断电的情况,可能导致最后一次持久化到断电期间的锁数据丢失,当服务器重启后,可能会出现分布式锁语义错误的情况。所以为了规避这种情况,官方建议Redis服务重启后,一个最大客户端TTL时间内该Redis服务不可用(不提供申请锁的服务),这确实可以解决问题,但是显而易见这肯定影响Redis服务器的性能,并且在多数节点都出现这种情况的时候,系统将出现全局不可用的状态。
这个我的理解大致是这样的面试官!!
Redis的非常快,很大一部分原因是因为Redis的数据存储在内存中,既然在内存中,那么当服务器宕机或者断电的时候,数据就会全部丢失了,所以Redis提供了两种机制来保证Redis数据不会因为故障而全部丢失,这种机制称为Redis的持久化机制。 Redis的持久化机制有两种:
RDB(Redis Data Base) 内存快照
AOF(Append Only File) 增量日志
RDB(Redis DataBase) 指的是在指定的时间间隔内将内存中的数据集快照写入磁盘,RDB是内存快照(内存数据的二进制序列化形式)的方式持久化,每次都是从Redis中生成一个快照进行数据的全量备份。 优点:
存储紧凑,节省内存空间
恢复速度非常快
适合全量备份、全量复制的场景,经常用于灾难恢复(对数据的完整性和一致性要求相对较低的场合)
缺点:
容易丢失数据,容易丢失两次快照之间Redis服务器中变化的数据。
RDB通过fork子进程对内存快照进行全量备份,是一个重量级操作,频繁执行成本高。
fork子进程,虽然共享内存,但是如果备份时内存被修改,最大可能膨胀到2倍大小。
RDB触发的规则分为两大类,分别是手动触发和自动触发: 自动触发:
配置触发规则
shutdown触发
flushall触发
手动触发:
save
bgsave
**AOF(Append Only File)**是把所有对内存进行修改的指令(写操作)以独立日志文件的方式进行记录,重启时通过执行AOF文件中的Redis命令来恢复数据。AOF能够解决数据持久化实时性问题,是现在Redis持久化机制中主流的持久化方案(后续会谈到4.0以后的混合持久化)。 优点:
数据的备份更加完整,丢失数据的概率更低,适合对数据完整性要求高的场景
日志文件可读,AOF可操作性更强,可通过操作日志文件进行修复
缺点:
AOF日志记录在长期运行中逐渐庞大,恢复起来非常耗时,需要定期对AOF日志进行瘦身处理(后续详述)
恢复备份速度比较慢
同步写操作频繁会带来性能压力
AOF日志是以文件的形式存在的,当程序对AOF日志文件进行写操作时,实际上将内容写到了内核为文件描述符分配的一个内存缓冲区中,随后内核会异步的将缓冲区中的数据刷新到磁盘中。如果缓冲区中的数据没来得及刷回磁盘时,服务器宕机了,这些数据就会丢失。 因此Redis通过调用Linux操作系统的glibc提供的fsync(int fid)来将指定文件的内容强制从内核缓冲区刷回磁盘,以此来保证缓冲区中的数据不会丢失。不过这是一个IO操作,相比Redis的性能来说它是非常慢的,所以不能频繁的执行。 Redis配置文件中有三种刷新缓冲区的配置:
appendfsync always 每次Redis写操作,都写入AOF日志,这种配置理论上Linux操作系统扛不住,因为Redis的并发远远超过了Linux操作系统提供的最大刷新频率,就算Redis写操作比较少的情况,这种配置也是非常耗性能的,因为涉及到IO操作,所以这个配置基本上不会用
appendfsync everysec 每秒刷新一次缓冲区中的数据到AOF文件,这个Redis配置文件中默认的策略,兼容了性能和数据完整性的折中方案,这种配置,理论上丢失的数据在一秒钟左右
appendfsync no Redis进程不会主动的去刷新缓冲区中的数据到AOF文件中,而是直接交给操作系统去判断,这种操作也是不推荐的,丢失数据的可能性非常大。
前面提到AOF的缺点时,说过AOF属于日志追加的形式来存储Redis的写指令,这会导致大量冗余的指令存储,从而使得AOF日志文件非常庞大,这种情况不仅占内存,也会导致恢复的时候非常缓慢,因此Redis提供重写机制来解决这个问题。Redis的AOF持久化机制执行重写后,保存的只是恢复数据的最小指令集,我们如果想手动触发可以使用如下指令
bgrewriteaof
Redis4.0后的重写使用的是RDB快照和AOF指令拼接的方式,在AOF文件的头部是RDB快照的二进制形式的数据,尾部是快照产生后发生的写入操作的指令。 由于重写AOF文件时,会对Redis的性能带来一定的影响,因此也不能随便的进行自动重写,Redis提供两个配置用于自动进行AOF重写的指标,只有这两个指标同时满足的时候才会发生重写:
auto-aof-rewrite-percentage 100:指的是当文件的内存达到原先内存的两倍
auto-aof-rewrite-min-size 64mb:指的是文件重写的最小内存大小
此外Redis4.0后大部分的使用场景都不会单独使用RDB或者AOF来做持久化机制,而是兼顾二者的优势混合使用。 最后来总结这两者,到底用哪个更好呢?
推荐是两者均开启
如果对数据不敏感,可以选单独用RDB
不建议单独用AOF,因为可能会出现Bug
如果只是做纯内存缓存,可以都不用
这个我的理解大致是这样的面试官!!
Redis是基于内存存储的key-value数据库,我们知道内存虽然快但空间小,当物理内存达到上限时,系统就会跑的很慢,所以我们会设置Redis的最大内存,当Redis内存达到设定阈值的时候会触发内存回收,Redis提供了很多内存淘汰策略:
**noeviction:**当达到内存限制并且客户端尝试执行可能导致使用更多内存的命令时返回错误,简单来说读操作仍然允许,但是不准写入新的数据,del(删除)请求可以。
**allkeys-lru:**从全体key中,通过lru(Least Recently Used - 最近最少使用)算法进行淘汰
**allkeys-random:**从全体key中,随机进行淘汰
**volatile-lru:**从设置了过期时间的全部key中,通过lru(Least Recently Used - 最近最少使用)算法进行淘汰,这样可以保证未设置过期时间需要被持久化的数据,不会被选中淘汰
**volatile-random:**从设置了过期时间的全部key中,随机进行淘汰
**volatile-ttl:**从设置了过期时间的全部key中,通过比较key的剩余过期时间TTL的值,TTL越小越先被淘汰
**volatile-lfu:**对有过期时间的key采用LFU淘汰算法