1、Redis的读写速率为什么要比普通关系型数据库更快?多线程的引入是为了解决什么问题?Pipeline管道知道多少?
Redis为什么快?
1、整体的数据结构以及每种数据类型的合理的编码结构。整体Key-Value的哈希表,查询时的时间复杂度是O(1),每种数据类型也进行了相应的优化,String->int->embstr->raw,List->LinkedList->ZipList->quickList,Hash->ZipList->HashTable,Set->IntSet->HashTable,ZSet->ZipList->SkipList 每种数据类型在存储不同类型数据时,大小时,Redis会择优选择。
2、Redis提供了两种持久化技术,但在对外提供服务时,Redis是一个内存型数据库,没有磁盘读写,内存读写迅速。
3、Redis在6.0之后引入了多线程,原本的单线程模型采用I/O多路复用技术,减少线程切换资源消耗的同时,也保证了原子性。基于以上,Redis的瓶颈主要在处理网络IO和内存大小上,使用多线程也仅限于接收和返回网络请求,真正的读写命令操作,还是由Redis主线程轮训执行。
4、Redis的虚拟内存机制,会将一部分冷数据交换到磁盘,腾出更多宝贵的内存空间来存放热数据。
多线程的引入解决了什么问题?
多线程主要致力于解决网络模块的瓶颈,通过使用多线程处理读/写客户端数据,进而分担主IO线程的压力,值得注意的是,命令处理仍然是单线程执行。
Pipeline管道又是什么?
客户端从 发送命令->命令等待->命令执行->结果返回 为一个完整的RTT过程,mget mset有效节约了RTT,但大部分命令(如hgetall,并没有mhgetall)不支持批量操作,需要消耗N次RTT ,这个时候需要pipeline来解决这个问题。Pipline就是将部分命令打包,一次性从客户端发送到服务端去执行,然后将结果批量返回客户端。
Pipeline pipe = jedis.pipelined();//获取jedis对象的pipeline对象
for(String key:keys){
pipe.del(key); //将多个key放入pipe删除指令中
}
pipe.sync(); //执行命令,完全此时pipeline对象的远程调用
2、Redis的数据类型有哪些?都常用于哪些场景?它们的底层数据结构又各是什么?
我们说的数据类型及相应的底层数据结构,都是在说Value值。从整个Redis结构去看,数据结构就是一张哈希表,Key的数据类型是String,Value的数据类型是RedisObject。
RedisObject里的type属性表示Value的具体数据类型,encoding表示相应的编码方式,ptr保存具体数值。
String
结构:
1、int:如果Value存入的是简单的整形数值,便用int数据结构来存储。
2、embstr:如果Value存入的是长度小于等于44的字符串,便用embstr数据结构来存储。
3、raw:如果Value存入的是长度大于44的字符串,便用raw数据结构来存储。
embstr和raw的底层数据结构都是SDS,即simple dynamic string。SDS有三个关键属性,buf[]数组存储每个字符,len长度记录buf[]数组长度,free长度记录buf[]数组的空余长度。SDS可以快速返回字符串长度、减少内存重新分配次数和避免缓存溢出。
使用场景:
1、缓存序列化后的对象信息 set key value
2、统计计数 incr命令 incr key num
List
结构:
1、LinkedList:有前后指针、有自己的长度信息
2、ZipList:是一组连续内存组成的顺序数据结构,节省空间,遍历速度更快,使用节点(内存块来存储数据。
3、quickList(3.2之后):是LinkedList和ZipList的组合
使用场景:
1、用作异步队列,lpush存,rpop取。
2、秒杀防止超卖,事先将资源加入队列,Redis单线程保证原子性。
3、流量削峰,将请求加入队列。
Hash
结构:
1、ZipList
2、HashTable:类似HashMap的数据结构,
使用场景:
1、缓存结构化信息
2、Redisson分布式锁的数据结构
set K1 k1 value:K1代表锁名称(资源Id+操作Id)、k1代表当前线程Id、value代表进入进入次数,是一个非阻塞可重复锁。hset Key1 Key2 value
Set
结构:
1、IntSet:在整数集合中,有三个属性值encoding、length、contents[],分别表示编码方式、整数集合的长度、以及元素内容,length就是记录contents里面的大小。
2、HashTable
使用场景:
1、数据去重 sadd Key Value
2、保证数据唯一性
ZSet
结构:
1、SkipList:多层单项顺序链表。
提取比例:每隔几个节点向上提取一层,默认值为0.5f(1/2,每隔两个向上提取一层;1/4,每隔四个向上提取一层)最高层数:16
节点个数从上到下由疏变密、每个节点除了有指向同层下一个节点的指针还有指向下一层相同节点的指针、节点会出现在下面每一层。
2、ZipList
使用场景:
1、评分、排序、统计 zadd Key Value
3、Redis的持久化方式有什么?
1、RDB:定时将内存中的数据以快照的形式保存到磁盘,每一次都是全量的新文件替换旧文件。
Redis有两种命令将快照文件保存到磁盘,save和bgsave。
执行save命令时,Redis会阻塞所有来自客户端的请求,是一个同步快照的操作,当内存中数据较多时,有可能导致Redis请求长时间未响应,所以 不推荐这种方式。
执行bgsave命令,会调用fork函数创建一个后台进程去执行快照操作,不会阻塞来自客户端的请求。
所以,在6.0之前说Redis是单线程也不准确,还有三个后台线程,close_file:关闭 AOF、RDB 等过程中产生的大临时文件、aof_fsync:将追加至 AOF 文件的数据刷盘、lazy_free:惰性释放大对象。
优点:直接的数据快照方式更适合做冷备和数据恢复;bgsave后台进程操作快照文件,不影响主进程处理请求;数据量大时,RDB的压缩文件直接数据恢复要比AOF日志文件执行命令恢复更快。
缺点:后台进程进行快照保存时,数据集也是上一次主进程处理请求后的数据,操作成本较高;高并发情况下数据丢失较多。
2、AOF:将每条修改命令以日志的形式追加到磁盘的AOF文件的末尾,顺序写入也没有磁盘寻址的开销,但执行写入日志操作可能会阻塞执行下一条修改命令,Redis默认情况不开启AOF。
Redis是单线程执行命令、执行写入日志操作,为了避免线程阻塞以及执行命令后的突然宕机,AOF提供了三种写回策略:
Always:同步执行写入日志操作,即每次执行完命令后就执行刷新日志到磁盘。
EverySec:执行完命令后,将日志写入AOF内存缓冲区,每隔一秒再刷新到磁盘。
No:执行完命令后,将日志写入AOF内存缓冲区,由系统决定何时写入磁盘。
优点:使用合适的写回策略数据丢失数据更少;AOF重写能让AOF文件体积相对减小,重写过程中,类似bgsave的后台进程会对AOF进行重写,而主进程则会将期间执行的命令写入AOF重写缓存,并且对重写后的文件进行改。
缺点:相对体积减小,但整体体积还是要比RDB文件更大;高并发情况下,同步写入日志到磁盘,还是会阻塞线程执行命令。
4、解释下缓存击穿、缓存穿透、缓存雪崩以及对应的解决方案?
1、缓存击穿:缓存中的Key失效,大量请求直接打到了数据库。
解决办法:
1、对Key进行提前预热
2、每次访问增加续时操作
3、不设置Key的过期时长
2、缓存穿透:缓存中的Key失效或压根就没有,数据库也没有。
解决办法:
1、网关拦截或者接口校验
2、缓存空值,但写入后记得更新缓存
3、使用BitMap作布隆过滤器,不存在的Key一定会被拦截
3、缓存雪崩:缓存中大量的Key失效,缓存击穿的批量事件。
解决办法:
1、同一类型的Key设置不同的过期时间(固定过期时间+随机过期时间)
2、服务端查询缓存时,设置一个短暂的随机延时时间
5、怎么实现Redis的高可用?
为了实现Redis的高可用,在生产环境一般采用集群化部署,集群里的主、从节点对外提供不同的写、读服务,以下是三种集群化部署模式。
1、主从模式
集群里的主节点提供读、写服务,从节点只提供读服务,从节点的数据来自主从复制。
当从节点启动第一次连接主节点,会采用RDB快照方式进行全量同步,之后会采用执行写命令的操作进行增量同步。
集群搭建方式简单,一主多从的架构使用方便,但主节点宕机后需要人为干预重新选举。
2、哨兵模式
为了解决人为干预的问题,Redis从2.8开始正式提供了Redis Sentinel(哨兵)架构来解决这个问题。你可以认为引入了一个哨兵监控系统来全程监控、参与选举来保证集群的高可用。每个节点都由多个哨兵来监控,以防止单个节点的主观问题。
单个哨兵会以每秒一次的频率向已知的Master、Slave以及其他哨兵发送PING命令,当节点的最后一次回复时间有效恢回复时间,该节点则会被当前哨兵标记为 主观下线。
当节点被标记为 主观下线 之后,监控该节点的其他的哨兵会继续以每秒一次的频率去PING该节点,当有足够数量的哨兵没有在指定时间内收到有效回复,该节点将被标记为 客观下线。
当有足够数量的哨兵在指定时间内又收到了节点的有效回复,客观下线 将被移除,当节点重新向当前哨兵发起有效回复,主观下线 将被移除。
有了哨兵的加入,不用人为的监控、干预,集群的健康状况和选举,但集群里的Master、Slave上的数据几乎是动态一致的,浪费内存并且不好在线扩容。
3、Cluster模式
为了解决上面的问题,Cluster集群应运而生,它在Redis3.0加入的,实现了Redis的分布式存储。对数据进行分片,也就是说每台Redis节点上存储不同的内容,来解决在线扩容的问题。并且,它也提供复制和故障转移的功能。
Cluster模式可以理解为,一个Redis集群有若干个节点,节点间相互通信,但不进行数据同步。节点大致可以分为两部分,一部分叫哈希槽,另一部分叫Cluster。
哈希槽的概念是,Redis集群中的若干个节点会平分16384个槽位,每个读写操作的Key经过计算后路由到正确的槽位(CRC16 % 16384)。例如Redis集群有三个节点,那A节点负责0~5460号哈希槽,B节点负责5461~10922号哈希槽,C节点负责10923~16383号哈希槽。
16384/1024/8大约是2K,一个Redis集群也不建议超过1000个节点,平衡消息的大小以及节点数量,选择16384个哈希槽。
Cluster可以理解为,每个节点也有自己的从节点,保证节点宕机后,启用从节点依旧能对外提供服务。整个Redis集群虽然有多个节点,但对外提供服务可以看作一个整体,节点间相互通信,保证了Key能够路由到正确的几点和槽位。
集群中某个节点发生故障,被从节点替换的过程称作故障转移。也有类似哨兵模式的 主观下线、客观下线,不同的是节点间相互监控,单个节点对当前节点标记为 主观下线 后,半数以上的节点同意,才能标记为 客观下线。而且当前节点的从节点也要得到其他节点半数以上的同意,才能替换当前节点。
6、使用过Redis分布式锁嘛?有哪些注意点呢?
项目中使用Redis分布式锁可能会有以下几种问题:
1、加锁和给锁添加过期时间分开写,可能会导致死锁问题。线程A在加锁之后,给锁添加过期时间之前突然终止,锁变的“长生不老”,后面的线程永远无法获取锁。
2、锁过期了,但业务操作还没结束,其他线程可能会再次获取锁。
3、锁被别的线程误删。
项目中我们可以直接引入Redisson客户端来实现分布式锁,以下是Redisson客户端加锁的原理以及优点:
1、首先要传入锁名称来获取一把锁。即Redisson分布式锁的数据结构是Hash(KEY Key Value),锁名称KEY,可以采用 资源ID+动作ID
2、Redisson分布式锁的加锁方法有lock()和tryLock()两种,lock()获取锁失败后,线程会加入自旋,tryLock()可以及时返回true/false,来进行下一步操作。
3、加锁方法和解锁方法最终都是执行一段Lua脚本来保证加锁、解锁的原子性。
加锁:首先判断KEY是否存在,不存在的话,设置KEY为锁名称,Key为当前线程ID,Value为进入次数,每次加1,设置过期时间,加锁成功;存在的话,Value再加1,设置过期时间,加锁成功。
解锁:首先判断KEY是否存在,不存在的话,解锁成功;存在的话,Value减1,判断Value是否大于0,大于0,设置过期时间,解锁失败,等待下一次解锁操作;否则解锁成功。
所以Redisson分布式锁是一种可重入锁。
4、加锁方法一般还有三个参数,分别是过期时间(默认30s)、最大等待时间(默认-1)、时间单位(默认毫秒),来防止提前释放锁以及可能造成的死锁。
三个参数都不设置的话,KEY的默认过期时间是30秒,WatchDog会每10秒(1/3时长)定时查看当前线程是否持有锁,并续时,直到业务操作结束,解锁。
设置了过期时间和最大等待时间的话,WatchDog将不会再续时,如果在最大等待时间后还没有执行完业务操作,锁会自动过期。
5、为了防止Master宕机下线,KEY尚未同步到Slave节点的问题,Redisson引入了RedLock算法,即在规定时间内,在互不通信的节点上,成功加锁的节点数量大于等于1/2节点总数+1,才算加锁成功。
7、MySQL与Redis 如何保证数据一致性?
缓存延时双删
这种操作可以理解为,当写请求先删除缓存失败后,执行了数据库写操作后,再二次删除缓存,其他线程的读操作,在二次删除缓存之前,还能读到错误缓存,但二次删除缓存之后,读到的就是最新数据了。可问题是,二次删除缓存操作也失败怎么办?
删除缓存重试机制
这种操作可以理解为,删除缓存操作无论在写库操作前还是后,失败的话就加入一种重试机制,可以是定时任务,也可以是其他第三方插件等。但这种编码和操作上都比较复杂。
读取biglog异步删除缓存
这种操作是一种异步操作,写库操作好触发binlog日志采集,将消息投递到消费队列,消费程序异步消费消息,执行删除缓存操作。略好于第二种方式。
8、Redis 过期策略和内存淘汰策略?
过期策略
1、定时过期:每个设置了过期时间的Key都需要创建一个定时任务,达到过期时间就会清理掉这个Key。优点是Key达到过期时间就被清理,及时释放内存,缺点是需要创建大量定时任务,CPU的使用率较高
2、惰性过期:每次访问这个Key时,判断过期时间是否到期,过期则删除。优点是节省了CPU使用消耗,缺点是大量过期,且没有被访问到的Key不会被清理,会一直占用内存。
3、定期过期:每隔一段时间去扫描Key是否达到过期时间,过期则会被清理。优缺点兼顾了上面两种过期策略。
Redis使用的是 惰性过期 + 定期过期 的过期策略。
内存淘汰策略
1、volatile-lru:当内存不足以容纳新写入数据时,从设置了过期时间的key中使用LRU(最近最少使用)算法进行淘汰;
2、allkeys-lru:当内存不足以容纳新写入数据时,从所有key中使用LRU(最近最少使用)算法进行淘汰。
3、volatile-lfu:4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用LFU(使用频率最少)算法进行删除key。
4、allkeys-lfu:4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU(使用频率最少)算法进行淘汰;
5、volatile-random:当内存不足以容纳新写入数据时,从设置了过期时间的key中,随机淘汰数据;。
6、allkeys-random:当内存不足以容纳新写入数据时,从所有key中随机淘汰数据。
7、volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的key中,根据过期时间进行淘汰,越早过期的优先被淘汰。
8、noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。
9、 聊聊Redis 事务机制?
Redis事务是一组隔离的命令操作,它将一组命令进行排队并按顺序执行,期间不允许其他客户端的命令插队。multi:开启事务;exec:执行事务;discard:取消事务
Redis的事务同样支持原子性,但是否回滚要看发生的是 组队错误 还是 运行错误。
组队错误 通常是指在命令排队时就发生的语义错误,例如:set key(不写value)、opp key value(没有opp指令),换句话说,在编译是就发生的错误,Redis的事务满足原子性,全部回滚,或者说压根就不执行。
运行错误 通常是指在执行命令时发生的数据错误,例如:incr key(但value并不是数字类型)、lpop key(但value并不是list结构),当通过编译后,发生在执行时的错误,Redis并不会回滚执行成功的命令,不满足所谓的原子性。
Redis经过迭代后,虽然加入了多线程,但执行命令时仍然是单线程,加上事务就是一种锁的形式,所以控制好语义才是Redis事务的关键。
Redis也可以通过Watch命令来实现乐观锁,客户端A、B都开启同一个Key的Watch模式,此时客户端A开启事务并执行命令,客户端B再开启事务并执行命令会报错,原因是开启Watch后客户端A、B都保留一个原始的版本号,客户端A执行完命令后,版本号发生了改变,客户端再执行命令时,发现版本号已经对不上了,所以会命令失败。
10、说说Redis的常用应用场景?
String、Hash可以用来作缓存(String存对象需要序列化和反序列化、Hash用来缓存对象结构更方便、HasH也是Redisson分布式锁的数据结构)
Incr key 可以用来做计数器
Zset Key Value score 可以用来做排行榜(Key:当日日期;Value:热搜词条;score:热度),zincrby Key Value 1(点击一下,热度加1)
Set可以去重、取交集
List可以做队列、栈(大量请求可以先入队,然后从另一端出,然后处理请求)
位操作
用于数据量上亿的场景下,例如几亿用户系统的签到,去重登录次数统计,某用户是否在线状态等等。
腾讯10亿用户,要几个毫秒内查询到某个用户是否在线,能怎么做?千万别说给每个用户建立一个key,然后挨个记(你可以算一下需要的内存会很恐怖,而且这种类似的需求很多。
这里要用到位操作——使用setbit、getbit、bitcount命令。原理是:redis内构建一个足够长的数组,每个数组元素只能是0和1两个值,然后这个数组的下标index用来表示用户id(必须是数字哈),那么很显然,这个几亿长的大数组就能通过下标和元素值(0和1)来构建一个记忆系统。
11、Redis 的 LRU 内存淘汰策略是怎么实现的?
LinkedHashMap 可以说实现了一部分 LRU 算法,LinedHashMap 的构造函数 accessOrder 参数可以控制其是插入顺序还是访问顺序,插入顺序是指,put()时的顺序,访问顺序是指,get()之后,该元素会被放在链表的末尾,调用 get()方法时,会先将后指针指向前指针的后指针,前指针指向后指针的前指针,目的是移除当前元素,然后调用 addBefore()方法,将元素添加到链表末尾,同时removeEldestEntry()方法会默认返回false。
用 LinkedHashMap 实现 LRU 算法,最简单的方法就是重写removeEldestEntry()方法,在达到预设的最大容量后,删除位于链表头的元素。
实际的 LRU算法描述的就是这样一种算法,即新插入的数据和访问到的数据都重新方法容器顶部,位于底部的数据自然就是不常用用到的数据,那么在设置一定的容器大小下,达到容器的最大上限,自动淘汰位于容器底的元素,就实现了 LRU 算法。
Redis 如果使用链表来实现 LRU 算法,需要额外的内存空间来保存 KV 链表,并且每次 KV 请求都会有额外的代码操作,所以采用了近似 LRU 的算法。首先设置了 LRU 全局时钟,并且在 KV 创建和获取时,都更新 V 的时间戳;其次,Redis 在处理命令时,都会先去判断使用内存是否超过了设置的 MaxMemory,如果超过了 MaxMemory,就会触发内存淘汰策略,随机选取一些 KV对,组成淘汰集合并根据时间戳淘汰最老的数据。