5.1 什么是Redis
- 内存型数据库,读写速度快
- 有多种数据类型用于不同业务
- 执行命令由单线程完成,不存在并发竞争
5.2 为什么用redis做mysql的缓存
- redis高性能:用户第一次访问mysql的数据会从硬盘读取比较慢,之后会将数据缓存在redis中下一次访问可以直接从缓存中读取速度快
- redis高并发:redis的qps是mysql的倍,redis的请求承受能力远远大于mysql
5.3 redis数据类型
5.4 什么是redis单线程,为什么又引入了多线程
- 指 接收客户请->解析请求->数据读写->发送数据给客户端 这个过程由一个主线程完成
- redis程序不是单线程,包含一个主线程,三个后台线程,三个IO线程
- 后台线程:bio_close_file (异步处理关闭文件)、bio_aof_fsync (AOF刷盘)、bio_lazy_free (释放内存)
- IO线程:io_thd_1、io_thd_2、io_thd_3,用来分担网络IO压力
5.5 redis单线程运行流程
- 初始化:
- 调用epoll_create()创建epoll对象,调用socket()创建一个服务端socket,调用bind()绑定ip和端口,调用listen()监听连接请求
- 调用 epoll_ctl() 将listen到的socket加入到 epoll,同时注册连接事件处理函数
- 主线程进入事件循环函数:
- 先调用处理发送队列函数,如果有任务通过write()函数发出去,如果这一轮没发送完就注册写sh
5.6 为什么单线程运行速度还是很快
- 大部分操作在内存中完成,并且采用了高效的数据结构
- 单线程可以避免多线程之间的竞争
- 采用IO多路复用机制处理大量socket请求
5.7 AOF日志如何实现
- redis在执行完一条写操作命令后,会把该命令以追加的方式写入一个文件夹,redis重启时会读取该文件记录的命令进行数据恢复
5.8 为什么先执行命令,再追加到AOF文件
- 执行命令时会检查语法问题,先记录可能会记录错误的语法;并且这样不会则是当前写操作命令的执行
- 这样可能会导致数据丢失,AOF也是在主线程执行,写磁盘的过程中可能会阻塞后续操作
5.9 AOF写回策略
- 执行写操作,将命令追加到server.aof_buf缓冲区
- 通过write()系统调用将aof_buf的数据写入到AOF文件,此时数据拷贝到了内核缓冲区还没有写入磁盘
- 内核缓冲区写入磁盘的过程有三种策略:
- always:每次写操作执行后同步将AOF日志写回磁盘
- evertsec:每隔一秒将缓冲区的内容写回磁盘
- no:不由redis控制,转交给操作系统控制写回时机
5.10 AOF过大会怎么办
- 提供了AOF重写机制来压缩AOF文件,相当于移除了历史命令(被覆盖的命令)
- 重写时,读取当前数据库所有键值对,然后将每个键值对用一条命令记录到新的AOF文件中,最后将新的替换现有的
- 重写AOF是由后台子进程bgrewriteaof完成的,避免阻塞主进程,此时父子进程共享物理内存,子进程对这部分内存只读,使用进程不用加锁就能保证数据安全
5.11 AOF数据一致性问题
- 重写AOF过程,主进程如果修改了key-value,会发生写时复制
- 在 bgrewriteaof子进程执行AOF重写期间,主要执行以下三个工作:
- 执行客户端命令
- 将写命令追加到AOF缓冲区
- 将写命令追加到AOF重写缓冲区
- 当子进程完成AOF重写工作后,向主进程发送一条异步信号,主进程调用信号处理函数将AOF重写缓冲区的所有内容追加到新的AOF文件中,使得新旧两个AOF文件保存数据状态一致,然后新的AOF覆盖旧的
5.13 RDB快照如何实现
- RDB快照记录的是某个瞬间的内存数据,而AOF记录的是命令操作日志,redis恢复数据时,RDB的效率会高于AOF,因为直接从RDB上读取数据到内存即可,AOF还需要执行操作命令
5.14 RDB会阻塞主线程吗
- 如果使用save命令会在主线程生成RDB文件,如果使用bgsave命令,会创建一个子进程来生成RDB文件
- redis的快照时全量快照,每次执行都把内存中的所有数据都记录到磁盘中,执行太频繁会影响redis性能,频率太低会丢失更多数据
5.15 RDB的数据一致性问题
- 会发生写时复制
- 执行bgsave命令时,通过fork()创建子进程,此时子进程和父进程共享一片内存,并且会复制父进程的页表,指向的物理内存相同,此时如果主进程执行读操作,则主进程和bgsave互不影响
- 如果主进程执行写操作,则被修改的数据会复制一份副本,然后bgsave子进程会把该副本数据写入RDB,在这个过程中主线程仍可以直接修改原来的数据
5.16 什么是混合持久化
- 集成了RDB数据恢复快和AOF丢失数据少的优点
- AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
- 在AOF重写日志时,重写子进程先以RDB的方式写入AOF文件,然后操作命令记录在重写缓冲区,重写缓冲区里的增量命令会以AOF方式写入到AOF文件,写入完成后通知主进程将新的含有RDB和AOF的文件替换原来的AOF文件
5.17 redis主从复制过程
- 采用读写分离的方式,主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。命令复制是异步操作
- 第一次同步:
- 建立连接、协商同步:主服务器把所有数据同步给从服务器(全量复制)
- 主服务器同步数据给从服务器:
- 主服务器生成RDB文件发送给从服务器,从服务器异步替换RDB文件
- 为了保证数据一致性,在主服务器生成RDB文件期间、主服务器发送RDB文件给从服务器期间、从服务器加载RDB文件期间,将新的写命令操作写到replication buffer中
- 主服务器发送新的写操作命令给从服务器:
- 主服务器发送replication buffer的写命令给从服务器,从服务器执行写命令
- 命令传播:
- 主服务器完成第一次同步后,双方维护一个TCP长连接,目的是避免频繁的连接和断开
- 分摊主服务器压力:
- 从服务器也可以有自己的从服务器,这样从服务器可以接收主服务器的同步数据,自己也可以将数据同步给其他从服务器,来分摊压力
- 网络断开又连接后,从服务器采用增量复制的方式继续同步,repl_backlog_buffer是一个环形缓冲区,用于从服务器断连后从中找出差异的数据,replication offset标记上面那个缓冲区的同步进度,主服务器使用master_repl_offset来记录自己写到的位置,从服务器使用slave_repl_offset来记录自己读到的位置
- 如果判断出从服务器要读取的数据还在缓冲区中,主服务器将采用增量同步的方式,如果不存在将采用全量同步的方式
5.18 哨兵机制
- 实现主从节点故障转移,用于监测主节点是否存活,如果主节点挂了就会选举一个从节点切换为主节点,并且把新主节点的相关信息通知给从节点和客户端
- 哨兵也是一个redis进程,相当于观察者节点,主要负责三件事:监控、选主、通知
- 哨兵通过发布订阅机制组成哨兵集群,通过INFO命令从主节点里获得从节点信息,可以和从节点建立连接并监控
- 判断主节点下线:某个哨兵判断主节点主观下线后向其他哨兵发起命令,其他哨兵做出赞成或反对的响应,当赞成数到达预定值后标记为客观下线
- 拿到半数以上赞成票并且拿到的票数大于等于哨兵配置文件中的quorum值会被选为哨兵leader
- 哨兵leader进行主从故障转移:从已下线的主节点的所有从节点中选出一个做为新的主节点
- 选择规则:过滤掉已离线的从节点,过滤掉历史网络连接状态差的从节点,剩下的节点进行优先级、复制进度、ID号三轮考察找到一个胜出节点作为新主节点
- 其余从节点修改复制目标,将新主节点的IP和信息通过发布订阅机制通知给客户端
- 继续监控旧主节点,如果重新上线将它设置为新主节点的从节点
5.19 切片集群模式
- 将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高redis的读写性能
- 采用哈希槽(hash slot),来处理数据和节点之间的映射关系。一个切片集群共有16384个哈希槽
- 切片过程:
- 根据key,按照CRC16算法计算一个16bit的值
- 再用16bit的值对16384取模,得到0~16383的数,每个数代表一个相应编号的哈希槽
- 哈希槽怎么映射到具体的redis节点上?
- 平均分配:比如说集群中有9个节点,则每个节点上槽的个数为16384/9
- 手动分配:使用cluster meet命令手动建立节点间的连接,再用cluster addslots命令指定每个节点上的哈希槽个数(需要把16384个槽都分配完,否则redis集群无法正常工作)
5.20 集群脑裂
- 由于网络问题,主节点于从节点失联,但是主节点与客户端网络正常,客户端仍向主节点写数据,但无法同步到从节点,哨兵模式会选举新主节点,这时集群就有两个主节点了
- 集群脑裂会造成数据丢失:
- 网络好了之后,哨兵会把原来的主节点降级为从节点,然后从节点会向新的主节点请求数据同步,第一次是全量同步会替换到从节点自身的数据,这样导致客户端写入的数据丢失
- 解决办法:
- 当主节点发现从节点下线或者通信超时的总数量小于阈值时,禁止主节点写数据,直接向客户端返回错误
- 当新主节点上线时,只能新主节点接收和处理客户端请求,新数据会写到新主节点中,旧主节点不再接收数据,即使它的数据被清空也不会有新数据丢失
5.21 如何删除已过期的key
- 如何判断key过期:
- 过期字典保存了数据库中所有key的过期时间
- 过期字典的数据结构是:key是指针,指向某个键对象,value是一个long long类型整数,保存key的过期时间
- 过期删除策略:
- 定时删除:在设置key的过期时间时,同时创建一个定时事件,当时间到达时由事件处理器自动执行key的删除操作
- 可以保证key会被尽快删除,内存能尽快释放
- 过期key较多的情况下,删除过期key会占用一定的CPU
- 惰性删除:不主动删除过期key,每次访问key时都检查是否过期,如果过期就删除
- 对CPU友好,对内存不友好
- 定期删除:每隔一段时间随机从数据库中检查一定数量的key是否过期
- 优点:限制操作频率可以减少对CPU的影响,同时删除一部分过期数据也减少对空间的无效占用
- 缺点:内存没有定时删除好,CPU没有惰性删除好,难以确定删除操作的时长和频率
- 定时删除:在设置key的过期时间时,同时创建一个定时事件,当时间到达时由事件处理器自动执行key的删除操作
- redis的过期删除策略:
- 惰性删除+定期删除
- 惰性删除:redis在访问或修改key之前,都会调用expirelfNeeded函数进行检查,如果过期则删除,可以选择同步删除或异步删除,然后返回null给客户端,如果没有过期则正常返回
- 定期删除:默认每秒10次检查一次数据库,每次抽取20个key,删除过期的key,如果过期key占比超过25%则再次抽取,否则等待下一轮检查
5.22 内存淘汰策略
- 当redis运行内存超过设置的最大内存后,会使用内存淘汰策略删除符合条件的key
- 不进行数据淘汰的策略:
- noeviction(默认的内存淘汰策略):内存超过设置后,禁止新数据写入
- 进行数据淘汰的策略:
- 在设置了过期时间的数据中淘汰:
- volatile-random:随机淘汰设置了过期时间的任意键值
- volatile-ttl:优先淘汰更早过期的键值
- volatile-lru:淘汰所有设置了过期时间的键值中,最久未使用的键值
- volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值
- 在所有数据范围内淘汰:
- allkeys-random:随机淘汰任意键值
- allkeys-lru:淘汰整个键值中最久未使用的键值
- allkeys-lfu:淘汰整个键值中最少使用的键值
- 在设置了过期时间的数据中淘汰:
5.23 Redis持久化时,对过期键如何处理?
- RDB文件生成阶段:从内存状态持久化到RDB文件过程中会对key进行过期检查
- RDB文件加载阶段:
- 如果redis是主服务器,载入RDB文件时,会对文件中的key检查
- 如果redis是从服务器,载入RDB文件时,不会检查,因为主服务器进行数据同步时会清空从服务器
- AOF写入阶段:当过期键被删除后,AOF文件会追加一条del命令
- AOF重写阶段:会进行过期检查,已过期的key不会保存到AOF文件中
5.24 Redis主从模式中,对过期键如何处理?
- 主数据库在key到期时,会在AOF文件里增加一条del命令,并同步到从节点
- 从数据库不会进行过期检查,是被动的
5.25 缓存雪崩
- redis设置过期时间来保证缓存数据与数据库一致性,当大量缓存在同一时间过期或redis故障宕机,会导致大量请求直接访问数据库,导致系统崩溃
- 原因1:大量数据同时过期
- 使用随机数均匀设置过期时间
- 访问数据如果不在redis中,设置互斥锁,保证同一时间只有要给请求来构建缓存
- 不设置过期时间,交给后台线程定时更新
- 后台复制数据淘汰,如果缓存被淘汰到下一次后台定时更新缓存的这段时间里,业务线程读取缓存就会返回空值
- 解决办法1:后台线程不仅负责定时更新缓存,而且也负责频繁地检测缓存是否有效
- 解决办法2:业务线程发现缓存失效后,通过消息队列通知后台线程更新缓存
- 缓存预热:业务刚上线提前把数据缓存起来,而不是等用户访问才触发,后台更新机制正好适用这个事
- 后台复制数据淘汰,如果缓存被淘汰到下一次后台定时更新缓存的这段时间里,业务线程读取缓存就会返回空值
- 原因2:redis故障宕机
- 服务熔断:暂停业务对缓存的访问,直接返回错误
- 请求限流:只将少部分请求发送到数据库处理,其余请求拒绝服务
- 构建redis缓存高可靠集群
5.26 缓存击穿
- 缓存中某个热点数据过期了,此时大量请求访问该热点数据只能访问数据库,导致数据库很容易被高并发的请求冲垮(缓存击穿是缓存雪崩的一个子集)
- 解决办法:
- 互斥锁
- 不给热点数据设置过期时间,由后台异步更新缓存
- 在热点数据马上过期前,提前通知后台线程更新缓存以及重新设置过期时间
5.27 缓存穿透
- 当用户访问的数据既不在缓存中也不在数据库中,就没办法构建缓存数据,如果有大量请求到来导致数据库压力骤增
- 原因:
- 业务误操作
- 黑客恶意攻击
- 解决方法:
- 非法请求的限制:在API入口处判断请求参数是否合理、是否含有非法值、请求字段是否存在等
- 缓存空值或默认值:针对查询数据,在缓存中设置一个空值或默认值,这样后续请求可以从缓存中读取空值或默认值
- 使用布隆过滤器快速判断数据是否存在,布隆过滤器由初始值都为0的位图数组和N个哈希函数两部分组成,查询布隆过滤器并不一定证明数据库中存在这个数据,但可以查询到数据库一定不存在这个数据
5.28 动态缓存热点数据策略
- 通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据
- 电商平台场景为例,要求只缓存用户经常访问的Top 1000的商品
- 在缓存系统中创建一个排序队列,系统根据商品访问时间更新队列信息,越最近访问商品越靠前
- 同时定期过滤掉队列中排名后200个商品,再从数据库随机取出200个商品加入队列
- 请求每次到达时,先从队列中获取商品ID,如果命中就根据ID再从另一个缓存数据结构中读取实际商品信息并返回
5.29 缓存更新策略
- cache aside(旁路缓存)策略:
- 写策略:先更新数据库中的数据,再删除缓存中的数据
- 读策略:如果读取的数据命中缓存则直接返回数据;如果读取的数据没有命中缓存则从数据库中读取,然后写入缓存
- 适合读多写少的场景,不适合写多的场景,因为写多缓存会被频繁清理,对命中缓存有一定影响
- read/writr through(读穿/写穿)策略:
- 应用程序只和缓存交互,由缓存和数据库交互,相当于缓存代理更新数据库操作
- read through:先查询缓存数据是否存在,如果存在直接返回,如果不存在则有缓存组件负责从数据库查询数据并将结构写入缓存组件,最后缓存组件将数据返回
- write through:当数据有更新时,先查询要写入的数据在缓存中是否存在,如果存在则更新缓存,并由缓存组件同步更新到数据库,然后缓存组件告知应用更新完成;如果缓存不存在,则直接更新数据库然后返回
- write back(写回)策略:redis不适用,不能异步更新数据库
- 更新数据的时候只更新缓存,同时将缓存数据设置为脏数据,然后立刻返回。对于数据库的更新会通过批量异步更新的方式进行
- 适合写多的场景,问题是数据不是强一致性的,而且会有数据丢失的风险