当面试遇到 Redis,我作为一个面试官是这么“刁难”你的!
目录
什么是Redis
- 用C语言开发的一个开源的高性能键值对(key-value)内存数据库
- 提供五种数据类型:字符串类型、散列类型、列表类型、集合类型、有序类型
- 是一种NoSql数据库
- 并不保证数据的强一致性,在某些特定的条件下可能丢失写操作
Redis持久化机制
Redis通过持久化机制把内存中的数据同步到硬盘文件来保证数据的持久化。当Redis重启后通过把硬盘中的文件重新加载到内存中,就能实现恢复数据的目的。
持久化方案有RDB和AOF。
- RDB:Redis默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件中。
- AOF:Redis会将每一个收到的写命令都用过Write函数追加到文件最后。当Redis重启时会通过重新执行文件的写命令来在内存中重建整个数据库的内容。
虽然Redis默认的是RDB,但是当同时存在时,会优先选择AOF恢复
缓存雪崩、缓存穿透、缓存击穿、缓冲预热、缓存更新、缓存降级
缓存雪崩(多个key失效)
原来缓存失效,新缓存未到,导致大量请求直接访问数据库,造成严重的问题。
-
解决方法:
-
- 加锁或者队列的方式来保证不会有大量的线程对数据库一次进行读写。
- 将缓存失效时间分散开
- 设置一定的永不过期的缓存
缓存穿透(不存在的key)
用户查询的数据在数据库中不存在,这样查询的时候在缓存中查不到,就会直接查询数据库。造成很大的压力
-
解决方法
-
- 布隆过滤器:利用高效的数据结构和算法快速判断出这个key是否在数据库中存在,不存在就直接return,存在就去查了DB刷新KV再去return
- 对查询为空的情况也进行短时间的缓存
- 在接口层进行校验。
缓存击穿(单个key)
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,这个时候就需要缓存被击穿的问题。
缓存在某个时间点过期的时候,恰在这个时间点对这个key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会把后端DB压垮
解决
- 设置热点数据永不过期
- 使用redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库
缓存预热
将相关缓存数据直接加载到缓存系统中
-
解决方法
-
- 写缓存刷新页面,上线时手工操作下
- 数据量不大,可以在项目启动的时候自动进行加载;
- 定时刷新缓存;
- 缓存更新
缓存降级
当访问量剧增,服务器出现问题或非核心服务邮箱到了核心流程的性能。这时就需要自动降级,也可以配置开关实现人工降级。
服务降级的目的:是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。一次对于不重要的缓存数据,可以采用服务降级策略。
Redis和Memcache的区别
-
Memcached(MC)
-
- 特点:
-
-
- 处理请求时使用多线程异步IO的方式,可以合理利用CPU多线程的游戏,性能优秀
- 功能简单,使用内存存储数据
- 对缓存的数据可以设置失效期,过期数据会自动被清除
- 失效的策略采用延迟失败,就是当再次使用数据时检查是否失效
- 当容量存满时,会对缓存中的数据进行剔除,剔除时除了会对过期key进行清理还会按照LRU策略对数据进行剔除
-
-
- 限制:
-
-
- key不能超过250个字节
- value不能超过1M个字节
- key的最大失效时间是30天
- 只支持K-V结构,不提供持久化和主从同步的功能
-
-
Redis:
-
- 特点
-
-
- 采用单线程模式处理请求:
-
-
-
-
- 采用了非阻塞的异步事件处理机制
- 缓存数据都是内存操作,IO时间不会太长,单线程可以避免线程上下文切换产生的代价
-
-
-
-
- Redis支持持久化,不仅仅可以作为缓存,也可以作为NoSQL数据库
- 支持多种数据结构
- Redis提供主从同步机制,以及Cluster集群部署能力,能够提供高可用服务
-
单线程的Redis为啥这么快
- 纯内存操作
- C语言实现,优化过的数据结构。
- 单线程操作,避免频繁上下文切换
- 采用非阻塞I/O多路复用机制
Redis的数据类型以及每种的使用场景
类型 | 使用场景 |
---|---|
String | 缓存、限流、计数器、分布式锁、分布式Session |
Hash | 存储用户信息、用户主页访问量、组合查询 |
List | 微博关注人时间轴列表、简单队列 |
Set | 赞、踩、标签、好友关系 |
Zset | 排行榜 |
Redis过期策略以及内存淘汰机制
过期策略
Redis采用的是定期删除+惰性删除策略。没有采用定时删除策略(因为需要一个定时器,十分消耗CPU资源)
定时删除:默认每100ms抽查一些key,过期就删除。为了避免很多key到时间没有删除就需要在采用惰性删除,
惰性删除:获取某个key的时候,redis会检查一下,如果过期了就删除。
内存淘汰机制(保证都是热点数据)
假设redis每次定期随机查询key的时候没有删掉,这些key也没有做查询的话,就会导致这些key一直保存在redis里面无法被删除,这时候就会走到redis的内存淘汰机制
- volatile-lru:从已设置过期时间的key中,移出最近最少使用的key进行淘汰
- volatile-ttl:从已设置过期时间的key中,移出将要过期的key
- volatile-random:从已设置过期时间的key中随机选择key淘汰
- allkeys-lru:从key中选择最近最少使用的进行淘汰
- allkeys-random:从key中随机选择key进行淘汰
- noeviction:当内存达到阈值的时候,新写入操作报错
Redis常见性能问题和解决方案
- Master最好不要做持久化工作,如RDB快照和AOF日志文件
- 数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次
- 为了主从复制的速度和连接的稳定性,Master和Slave最好在统一局域网内
- 尽量避免在压力很大的主库上增加从库
- 主从复制不要用图状结构,用单向链表结构更加稳定。
Redis为什么是单线程的
由于CPU不是Redis的瓶颈,瓶颈最有可能是机器内存的带下或者网络带宽,所以就使用单线程方案。Redis利用队列技术将并发访问变为串行访问。
-
绝大多数请求时纯粹的内存操作
-
采用单线程,避免不必要的上下文切换和竞争条件
-
非阻塞IO优点
-
- 速度快
- 支持多种数据类型
- 支持事务且操作是原子性
- 丰富的特性
Redis事务
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的;Redis会将一个事务中的所有命令序列化,然后按顺序执行。
- Redis不支持回滚
- 如果一个事务中命令出现错误,那么所有命令都不会执行
- 如果一个事务中出现运行错误,那么正确的命令会被执行。
四大原语
- MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
- EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
- 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
- WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
怎么实现Redis的高可用性
要想实现高可用性,一台机器肯定不够,主要的方案有下面两种
主从架构
主从架构是最简单的实现高可用性的方案,其核心就是主从同步。其原理如下:
- 这里用PSYNC举例:
主要的过程:
- 客户端向服务器发送SLAVEOF命令,让当前服务器成为Slave;
- 当前服务器根据自己是否保存Master runid来判断是否是第一次复制,如果是第一次同步则跳转到3,否则跳转到4;
- 向Master发送PSYNC ? -1 命令来进行完整同步;
- 向Master发送PSYNC runid offset;
- Master接收到PSYNC 命令后首先判断runid是否和本机的id一致,如果一致则会再次判断offset偏移量和本机的偏移量相差有没有超过复制积压缓冲区大小,如果没有那么就给Slave发送CONTINUE,此时Slave只需要等待Master传回失去连接期间丢失的命令;
- 如果runid和本机id不一致或者双方offset差距超过了复制积压缓冲区大小,那么就会返回FULLRESYNC runid offset,Slave将runid保存起来,并进行完整同步。
哨兵
基于主从方案的缺点还是比较明显的,遇到master宕机,那么就不能写入数据,那么slave也就没了作用。
而哨兵的功能具备自动故障转移、集群监控、消息通知等功能。
Redis集群的原理
Redis集群是Redis提供的分布式数据存储方案,集群通过数据分片sharding来进行数据的共享,同时提供复制和故障转移的功能。
集群之间异步复制,且集群的最大节点个数为16384个
Redis哈希槽
Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis 集群有16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。
Redis如何做内存优化
尽量使用散列表,由于散列表使用的内存比较小,所以应该尽可能的将数据模型抽象到一个散列表中,而不是设置单独的key。
Redis的过期时间和永久有效如何设置
使用EXPIER和PERSIST
-
如何用Redis做异步队列
-
- 一般使用list结构作为队列,rpush生产消息,lpop消费消息,当lpop没有消息的时候,需要适当sleep一会再重试
- 或者不用sleep使用blpop在没有消息的时候,它会阻塞住直到消息的到来
- 可以使用pub/sub主题订阅者模式,可以实现1:N的消息队列
- pub/sub缺点:在消费者下线的情况下,生产的消息会丢失,要使用专业的消息队列(RocketMQ)
-
Redis如何实现延时队列
-
- 使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理
-
机器突然掉电会怎么样
-
- 取决于AOF日志sync属性的配置,如果不要求性能,在每条指令时都sync一下磁盘,就不会丢失数据。但在高性能的要求下一般都是使用定时sync。
-
Pipeline有什么好处,为什么要使用Pipeline
-
- 可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。
-
多个操作系统同时操作Redis带来的数据问题
-
- 可以基于Zookeeper实现分布式锁,每个系统通过Zookeeper获取分布式锁,确保同一时间,只有一个系统实例在操作某个key别人都不允许读和写。
- 每次要写之前都要先判断一下当前这个Value的时间戳是否比缓存里的Value的时间戳要新。
-
数据一致性问题
-
- 使用串行化:即读请求和写请求串行化,串行到一个内存队列中。
- 但是会导致系统的吞吐量大幅度降低
- 顺序不会乱但是并发高了,这队列很容易阻塞,反而成为了系统的瓶颈
-
最经典的KV、DB读写模式
-
- 最经典的缓存+数据库读写模式就是Cache Aside Pattern
-
-
- 读的时候先读缓存,缓存没有的话,就读数据库,然后取出数据后放入到缓存中,同时返回响应
- 更新的时候,先更新数据库,然后在删除缓存。
-
-
为什么是删除缓存不是更新缓存
-
- 在复杂点的缓存场景,缓存不单单是数据库直接取出的值
- 更新缓存的代价较高,用到缓存的地方再去计算缓存
-
Redis的线程模型
-
- Redis内部使用文件事件处理器file event handler,这个文件处理器是单线程的,所以Redis才叫做单线程的模型。它采用IO多路复用机制同时监听多个Socket,根据Socket上的事件来选择对应的处理器进行处理
- 文件事件处理器的结构包括四个部分
-
-
- 多个Socket
- IO多路复用程序
- 文件事件分配器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
-
-
- 多个Socket可能会并发产生不同的操作,每个操作对于不同的文件事件,但是IO多路复用程序会监听多个Socket,会将 Socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
-
缓存有哪些类型
缓存是高并发场景下提高热点数据访问性能的一个有效手段。 -
- 本地缓存:就是在进程的内存中进行缓存,比如我们的JVM堆中,可以用LRUMap来实现,也可以使用Ehcache这样的工具来实现;本地缓存是内存访问,没有远程交互开销,性能最好,但是受制于单机容量,一般缓存较小且无法扩展
- 分布式缓存:一般去油良好的水平扩展能力,对较大数据量的场景也能应对自如。缺点是需要进行远程请求,性能远不如本地缓存
- 多级缓存:实际业务中一般使用多级缓存,本地缓存只保存访问频率最高的部分热点数据,其他的热点数据放在分布式缓存中。。
-
Redis的高级用法:
-
- Bitmap:位图是支持按bit位来存储信息,可以用来实现布隆过滤器(BloomFilter)
- HyperLogLog:提供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计UV
- Geospatial:可以用来保存地理位置,并作为位置距离计算或者根据半径计算位置
- pub/sub:功能是订阅发布功能,可以用作简单的消息队列
- Pipeline:可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答。
- Lua:Redis支持提交Lua脚本来执行一系列的功能
最后
- 如果觉得看完有收获,希望能给我点个赞,这将会是我更新的最大动力,感谢各位的支持
- 欢迎各位关注我的公众号【java冢狐】,专注于java和计算机基础知识,保证让你看完有所收获,不信你打我
- 如果看完有不同的意见或者建议,欢迎多多评论一起交流。感谢各位的支持以及厚爱。