Redis面试总结
常考题目:
Redis支持的数据类型(必考)
zset跳表的数据结构(必考)
Redis的数据过期策略(必考)
Redis的持久化机制(必考)
Redis为什么能这么快
完全基于内存,绝大部分请求时纯粹的内存操作,执行效率高
数据结构简单,对数据操作也简单
采用单线程,单线程也能处理高并发请求,想多核也可以启动多实例
使用多路I/O复用模型,即非阻塞IO(Reids会根据不同的操作系统选择不同的底层实现)
Redis 适合的场景
1. 缓存:减轻 MySQL 的查询压力,提升系统性能;
2. 排行榜:利用 Redis 的 SortSet(有序集合)实现;
3. 计算器/限速器:利用 Redis 中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等。限速器比较典型的使用场景是限制某个用户访问某个API的频率,比如抢购时,防止用户疯狂点击带来不必要的压力;
4. 好友关系:利用集合的一些命令,比如求交集、并集、差集等。可以方便解决一些共同好友、共同爱好之类的功能;
5. 消息队列:除了 Redis 自身的发布/订阅模式,我们也可以利用 List 来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的 DB 压力,完全可以用 List 来完成异步解耦;
6. Session 共享:Session 是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用 Redis 保存 Session 后,无论用户落在那台机器上都能够获取到对应的 Session 信息。
Redis如何实现单线程的
redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
Redis中的数据类型:
数据结构 |
数据类型存储的值 |
说明 |
使用场景 |
STRING(字符串-常用) |
可以是保存字符串、整数和浮点数 |
可以对字符串进行操作,比如增加字符串或者求子串;如果是整数或者浮点数,可以实现计算,比如自增等 |
缓存、计数器、共享 Session、限速 |
LIST(列表) |
它是一个链表,每一个节点都包含一个字符串 |
Redis支持从链表两端插入或者弹出节点,或者通过偏移对它进行裁剪;还可以读取一个或者多个节点,根据条件删除或者查找节点等 |
阻塞队列 |
SET(集合) |
是一个收集器,但是是无须的,里面的每一个元素都是一个字符串,而且是独一无二,各不相同 |
可以新增,读取,删除单个元素,检查一个元素是否在集合中,计算它和其他集合的交集、并集和差集等;随机从集合中读取元素 |
关系处理 |
HASH(哈希散列表-常用) |
类似Java中的Map,是一个键值对应的无序列表 |
可以增,删,查,改单个键值对,可以获取所有的键值对 |
|
ZSET(有序集合) |
可以包含字符串,整数,浮点数,分值(score),元素的排序是根据分值的大小决定的 |
可以增删改查元素,根据分值的范围或者成员来获取对应的元素 |
排行榜 |
用于计数的HyperLogLog,用于支持存储地理位置信息的Geo
Z-set内部数据结构实现:
zset内部使用skiplist跳表实现,实现简单,插入、删除、查找的复杂度均为O(logN)。
悲观锁(Pessimistic Lock):每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block,直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在做操作之前先上锁。
乐观锁(Optimistic Lock) :每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号(version)等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。乐观锁策略:提交版本必须大于记录当前版本才能执行更新。
如何理解Redis中的乐观锁
当一个线程去执行某些业务逻辑,但是这些业务逻辑操作的数据可能被其他线程共享了,这样会引发多线程中数据不一致的情况。为了克服这个问题,首先,在线程开始时,读取这些多线程共享的数据,并将其保存到当前进程的副本中,我们称为旧值(old value),watch命令就是这样一个功能。然后,开启线程业务逻辑,由multi命令提供这一功能。在执行更新前,比较当前线程副本保存的旧值和当前线程共享的值是否一致,如果不一致,那么该数据已经被其他数据操作过,此次更新失败。为了保持一致,线程就不去更新任何值,而将事务回滚;否则就认为它没有被其他线程操作过,执行对应的业务逻辑,exec命令就是执行“类似”这样的一个功能。
如何保证缓存与数据库的双写一致性?
/*
如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做:读请求和写请求串行化,串到一个内存队列里去。
串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
更新的时候,先更新数据库,然后再删除缓存。
*/
如何解决缓存雪崩
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。
把每个Key的失效时间都加个随机值,避免数据在同一时间大面积失效
事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
如何解决缓存击穿
缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个key在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。或者是查询一个一定不存在的值,也需要直接请求数据库。
解决方式:
1 可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。
2 布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。
如何解决缓存穿透
访问缓存和数据库都不存在的值
增加校验
布隆过滤器
分布式锁需要解决的问题:互斥性、安全性、死锁、容错
如何使用Redis实现异步队列
使用List作为队列,RPUSH生产消息,LPOP消费消息
缺点:不管队列里有没有值,都会直接消费
弥补:可以通过在应用层引入Sleep机制去调用LPOP重试,或使用BLPOP
更好的:pub/sub 主题订阅者模式
订阅者可以订阅任意数量的频道
缺点:消息的发布是无状态的,无法保证可达
Redis如何做持久化
RDB(快照)持久化:保存某个时间点的全量数据快照
BGSAVE:Fork出一个子进程来创建RDB文件,不阻塞服务器进程
AOF(Append-Only-File)持久化:保存写状态
记录下除了查询以外的所有变更数据库状态的指令
以append的形式追加保存到AOF文件中(增量)
RDB优点:全量数据快照,文件小,恢复快 缺点:无法保存最近一次快照之后的数据
AOF优点:可读性高,适合保存增量数据,数据不易丢失 缺点:文件体积大,恢复时间长
RDB-AOF混合持久化方式
键的过期时间
Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。
对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。
Redis 常见性能问题和解决方案?
1. Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件。如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次;
2. 为了主从复制的速度和连接的稳定性, Master 和 Slave 最好在同一个局域网内;
3. 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3…