1 Redis 数据结构有哪些? zset数据结构?
- String
redis 的 string 可以包含任何数据. 如数字, 字符串, jpg图片或者序列化的对象
使用方法: get, set, del, incr, decr 等
实战场景:
- 缓存: 经典使用场景, 把常用的信息,字符串,图片等信息放到redis中, redis作为缓存层, mysql做持久化层,降低mysql的读写压力;
- 计数器: redis是单线程模型, 一个命令执行完才会执行下一个, 同时数据可以一步落地到其他的数据源;
- session: 常见方案spring session + redis 实现 session共享
-
Hash
是一个Mapmap, 指本身又是一种键值对结构, 如 value={{k1,v1},…{kn,vn}}
使用方法: 所有hash的命令都是 h 开头的 hget hset hdel 等
实战场景:
缓存: 能直观, 相比String 更节省空间的维护缓存信息, 如用户信息等 -
List
List 说白了就是链表(redis 使用双端链表实现的 List), 是有序的, value可以重复, 可以通过下标取出对应的value值, 左右两边都能进行插入和删除数据;
使用列表的技巧:- lpush + lpop = Stack
- lpush + rpop = Queue
- lpush + ltrim = Capped Collection(有限集合)
- lpush + brpop = message Queue(消息队列)
实战场景:
- timeline: 例如微博的时间轴, 有人发布微博, 用push 加入时间轴,展示新的列表信息
- Set
集合类型也是用来保存多个字符串的元素, 但和列表不同的是 集合中: 1. 不允许有重复的元素 2. 集合中的元素是无序的,不能通过索引下标获取元素 3. 支持集合间的操作,可以取多个集合取交集,并集,差集
方法: 命令都是以s开头的, sset, srem, scard, smemebers, sismemeber
实战场景:
- 标签(tag) 给用户添加标签, 或者用户给消息添加标签, 这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人;
- 点赞, 或点踩, 收藏等
-
zset
有序集合和集合有这必然的联系, 保留了集合不能有重复成员的特性,区别是,有序集合中的元素是可以排序的,它给每个元素设置一个分数,作为排序的依据(有序集合中的元素不可重复,但是分数可以重复;就想一个班里的同学学号不能重复,但考试成绩可以相同)
方法: 有序集合的命令都是以 z 开头, zadd, zrange, zscore
实战场景:
排行榜: 有序集合经典使用场景; 例如小说视频等网站需要对用户上传的内容做排行榜. 榜单可以按照用户关注数,更新时间,字数等打分,做排行.用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
以下高级部分-----加分内容
- Bitmap :
位图是支持按 bit 位来存储信息,可以用来实现 布隆过滤器(BloomFilter); - HyperLogLog:
供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计 UV; - Geospatial:
可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等。有没有想过用Redis来实现附近的人?或者计算最优地图路径?
这三个其实也可以算作一种数据结构,不知道还有多少朋友记得,我在梦开始的地方,Redis基础中提到过,你如果只知道五种基础类型那只能拿60分,如果你能讲出高级用法,那就觉得你有点东西。 - pub/sub:
功能是订阅发布功能,可以用作简单的消息队列。 - Pipeline:
可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答。 - Lua:
Redis 支持提交 Lua 脚本来执行一系列的功能。
2 聊聊redis的线程模型, 为啥单线程还能有很高的效率?
1)纯内存操作
2)核心是基于非阻塞的IO多路复用机制
3)单线程反而避免了多线程的频繁上下文切换问题
3 Redis 的hash(map)底层实现原理, 为什么不能过多?
redis的哈希对象的底层存储可以使用ziplist(压缩列表)和hashtable。当hash对象可以同时满足一下两个条件时,哈希对象使用ziplist编码。
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
- 哈希对象保存的键值对数量小于512个
当我们创建一个空的Hashes的时候使用的ziplist编码, 当某个键或某个值的长度大于hash_max_ziplist_value设定的值,会切换的Dict编码,还有一种情况也会切换就是ziplist的entries(节点数)大于hash_max_ziplist_entries。hash_max_ziplist_value和 hash_max_ziplist_entries在redis.conf中设置,默认值分别是512和64。
不能过多的原因猜测,可能是因为Redis是单线程模型, 如果Map过多,在进行读取操作的时候可能会影响性能
4 项目中Redis数据的更新方式
先更新数据库,后删除缓存
4.1 就不会有并发问题了吗?
依然存在,比如:如果先写了库, 在删除缓存前, 写库的线程当机了, 没有删除掉缓存,则也会出现数据不一致情况;
解决方案:
1. 数据库与缓存更新与读取操作进行异步串行化
更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个jvm内部的队列中
读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个jvm内部的队列中
2. 一个队列对应一个工作线程
每个工作线程串行拿到对应的操作,然后一条一条的执行
这样的话,一个数据变更的操作,先执行,删除缓存,然后再去更新数据库,但是还没完成更新
此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成(如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可)
待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中
如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回; 如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值
5 redis hash结构的扩容方案和 java hashmap扩容的一个区别,因为扩容阶段,一般是无法进行读写操作的,那redis是如何避免扩容导致的卡顿的(hash)
redis rehash与hashmap扩容的区别
redis的rehash,是将ht[0]的数据搬到ht[1] (ht[1]是ht[2]两倍),扩容过程中新加入的元素直接往ht[1]里面添加
hashmap的resize,创建一个新的数组,将旧的搬到新的数组中
redis的Hash在扩容时是如何避免卡顿的
rehash操作不是一次性、集中式完成的,而是分多次,渐进式,断续进行的,这样才不会对服务器性能造成影响。
dict中ht[2]中有两个hash表, 我们第一次存储数据的数据时, ht[0]会创建一个最小为4的hash表, 一旦ht[0]中的size和used相等, 则dict中会在ht[1]创建一个size*2大小的hash表, 此时并不会直接将ht[0]中的数据copy进ht[0]中, 执行的是渐进式rehash, 即在以后的操作(find, set, get等)中慢慢的copy进去, 以后新添加的元素会添加进ht[0], 因此在ht[1]被占满的时候定能确保ht[0]中所有的数据全部copy到ht[1]中.
6 持久化策略
持久化策略:master节点开启RDB和AOF写到本地磁盘
-
RDB持久化机制
RDB在做持久化操作的时候,是从redis主进程中fork出一个子进程,让子进程执行磁盘IO操作来进行RDB的持久化即可.因此它可以让Redis保持高性能;
RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中redis的数据,这种多个数据文件的形式,非常适合做冷备;
相对于AOF持久化机制来说, 直接基于RDB数据文件来重启和恢复redis进程,更加快速缺点:
(1) 如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好, 一般来说RDB数据快照文件,都是每隔5分钟或者更长时间生成一次. 这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据;
(2) RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大, 可能会导致对客户端提供的服务暂停数毫秒,甚至数秒 -
AOF持久化机制
AOF可以更好的保护数据不丢失, 一般AOF会每隔1秒, 通过一个后台线程执行一次 fsync 操作,最多丢失1秒钟的数据;
AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销, 写入性能非常高, 而且文件不容易破损,即使文件尾部破损,也很容易修复;
Redis可以在 AOF 文件体积变得过大的时候,自动的在后台对AOF进行重写;
AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复, 比如不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令删掉, 然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据;缺点:
对同一份数据来说,AOF日志文件通常比RDB数据快照文件更大
AOF开启后, 支持的写QPS会比RDB支持的写QPS低, 因为AOF会配置成每秒fsync一次日志文件,当然每秒一次fsync性能也还是很高的
RDB和AOF持久化机制该如何取舍
- 不要仅仅使用RDB,因为那样会导致你丢失很多数据
- 也不要仅仅使用AOF,因为那样有两个问题:第一,通过AOF做冷备,没有RDB做冷备,来的恢复速度更快;第二,RDB每次简单粗暴生成数据快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug
- 结合使用AOF和RDB两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择;用RDB来做不同程度的冷备,在AOF文件丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复
7 谈谈redis缓存雪崩、穿透 出现的场景以及防止手段?都有哪些监控措施?
-
缓存雪崩
现象:假设 系统A 其后台数据架构是 缓存+数据库; 该系统每天高峰期每秒有5000个请求,正常情况下,该系统的缓存高峰期可以抗每秒4000请求,数据库高峰期可抗每秒1000个请求;
而此时缓存由于某些原因宕机, 高峰的请求每秒5000个,直接落到数据库上, 数据库无法扛起每秒5000的请求,因此数据库直接被打死;就算重启数据库也会立马被新的流量给打死, 这就是缓存雪崩解决:
事前: redis 高可用(主从+哨兵, redis cluster(集群)) 避免全盘崩溃
事中: 本地再添加一个 ehcache 缓存+Hystrix限流&降级, 避免 MySQL数据库被打死
事后: redis持久化, 快速恢复缓存数据 -
缓存穿透:
现象:还是以上面的系统A为例, 假设某天的高峰的5000个请求,只有1000个是正常的普通用户发出,剩余4000都是恶意请求,因此这4000个请求不可能在缓存中寻得数据,也不可能在数据库中查到数据从而反写到缓存中,而数据库根本扛不住4000每秒的并发量而被打死;
解决:
解决方式其实不难, 就是只要从数据库中没查到,就在数据库中set一个空值,或具有特殊意义的固定值;
8 redis你是如何压测的,瓶颈在哪?为什么?
9 Redis中key的过期机制?
(1) 手动设置过期时间
当过期时间到了,Redis会通过定期删除+惰性删除的方式,进行这批过期缓存数据的删除
所谓定期删除,就是redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除;
所谓惰性删除,就是说,当你获取某个key时,redis会检查这个key如果设置了过期时间是否已经到点了?如果过期了就删除,并且不会返回给你任何东西
(2) 内存淘汰机制
上面定期删除+惰性删除并没有全部删除掉实际上已经过期的key,这个时候就会有大批量的无用的缓存数据占据着你的内存,这样是不能够被允许的,该怎么办呢? 走内存淘汰机制,其淘汰策略如下:
- noeviction: 当内存不足以容纳新写入数据时, 新写入操作会报错;(太恶心)
- allkeys-lru: 当内存不足以容纳新写入数据时, 在键空间中,移除最少使用的key(最常用)
- allkeys-random: 当内存不足以容纳新写入数据时,在键空间中,随机移除某个key(一般没人用)
- volatile-lru: 当内存不足以容纳新写入数据时, 在设置了过期时间的键空间中,移除最近最少使用的key
- volatile-random: 当内存不足以容纳新写入数据时, 在设置了过期时间的键空间中, 随机移除某个key
- volatile-ttl: 当内存不足以容纳新写入数据时, 在设置了过期时间的键空间中, 有更早过期时间的key优先移除
10 Redis 主从复制方式 ,复制步骤
(1) 当启动一个slave node的时候,它会发送一个PSYNC
命令给master node
(2) 如果这是slave node重新连接master node,那么master node仅仅会复制给slave部分缺少的数据; 否则如果是slave node第一次连接master node,那么会触发一次full resynchronization
(3) 开始full resynchronization的时候,master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端收到的所有写命令缓存在内存中。RDB文件生成完毕之后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后再从本地磁盘加载到内存中。然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据。
(4) slave node如果跟master node有网络故障,断开了连接,会自动重连。master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node。
11 Redis数据丢失的两种场景
-
主备切换的过程,可能会导致数据丢失
因为 mater -> slave 的复制是异步的, 所以可能有部分数据还没有复制到 slave, master 就宕机了,此时这部分数据就丢失了
-
脑裂会导致数据丢失
脑裂产生的时候,虽然某个slave被切换成了master, 但是可能client还没来得及切换到新的master, 此时还是在往旧的master上写数据. 但是我们知道, 脑裂情况下,当旧的master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空, 选择重新从新的master复制数据;
因此, 这一部分的数据会丢失;
解决数据丢失的问题
min-slaves-to-write 1
min-slaves-max-lag 10
表示:
要求至少有1个slave,数据复制和同步的延迟不能超过10秒
-
减少异步复制的数据丢失
有了 min-slaves-max-lag 这个配置,就可以确保说,一旦slave复制数据和ack(回执)延时太长,就认为可能master宕机后损失的数据太多了, 那么就拒绝客户端写请求,这样可以把master宕机时由于部分数据未同步到slave导致的数据丢失降低的可控范围内;
-
减少脑裂的数据丢失
如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的slave 发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求;
上面的配置就确保了,如果任何一个slave丢了连接, 在10秒后发现没有slave给自己ack, 那么就拒绝新的写请求; 因此在脑裂场景下, 最多就丢失10秒的数据
12 Redis实现分布式锁
可以解决Redis并发竞争问题
13 Redis集群
14 redis如何通过读写分离来承载读请求QPS超过10万+?
redis主从架构 -> 读写分离架构 -> 可支持水平扩展的读高并发架构