lins1

Redis总结

本文借鉴自JavaGuideCS-Notes

1. 基本概念

1.1 架构

1.1.1 数据库

基于内存的NoSql,默认有16个db,采用select 0/15的方式切换db

1.1.2 单线程

1)定义

Redis4中执行命令的模块采用单线程的方式处理客户端请求,采用IO多路复用的方式(类似于epoll)来监听多个套接字,最终将被激活的socket对应事件交由文件事件分配器分派给对应事件处理器来执行,如下图所示

img

img

图片摘自

JavaGuide-redis总结

Redis 到底是单线程还是多线程?我要吊打面试官! - Java技术栈 - 博客园 (cnblogs.com)

2)优劣
    • 易维护
    • 不需要考虑多线程并发的问题
    • 性能瓶颈在网络IO和内存,不在CPU
    • 采用单线程处理消息队列会降低IO效率,redis6中引入多线程解决了此问题

1.2 数据结构

k-v键值对的方式存储

k固定为字符串,采用字典(hashtable)的形式存储,每个key对应两张hashtable,为的是适应渐进式hash分批进行hash的特性

val可以为五种形式

  • string
  • list 链表
  • hash hashmap
  • set hashset
  • zset treeset 采用分数进行排序

2.常见问题

2.1 内存过期与淘汰

2.1.1 内存过期机制

可以为key设置过期时间,这样在时间到时,就会从缓存中删除此数据;过期字典中记录各个key的过期时间

2.2.2 内存淘汰机制

  • 设置了过期时间的keys
    • lru volatile-lru
    • random volatile-random
    • ttl 即将过期 volatile-ttl
  • 所有keys
    • lru allkeys-lru
    • random allkeys-random

除了以上还有不过期 no-eviction

2.2 持久化

2.2.1 rdb

大规模数据恢复+弱一致性

默认的方式,通过在内存中备份redis的数据库快照(这样短时间内,内存中有两份一样的数据),并定期将快照写入磁盘的方式,写入的周期如下

  • save 900 1 #超过900s且有至少1个key变动,则触发保存---长期少量
  • save 300 10 #超过300s且有至少10个key变动,则触发保存---中期中量
  • save 60 10000 #超过60s且有至少10000个key变动,则触发保存---短期大量

2.2.2 aof

rdb辅助,强一致性

可选方式,作为rdb的补充,通过对redis中存储的数据分析,逆向分析出存储这些数据最少需要执行的命令,将其写入aof文件中,写入时只允许在尾部追加,如果aof写满,则创建新文件先写入其中,之后采用重命名的方式覆盖原文件

同步aof文件到磁盘可选取的周期如下

  • appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度

  • appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘

  • appendfsync no #让操作系统决定何时进行同步

或者通过手动触发的方式

  • save

    通过save会立即阻塞所有操作并进行rdb文件的持久化

  • bgsave

    通过bgsave可以在后台异步的完成rdb文件持久化,可以通过lastsave获取最近一次的保存时间

  • flushall

    即使是清空所有数据库,也会触发一次rdb文件的持久化,只不过为空

2.3 事务

2.3.1 MULTI

打开命令缓存队列,之后输入的命令均会放入队列中,之后通过EXEC命令一次性执行队列中的命令,如果

  • 有语法错误的命令,则整个队列都撤销
  • 出现了命令与操作数不匹配(对string做INCR),则只撤销出现此类问题的命令

2.3.2 WATCH

对某个字段打开监控(上乐观锁),通过此方式,可以达到事务全部回滚的特性,使用时,首先客户端AWATCH K1,K1即为要监视的对象,之后使用MULTI打开队列,如果此时其他客户端对K1进行修改,那么如果队列中

  • 涉及到关于K1的操作,整个队列中的命令都被撤销

  • 否则,队列不被撤销

2.4 扩容

2.4.1 主从(垂直扩容-灾备/读写分离)

1)定义

通过在redis-2上调用salveof redis-1的IP 端口,即可实现手动设置主从关系(保持到下一次重启),这样主机会将rdb文件拷贝到从机,从而实现与主机内容同步。主从方式不会"真正"增加可以存储的数据,只是提高了主机的可靠性且可以将读压力转移到从机上

2)实现

设置主从关系时,一个机器只能有一个主机,一个主机可以有多个从机,可以设计多级slave,来分担同步压力

2.4.2 集群(水平扩容)

多个redis存储不同的数据,这样可以实现容量提升,提高有效性

2.5 IO多路复用

主要是同步/异步+阻塞/非阻塞IO对比

假设client和server建立了socket,在client发出read调用时

如果是

  • 阻塞

    client会一直阻塞到read系统调用返回,也即server返回了数据并且client将数据从内核空间拷贝到了用户空间,这也是传统的BIO采用的方式,这时需要每个socket都对应一个线程

  • 非阻塞

    client发出read调用后不会阻塞

    由于不会阻塞,此时有两种解决方式

    • 轮询

      client周期性的发起read请求,检查是否有server传来的数据已经准备好

    • IO多路复用

      • select

        建立一个数组包含所有要监听的socket,构建为一个待查队列,当用户进程调用select()时会一直阻塞,直到其中一个socket就绪,同时返回活跃的socket数量,但是不知道具体哪些socket,用户进程需要遍历所有的socket才可以找到哪些socket活跃

      • poll

        类似于select,采用链表方式构建待查队列

      • epoll

        将需要监听的socket和其要执行的事件(如accept)与一个回调函数一并注册到epoll中,构成一个待查队列,其中的事件都还未得到响应;当待查队列上的某个事件激活时,会调用回调函数将此socket和对应事件插入到就绪队列中,那么用户进程每次在调用epoll_wait(类似于select())时,只遍历就绪队列即可,这样省去了检查整个待查队列的开销

      • NIO-selector

        NIO中的selector即Reactor模式中的多路选择(解复用)器,用户进程需要将socket(channel)和对应的事件注册到selector上,之后selector所在线程调用select(),其会阻塞到注册的事件中至少有一个被激活,当select()返回之后调用selectedKeys()用于返回所有的就绪事件对应的key,之后用户进程只需要遍历返回的就绪时间队列,判断对应的事件类型,执行对应的方法即可(如read返回,则开始读数据)

当read系统调用返回后,如果

  • 同步

    那么client会一直阻塞等待接收server传来的数据,直到没有数据要传递

  • 异步

    那么client会为此事件保留一个callback,不会发生阻塞,当server传来的数据抵达client时,调用callback通知client来开始读取传来的数据

redis中,采用类似于selector的方式,构建了多路复用器,也即前面图中的文件事件分派器,从而避免阻塞等待请求系统调用响应

2.6 失效

2.6.1 缓存穿透

要访问的数据不在redis中时,就会到mysql中去获取,若有人恶意捏造不存在的数据请求,则会为mysql带来巨大压力,因而需要对传来的请求进行数据校验,保证其合法性

至于参数校验可以参考JavaGuide-布隆过滤器

2.6.2 缓存雪崩

redis中的数据大面积失效,这时大量的请求导向mysql,为mysql带来巨大压力,这时可以采用

  • 提高可靠性

    主从复制,或者集群多点备份数据

  • 降流

    采用消息队列缓存请求,避免冲击mysql

2.7 与memcached对比

主要有如下几点

  • memcached的value只支持string
  • memcached不能持久化

相关文章:

  • 2021-07-06
猜你喜欢
  • 2021-04-04
  • 2021-08-15
  • 2021-09-20
相关资源
相似解决方案