redis学习总结

Redis

  • Codis 豌豆荚中间件团队搞的
  • 阿里多隆 美团 用的 tair

NoSQL(not only sql )

  • cap + base
  • 扩展性好,可以随时变更记录结构

NoSQL4大类型

  • KV键值对 redis ,tair,memcache,berkeleyDB
    • 查询速度快
    • 无结构化,通常存储字符串和二进制
  • 文档数据库 MongoDB ,最像关系型数据库的,基于分布式文件存储
    • 类似(key,value),只是value是结构化的
    • BSON :类似json ,二进制的json = (Binary json)
    • 这个结构化不像RDBMS那么严格,不用预先定义表结构
    • 查询性能不高
  • 列簇式存储 Hbase 分布式文件系统
    • 查询速度快,更加容易分布式扩展
    • 列簇式:将同一列存储到一起
    • 功能相对局限
  • 图关系存储 关系图谱(社交网络,朋友圈,ad推荐) Neo4j,InfoGrid
    • 图结构 (【家庭】关系网,族谱)
    • 可利用算法:最短关系寻址
    • 基于整个图做分析计算,不利于做集群方案

Redis性能高原因

  1. 高效的数据结构

  2. 多路复用 IO 模型

  3. 事件机制

1、高效的数据结构
Redis 支持的几种高效的数据结构 string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)

如果你是Redis中高级用户,还需要加上下面几种数据结构HyperLogLogGeoPub/Sub

以上几种对外暴露的数据结构它们的底层编码方式都是做了不同的优化的,不细说了,不是本文重点

2、需要注意的是,除了异步 IO 外,其它的 I/O 模型其实都可以归类为阻塞式 I/O 模型,不同的是像阻塞式 I/O 模型在第一阶段读取数据的时候,如果此时数据未准备就绪需要阻塞,在第二阶段数据准备就绪后需要将数据从内核态复制到用户态这一步也是阻塞的。而多路复用 IO 模型在第一阶段是不阻塞的,只会在第二阶段阻塞。

redis多路复用

3、redis工作模式 = reactor模式+ 事件队列 (bossgroup(1)+workergroup(1) )

redis之reactor事件机制

4、注意redis储存的值不宜过大,指令执行时间超过10毫秒就算慢查询了。 内存DDR4的读取50G/s

  • 纯内存数据
  • i/o多路复用 (epoll)
  • redis 单线程多进程
    • nginx多进程单线程
    • Memcached:单进程多线程模型
  • C语言

Redis数据类型

Sds (Simple Dynamic String,简单动态字符串)

  • C语言中char* , 无法快速计算数据长度; 无法快速动态追加
  • sds 既可高效地实现追加和长度计算, 同时是二进制安全的。
    • strlen,append
    • sds是通过buf以及len来判断字符串内容的,而不是通过“\0”来判断
    • 简单来说,二进制安全就是,字符串不是根据某种特殊的标志来解析的,无论输入是什么,总能保证输出是处理的原始输入而不是根据某种特殊格式来处理。比如redis通过len来表示字符串长度,不会因为中间插入了“\0”就返回错误结果
  • sds 会为追加操作进行优化:加快追加操作的速度,并降低内存分配的次数,代价是多占用了一些内存,而且这些内存不会被主动释放。
    • 当大小小于 1MB 的字符串执行追加操作时, sdsMakeRoomFor 就为它们分配多于所需大小一倍的空间; 当字符串的大小大于 1MB , 那么 sdsMakeRoomFor 就为它们额外多分配 1MB 的空间。
struct sdshdr {
    len = 11;
    free = 0;
    buf[] = "hello world\0";  // buf 的实际长度为 len + 1
};
// 通过对 buf 分配一些额外的空间, 并使用 free 记录未使用空间的大小, sdshdr 可以让执行追加操作所需的内存重分配次数大大减少
  • 基本类型: string,hash,set ,zset ,list

  • bitmaps

    存储效率 (拿时间换空间,最后却可以很快地统计)

    多用于统计数据状态,分析

  • HyperLogLog

    一种算法,基数统计,只记录数量不记录具体数据

    有一定误差

  • GEO

    • 地理位置 (经纬度)

问题集

从海量数据中查询某一固定前缀的key

使用keys指令可以扫出指定模式的key列表。

对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?

这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

基于游标的迭代器,需要使用上一次游标延续之前的迭代过程。游标为0的时候代表开始或结束。

#模式
scan cursor match pattern count
#示例
scan 0 match k* count 10

Redis做异步队列

一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。

如果对方追问可不可以不用sleep呢?list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。

如果对方追问能不能生产一次消费多次呢?使用pub/sub主题订阅者模式,可以实现1:N的消息队列。

如果对方追问pub/sub有什么缺点?在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。

如果对方追问redis如何实现延时队列?我估计现在你很想把面试官一棒打死如果你手上有一根棒球棍的话,怎么问的这么详细。但是你很克制,然后神态自若的回答道:使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

Redis如何做持久化的?

bgsave做镜像全量持久化,aof做增量持久化。因为bgsave会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要aof来配合使用。在redis实例重启时,会使用bgsave持久化文件重新构建内存,再使用aof重放近期的操作指令来实现完整恢复重启之前的状态。

对方追问那如果突然机器掉电会怎样?取决于aof日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。

对方追问bgsave的原理是什么?你给出两个词汇就可以了,fork和cow。fork是指redis通过创建子进程来进行bgsave操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。

Pipeline有什么好处,为什么要用pipeline?

可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。

Redis的同步机制了解么?

Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。

Redis事务

Redis中的事务(transaction)是一组命令的集合

单线程为什么还要事务

因为当多个client端同时发送命令时,redis处理命令的顺序是不确定的.

在复杂场景下,提交一次操作可能包含多条redis命令。但业务逻辑是一次操作,但多条redis命令提交,是没法保证在服务端在各个命令中间不插入其他的redis命令。所有一般用事务来保证。但redis事务的概念又有别于普通数据库事务......

如A事务中有c,d,e三个命令,B事务中h,l两个命令,当两个客户端同时发送给redis时,redis可能先执行c,然后又执行了h,顺序可能变成 c->h->d->l->e.而期望的顺序是c->d->e->h->l.

因此只有redis在事务执行期间,不再响应其他客户端请求,才能保证一个客户端上的事务完整按序执行

对事务是部分支持
  • 多个命令准备入队,编译时出异常,所有命令都会discard

  • 多个命令准备入队,编译时无异常,所有命令入队成功,但是运行时如果出异常 除了出异常的命令其它命令都会执行成功

    • 由于运行时会出现部分提交的情况,所以要手动进行事务回滚

      记录事务过程中要操作key的原始值,一但发生异常,就重新设值回去 (包括key的过期时间)

  • 不保证原子性,也没有事务隔离

  • 事务分为3阶段 : 开启,入队,执行(取消);

命令

​ multi 命令标记开启事务

​ exec 执行所有事务块内的命令

​ discard 取消事务

​ watch/unwatch 监视一个或多个key

WATCH
  • 先watch再multi ,watch命令类似CAS乐观锁
    • 事务开启前(multi) 加上watch 'key' , 如果key的值已经被别的客户端改变,那么整个事务队列的命令都不会执行
  • unwatch或者exec执行后,监控锁就会被释放

AOF重写

why & what

  • AOF 持久化是通过保存被执行的写命令来记录数据库状态的,所以AOF文件的大小随着时间的流逝一定会越来越大

  • 为了解决AOF文件体积膨胀的问题,Redis提供了AOF重写功能:Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个文件所保存的数据库状态是相同的,但是新的AOF文件不会包含任何浪费空间的冗余命令,通常体积会较旧AOF文件小很多。

  • AOF重写并不需要对原有AOF文件进行任何的读取,写入,分析等操作,这个功能是通过读取服务器当前的数据库状态来实现的。

  • AOF重写其实是一个有歧义的名字,实际上重写工作是针对数据库的当前状态来进行的,重写过程中不会读写、也不适用原来的AOF文件;

  • AOF可以由用户手动触发,也可以由服务器自动触发。

实现原理

伪代码
# 首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录该键值对的多个命令;伪代码如下:
def AOF_REWRITE(tmp_tile_name):

  f = create(tmp_tile_name)

  # 遍历所有数据库
  for db in redisServer.db:

    # 如果数据库为空,那么跳过这个数据库
    if db.is_empty(): continue

    # 写入 SELECT 命令,用于切换数据库
    f.write_command("SELECT " + db.number)

    # 遍历所有键
    for key in db:

      # 如果键带有过期时间,并且已经过期,那么跳过这个键
      if key.have_expire_time() and key.is_expired(): continue

      if key.type == String:

        # 用 SET key value 命令来保存字符串键

        value = get_value_from_string(key)

        f.write_command("SET " + key + value)

      elif key.type == List:

        # 用 RPUSH key item1 item2 ... itemN 命令来保存列表键

        item1, item2, ..., itemN = get_item_from_list(key)

        f.write_command("RPUSH " + key + item1 + item2 + ... + itemN)

      elif key.type == Set:

        # 用 SADD key member1 member2 ... memberN 命令来保存集合键

        member1, member2, ..., memberN = get_member_from_set(key)

        f.write_command("SADD " + key + member1 + member2 + ... + memberN)

      elif key.type == Hash:

        # 用 HMSET key field1 value1 field2 value2 ... fieldN valueN 命令来保存哈希键

        field1, value1, field2, value2, ..., fieldN, valueN =\
        get_field_and_value_from_hash(key)

        f.write_command("HMSET " + key + field1 + value1 + field2 + value2 +\
                        ... + fieldN + valueN)

      elif key.type == SortedSet:

        # 用 ZADD key score1 member1 score2 member2 ... scoreN memberN
        # 命令来保存有序集键

        score1, member1, score2, member2, ..., scoreN, memberN = \
        get_score_and_member_from_sorted_set(key)

        f.write_command("ZADD " + key + score1 + member1 + score2 + member2 +\
                        ... + scoreN + memberN)

      else:

        raise_type_error()

      # 如果键带有过期时间,那么用 EXPIREAT key time 命令来保存键的过期时间
      if key.have_expire_time():
        f.write_command("EXPIREAT " + key + key.expire_time_in_unix_timestamp())

    # 关闭文件
    f.close()

key对应的数据如果过大

实际为了避免执行命令时造成客户端输入缓冲区溢出,重写程序在处理list hash set zset时,会检查键所包含的元素的个数,如果元素的数量超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那么重写程序会使用多条命令来记录键的值,而不是单使用一条命令。该常量默认值是64– 即每条命令设置的元素的个数 是最多64个,使用多条命令重写实现集合键中元素数量超过64个的键;

使用子进程进行AOF重写
  • aof_rewrite函数可以创建新的AOF文件,但是这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间的阻塞,因为Redis服务器使用单线程来处理命令请求;所以如果直接是服务器进程调用AOF_REWRITE函数的话,那么重写AOF期间,服务器将无法处理客户端发送来的命令请求;
  • Redis不希望AOF重写会造成服务器无法处理请求,所以Redis决定将AOF重写程序放到子进程(后台)里执行。这样处理的最大好处是:
    • 子进程进行AOF重写期间,主进程可以继续处理命令请求;
    • 子进程带有主进程的数据副本,使用子进程而不是线程,可以避免在锁的情况下,保证数据的安全性。
  • 子进程在进行AOF重写期间,服务器进程还要继续处理命令请求,而新的命令可能对现有的数据进行修改,这会让当前数据库的数据和重写后的AOF文件中的数据不一致
    • 为了解决这种数据不一致的问题,Redis增加了一个AOF重写缓存,这个缓存在fork出子进程之后开始启用,Redis服务器主进程在执行完写命令之后,会同时将这个写命令追加到AOF缓冲区和AOF重写缓冲区
    • 即子进程在执行AOF重写时,主进程需要执行以下三个工作:
      • 执行client发来的命令请求;
      • 将写命令追加到现有的AOF文件中;
      • 将写命令追加到AOF重写缓存中。
子进程完成AOF之后
  • 当子进程完成对AOF文件重写之后,它会向父进程发送一个完成信号,父进程接到该完成信号之后,会调用一个信号处理函数,该函数完成以下工作:

    • 将AOF重写缓存中的内容全部写入到新的AOF文件中;这个时候新的AOF文件所保存的数据库状态和服务器当前的数据库状态一致;
    • 对新的AOF文件进行改名,原子的覆盖原有的AOF文件;完成新旧两个AOF文件的替换。
  • 当这个信号处理函数执行完毕之后,主进程就可以继续像往常一样接收命令请求了。在整个AOF后台重写过程中,只有最后的“主进程

    写入命令到AOF缓存”和“对新的AOF文件进行改名,覆盖原有的AOF文件。”这两个步骤(信号处理函数执行期间)会造成主进程阻塞,在其他时候,AOF后台重写都不会对主进程造成阻塞,这将AOF重写对性能造成的影响降到最低。

触发AOF后台重写的条件

  • AOF重写可以由用户通过调用BGREWRITEAOF手动触发。

  • 服务器在AOF功能开启的情况下,会维持以下三个变量:

    • 记录当前AOF文件大小的变量aof_current_size。
    • 记录最后一次AOF重写之后,AOF文件大小的变量aof_rewrite_base_size。
    • 增长百分比变量aof_rewrite_perc。
  • 每次当serverCron(服务器周期性操作函数)函数执行时,它会检查以下条件是否全部满足,如果全部满足的话,就触发自动的AOF重写操作:

    • 没有BGSAVE命令(RDB持久化)/AOF持久化在执行;

    • 没有BGREWRITEAOF在进行;

    • 当前AOF文件大小要大于server.aof_rewrite_min_size(默认为1MB),或者在redis.conf配置了auto-aof-rewrite-min-size大小;

    • 当前AOF文件大小和最后一次重写后的大小之间的比率等于或者等于指定的增长百分比(在配置文件设置了auto-aof-rewrite-percentage参数,不设置默认为100%)

  • 如果前面三个条件都满足,并且当前AOF文件大小比最后一次AOF重写时的大小要大于指定的百分比,那么触发自动AOF重写。

    原文链接:https://blog.csdn.net/hezhiqiang1314/article/details/69396887

过期删除

  • 内存和cpu占用之间寻找一种平衡(cpu忙的时候就不要启动过期删除任务)

  • expires 过期的存储结构 hash ( 地址 ,时间戳 )

  • 在 redisDb 中使用了 dict *expires,来存储过期时间的。其中 key 指向了 keyspace 中的 key(c 语言中的指针), value 是一个 long long 类型的时间戳,标定这个 key 过期的时间点,单位是毫秒。

    如果我们为上文的 mobile 增加一个过期时间。

    >redis PEXPIREAT mobile 1521469812000
    

    这个时候就会在过期的 字典中增加一个键值对。如下图:

    db

    对于过期的判断逻辑就很简单:

    1. 在 字典 expires 中 key 是否存在。
    2. 如果 key 存在,value 的时间戳是否小于当前系统时间戳。
  • 过期策略

    • 定期删除
      • Redis定时的删除内存里面所有过期的键值对,这样能够保证内存友好,过期的key都会被删除,但是如果key的数量很多,一次删除需要CPU运算,CPU不友好。
    • 惰性删除 (下次使用时再删除)
      • 有 key 在被调用的时候才去检查键值对是否过期,但是会造成内存中存储大量的过期键值对,内存不友好,但是极大的减轻CPU 的负担。
    • 定时部分删除,Redis定时扫描过期键,但是只删除部分,至于删除多少键,根据当前 Redis 的状态决定。
    • 这三种策略就是对时间和空间有不同的倾向。Redis为了平衡时间和空间,采用了后两种策略 惰性删除和定时部分删除。

Redis发布订阅

​ 6379>SUBSCRIBE channel1 ,channel2

​ 6379>publish channel1 helloworld

Redis主从复制

复制原理

slave 启动连接到master 后会发送一个sync命令

master接到命令

全量复制,增量复制

1.建立连接 2. 数据同步 3.命令传播阶段 (增量同步)

定时任务 slave->ping->master master -pong->slave

  • 服务器运行标识(runid)

  • 复制缓冲区(FIFO QUEUE)

    • RESP形式的命令字节值
    • 偏移量(offset)
      • master ,slave都要记录
      • master发送一次记录一次 且为每一个slave记录(多个)
      • slave接收一次记录一次 (只需记录一个)
      • 作用:同步信息,比对master和slave差异,当slave断线后恢复数据用
主从复制步骤
  1. slave 发送指令:psync2 ? -1 ( psync2 )

  2. master 执行bgsave 生成RDB文件 ,记录offset

  3. master 发送 + FULLRESYNC runid offset ----socket----> slave

3.1 期间可能收到客户端命令 master端的offset发生变化 (需要增量复制)

  1. slave收到 + FULLRESYNC 保存master发来的runid和offset

4.1 清空自己全部数据,通过接收的master发来的RDB恢复数据

  1. 全量复制完成

  2. slave 发送命令: psync2 runid offset

  3. master接收命令,判定runid是否匹配,判定offset是否在缓冲区中

  • 如果runid不匹配或者offset不在缓冲区中,执行【全量复制】
    • 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的redis节点并不是当前的主节点,只能进行全量复制;
    • 如果master重启runid会发生变化,所有slave都要重新全量同步 ?
      • master内部会创建master_replid变量,会备份 runid 和offset到rdb文件中 ,是可以恢复的
    • 如何设置好缓冲区大小以减少全量复制的次数
      • slave断线重连的平均时长
  • 如果检验通过再判定 slave.offset = master.offset ,如果相同表明是最新数据,不相同开始发送数据
  • 发送 缓冲区中(slave.offset 到 master.offset )的数据 ,发送 +CONTINUE offset
  1. slave接收 +CONTINUE ,保存新的offset ,接收信息(命令)后执行 bgrewriteaof ,恢复数据
心跳

master和slave定时持续的同步更新

  • master

    • 周期 10s (repl-ping-slave-period) , ping
    • 检测slave是否在线
  • slave

    • 周期 1s REPLCONF ACK offset
    • 上传汇报自己的偏移量(要数据),检测master是否在线

模式历程

Redis-Sentinel(v2.8)支持高可用,Redis-Cluster(v3.0)支持分布式。

  • 一主二从(多从)

    • 主的压力很大,slave机器越多,复制延时就越严重
  • 薪火相传

    • 有些节点(b)既作为master又作为slave , b slaveof a , c slaveof b ;
    • 树状结构 (层级变深,一致性变弱)
  • 反客为主

    • master宕机后 ,重新分配 : slaveof no one
  • 哨兵模式(sentinel)

    • 加入监控作用, 相当于反客为主的自动版
      • 主观下线,客观下线(sdown,odown)
    • 通知其它机器master挂了
    • 主机挂了后,选举出新的master (故障转移)
    • 哨兵是一套独立的分布式系统 (也是redis服务器,一般为单数个)
      • 哨兵中选出一个话事人,来决定哪个slave上位
      • 留下和老master关系近的
      • 比较同步偏移量,qroumID
  • cluster

    • 构建集群redis4.0 : redis-trib.rb (ruby,gem) ,新版本可能有变化

    • 多主从结构

      • slave 1s连一次master,如果master当机,slave会上位
    • hash分槽(slot)

      • master有各自负责的槽区间

      • redis-cli -c (集群模式连接)

    • codis

Redis解决方案

特殊问题现象

  • 缓存预热

    • 系统启动前得让缓存中有启动时和启动后马上要用到的数据信息,避免系统一启动就发生大量的数据库查询操作
  • 缓存雪崩

    • 问题原因

      • 雪崩效应
      • 较短时间内,缓存key集中过期
        • 随机过期时间 (错开过期时间)
        • 热点数据永久key ,不过期
      • 大量请求积压
    • 解决方案

      • LRU -> LFU

      • 多级缓存 (Nginx + redis + ehcache )

      • 灾难预警

      • 限流,降级

  • 缓存击穿

    • 某个高热key过期,且这个key访问过大
  • 缓存穿透

    • 访问不存在的东西

    • 黑客攻击

    • 白名单(布隆过滤器,一个过滤器)

      • 布隆过滤器有一定的误判率

      • 相当于把所有正常的key加入白名单,拦截了一层(性能效率变低)

    • 实时监控redis命中率

      • 有一个业务正常范围的波动值
      • 如果命中率下降到这个值的10倍,50倍,就纳入重点排查
        • 加入黑名单(ip过滤)
    • key加密

      • 随机加密串,只有应用程序知道的加密策略
      • 相当于动态的白名单
      • 防灾为主

性能监控

  • 性能指标(Performance)
    • redis 响应时间 (latency)
    • hit rate (缓存命中率)
    • OPS ( instantaneous_ops_per_sec ) - 平均每秒处理请求总数
  • 内存指标
  • 基本活动指标
  • 持久化指标
  • 错误指标

工具命令

  • zabbix (工具)

自带命令

  • -benchmark

    redis-benchmark 性能测试

  • redis cli

    • monitor
      • 监控所有执行,实时监控查看日志
      • 对线上环境用这个命令意义不是很大
    • slowlog (慢查询日志)

redis双活数据中心

posted @ 2021-06-27 17:29  沉梦匠心  阅读(88)  评论(0)    收藏  举报