Redis的一些问题
阅读原文时间:2023年07月11日阅读:1

date: 2020-10-15 10:58:00

updated: 2020-10-19 18:00:00

Remote Dictionary Server

底层C写的

类似于 mysql,可以把最近的query和对应的结果保存下来 => hashquery 存入到缓存里,如果其他用户的query的hash值是一样的,说明查询是一样的,直接从缓存里读出来结果 => 多级缓存,目的是将最初的query进行过滤、优化,尽可能只让最后的事务操作落到DB中,缓解DB压力

1. redis出现的原因

数据文件过大,查询变慢 -> 因为要走 全量扫描

数据存储的2个位置

  • 磁盘

    影响指标

    • 带宽/吞吐:百兆/s -- G/s
    • 寻址时间:毫秒
  • 内存

    纳秒级别

mysql 查询快的原因是因为数据会分成block/datapage来保存,通过B+树建立索引,避免了全表扫描。

当mysql中的表非常大的时候,查询会很慢吗?

  • 如果是一条sql命中了索引的话,还是很快的
  • 如果是并发量很大的话,并且命中索引后指向的是独立的datapage,那么mysql会把每一个datapage拉到内存中查询,一个datapage是4k大小,如果请求量过大,磁盘带宽跟不上,会导致查询变慢

同样的数据,在内存中占的大小比在磁盘中占的大小要小很多。因为有指针,数据复用的现象。

=> 把热点数据放入到内存中,就是 redis,类似的还有 memcache。java 借助 LinkedHashMap 实现 LRU

2. redis 特征

关于单线程以及其他问题的官方回答

  • 绝大部分请求是纯粹的内存操作
  • worker 单线程串行的
    • 关于线程这个问题,影响的瓶颈在于IO通信。Redis 4.x 之后,对数据的具体计算过程等是单线程的,对数据的获取、返回客户端等是多线程的(IO Threads)
  • value 类型且每个类型有自己的本地方法。比如存入一个数组 [a,b,c],客户端要c的下标,redis有数组的index方法可以直接返回2,不需要返回整个数组,在客户端进行计算。相比之下 memcache 就是单纯的 kv 格式,都是string
  • 客户端的访问是并发的,通过epoll解决

内存操作 + 单线程避免锁、上下文竞争切换 + 非阻塞IO + value 的数据结构专门设计 => 速度快

多线程的弊端:一致性。redis的数据要多线程,一定要加锁,会退化成串行,不如单线程。

单线程的弊端:cpu利用率不高。redis单线程已足够用了,如果想继续压榨cpu,就多开实例,相当于多开redis服务器。

redis 的 worker单线程,但是IO Thread可以是多线程 !!!

客户端C1、C2并发请求,IO Thread1 可以读取C1的数据data1,IO Thread2 可以读取C2的数据data2。然后把数据交给单线程的worker来依次计算data1,data2,保证数据是串行的,不会发生错误。worker计算后的数据可以再交给对应的 IO Thread 分别返回给C1、C2。[Redis 6.x 版本的IO Threads多线程]

但是存在问题,即先计算data2后计算data1,理论上来说C1、C2请求负载均衡到不同服务器,本来就可能是无序的状态。优化的话,比如在负载均衡的时候,根据uri来分组,绑定到一个连接池里,交给一个connection里,就可以按照顺序来处理(不是很懂,但是io thread多线程是存在的)

3. 跟踪redis启动后的线程情况

yum install strace
strace -ff -O ./strace_out/out ./redis-server
在启动redis-server的同时,将对系统的调用情况输出到 ./strace_out/ 目录下的,文件名前缀 out

4. redis 持久化

memcache 不支持持久化

指令 BGSAVE 会在后台执行将数据持久化到磁盘,底层调用的是 copyOnWrite 方法,内核中clone指令(类似于fork指令)

copyOnWrite--当前线程A读取数据提供服务,启动线程B来持久化数据,指向的物理空间数据地址都是同一个。如果此时数据发生变化,A指向新的数据地址,B不变。

cow 具体原理: 在内存里已经有一部分数据了,此时执行持久化指令,不可能复制一份数据,可能存不下,存下也是浪费。所以新起一个线程B,不耽误正在提供服务的线程A,线程指B向的数据的物理地址和线程A是一样的,如果同一个key在持久化的时候被修改了,那么在内存中重新开辟一个地址,线程A指向新的物理地址。

5. CPU中断与内核

内核常驻在内存中。

CPU 里有晶振时间中断,每秒震荡很多次,将一秒分成了很多的时间片,每一个设备占用时间片的时候会拿到中断号(类似于时间片号),内核里有一个回调函数,通过中断号可以获取到数据在内存中的地址,这样CPU就可以在对应的时间片内去获取数据处理数据,从而达到切换程序的效果。

内核的指令 clone、select、epoll、read、write、sendfile

6. BIO -> NIO -> epoll(多路复用)

  • BIO: 客户端请求数据,发送一个key,如果这时候数据还没到,没有value,会一直等着,导致用户进程的阻塞。即一个连接一个线程
  • NIO: 客户端每次请求的key都扔到一个线程里,哪个线程能获取到value了,就把数据返回。即一个连接N个线程
    • 缺点: 如果请求量特别多,会导致线程过多(几十万的请求量就相当于几十万的线程),不断循环线程的时间复杂度是 O(N),影响效率。所以通过内核指令 select,在内核里维护一个fds(fileDescription)的数组,数组里是需要监听的socket对象。当其中一个数据到了,就会启动CPU中断,select() 返回,启动一个线程,让socket接收数据。缺点就是需要多次遍历数组,不知道是哪一个socket拿到了数据,另外还得从等待队列中移除被唤醒的进程。
  • epoll(多路复用): 通过内核的epoll指令,redis可以知道哪一个客户端(socket)的数据已经准备好了,然后串行获取数据,计算结果返回给客户端
    • 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
    • 调用epoll_ctl向epoll对象中添加这100万个连接的socket
    • 调用epoll_wait收集发生的事件的连接(添加到一个双向链表里)

7. 零拷贝

kafka、nginx 零拷贝技术大同小异

客户端请求数据 socket -> 调用内核 sendfile 指令 -> 获取文件数据(文件描述符fd)并缓存到内核中 -> 直接返回网卡到客户端

8. expire 过期删除

redis 维护了一个 expire dict字典(因为不是所有的key都会过期,设置为过期的单独保存到字典中,可以更节省内存)来保存会过期的key,用到数据的时候会先检查key是否过期,过期则删除,然后返回错误。这是惰性删除,如果数据不被查找可能不会删除,造成内存浪费,所以同时有一个定时执行的函数,servercron,会在一定时间内,随机选出来字典里的数据,如果过期删除,否则再随机选择,直到规定的时间结束。这个时间有长有短,即servercron的运行时间有长有短。一般执行短的删除,每隔一段时间执行一次长的删除。

9. value 的五个类型

typedef struct redisObject {
    // 类型 一共5个类型
    unsigned type:4;

    // 编码 字符串--raw 数值--int ...
    unsigned encoding:4;

    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */

    // 引用计数
    int refcount;

    // 指向实际值的指针
    void *ptr;

} robj;
  1. string

    • 数值可以按string保存,对于已经进行过计算的数值,在encoding里会把这个value设置为int,这样下次再计算的时候直接计算,而不需要判断是否能转换成数值。第一次进行计算的时候是需要进行判断的。

      • 比如对于秒杀场景,初始化值10是string,第一次-1会判断10能不能转换为数字,如果可以-1,并且把encoding设置为intencoding=raw 表示字符串),下一次-1的时候直接-1,省略了判断的过程,提高效率
    • bitmap 位图 8个比特位,首位默认是0,遵循ascii码。但是如果 offset 超过7,那么redis会自动拓展这个位图到设置的那个长度

      setbit k1 1 1 # 偏移量1的位置设置为1
      get k1 # 返回 "@" 01000000 二进制64,对应的是 @
      setbit k1 7 1 # 偏移量7的位置设置为1
      get k1 # 返回 "A" 01000001 二进制65,对应的是 A
      • bitcount 可以统计出value里有多少个7,换算到字节的话,长度/8,可以减少存储空间;
      • bitop 可以进行二进制的与或非操作
      • 适用场景:
  2. list

    • 按放入顺序 有序
    • value 保存是双向链表的结构,同时key保存了头元素和尾元素的指针,访问头尾都是O(1)
    • 插入、弹出有方向区别,为了模拟不同的数据结构。同向:栈;异向:队列。通过index指令还可以模拟数组。
    • 适用场景:数据共享
  3. set

    • 去重 无序,当元素变多之后元素可能顺序和之前也不一样。

      • java hashset 底层实现是hashmap,只不过value设置为null,当元素变多,会触发rehash,导致元素顺序更不一样
    • 适用场景:随机事件;差集可以做推荐好友,交集可以做推荐共同好友

  4. hash

    • 即hashmap,通过 hset key field value 来设置;通过 hgetall key 可以直接返回field和value
    • 适用场景:商品详情页,可以把商品的一整套信息放在一起
  5. zset: 排序的 set

10. 持久化

两种方式:

  1. RDB

    • 快照恢复速度快,但是可能会丢失大量数据
  2. AOF

    • 日志完整,但是恢复速度慢,同时可能存在冗余日志
    • 存在冗余日志 => 4.x 之后版本会自动开启对aof日志的重写,即对同一key的操作日志,只保留有效的最近一次的日志,aof文件超过64MB的时候会自动触发

使用方法:

  • 4.x 之前版本

    • 默认开启 rdb 关闭 aof。如果开启了 aof 那么 rdb 就不生效 => 即不推荐 aof,因为过多的io会造成redis性能下降
  • 4.x 之后版本

    • 混合使用 rdb + aof => 持久化的时候,会生成一个aof文件,这个文件包含两部分,第一部分是此刻 rdb 的数据,第二部分是此刻后追加的日志记录 => 类比于 hdfs,数据保存的形式为 fsimage + edits.log

11. 压力测试

eg: redis-benchmark -c 客户端连接数(默认50) -n 请求数(默认10万) -q(quiet 不输出多余日志) -t(设置测试的命令列表) set(只测试set一个)

影响效率的几点:

  1. 网络IO -> 物理地址ip、局域网 是否走DNS转换等
  2. 磁盘IO -> aof 文件写的频率,默认是 appendfsync=appendfsync,如果改成 appendfsync=always 即每一次操作都落入到日志文件里,会降低redis的处理效率

12. 分布式部署

分布式要解决的两个问题

  1. 单点故障 -> 主从复制 -> 为了保证其中一个挂了,另一个能顶替,那么就需要做到数据同步
  2. 单个结点压力大 -> 分片集群,即将数据分片,将不同片的数据放在不同机器上,缓解访问压力 -> 不需要做到数据同步

redis 的数据分片就是每个节点负责不同的槽位,hash(key)%65536

12.1 主从复制中数据同步的方式

分布式CAP定理 -- Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),最多只能同时三个特性中的两个,三者不可兼得

P 指的是节点之间的网络通信,一般情况来说网络故障很正常,所以只能在CA之间进行取舍

但是对于银行转账这样的涉及金融,C是一定需要的,那么就在AP之间取舍

  1. 强一致性

    • client 发送请求,主节点同步把请求发给子节点,只有两个节点都成功,才返回给client成功 => 如果子节点挂了,主节点会一直等子节点重启、执行命令、返回请求,影响效率,即强一致性破坏可用性
  2. 弱一致性(redis默认使用这种)

    • client 发送请求,主节点执行成功,返回给client,同时把请求异步发给子节点,但并不能保证子节点一定执行成功 => 可能会影响一部分数据的一致性 => 如果redis的使用场景是为了缓存热点数据的话,丢一部分数据没有问题;如果是做分布式锁的话,可能会出现问题
  3. 最终一致性

    • client 发送请求,主节点执行成功,返回给client,同时把请求写入到一个位置(类似于黑盒的一个可靠的集群),子节点会读取这个位置的记录,一条条指令执行,也许会有延迟,但是能保证数据最终的一样,哪怕子节点掉了,也可以重新读记录
    • HDFS 中两个namenode之间共享数据来保证数据同步靠的是 journalnode(这也是一个集群,N个节点) ,思想就是:一条数据在journalnode集群里保证至少 n/2 + 1 个节点记录这个成功即可,这个集群会自动在n个journalnode里找到最新版本的数据来更新数据
    • 类似思想也存在于 zookeeper、哨兵、kafka等,参考Paxos

12.2 AKF 拆分原则

分片集群,每个节点管理一部分数据 -> 存在某个节点挂了,无法访问某一部分数据的情况 -> 是否针对每一个节点做HA?需要针对业务做结合

AKF 拆分原则:通过加机器可以解决容量和可用性问题

  • X轴:水平复制,HA 高可用
  • Y轴:按照业务切分,比如分业务分库
  • Z轴:数据分区(切片)