美文网首页
redis从入门到原理

redis从入门到原理

作者: mrchen004 | 来源:发表于2019-10-16 20:30 被阅读0次
    重要声明:文中许多知识点来自慕课网的视频课程以及掘金小册的电子书
    1、mac下安装redis:brew install redis
    查看redis版本:redis-server -v
    启动redis:redis-server
    停止redis:redis-cli shutdown或者kill redis的pid
    客户端连接redis服务器:redis-cli或者redis-cli -h 127.0.0.1 -p 6379
    配置文件路径:/usr/local/etc/redis.conf
    
    2、通用命令
    expire key timeout:设置失效时间,单位是秒(默认key是没有失效时间的。set命令会让key变为永久key)
    ttl key:返回剩余失效时间
    persist key:取消设置失效时间
    object encoding key:查看对象内部编码
    object idletime key:lru最后访问时间和当前时间的差值,不会改变对象的最后访问时间
    object refcount key:对象被引用的次数
    
    3、字符串命令
    get key
    set key value
    del key
    incr key:如果key存在,对key加1,如果不存在则是将key设置为1,返回值是增加之后的key的值
    decr key:如果不存在,设置为-1
    incrby key k
    decrby key k
    setnx key value/set key value nx:key不存在才会设置
    setxx key value/set key value xx:key存在才会设置
    mget key1 key2 key3:获取多个key的值,批量操作可以减少server和client之间的网络连接的时间
    mset key1 value1 key2 value2:设置多个key-value
    getset key newvalue:设置新值,返回旧值
    append key value:追加value
    strlen key:value的长度
    incrbyfloat key floatvalue
    getrange key start end:类似substring
    setrange key start value
    以上命令中,除了mget、mset是o(n)之外,其他命令都是o(1)
    
    4、hash相关的命令
    hget key field:获取key中的field的value
    hset key field value:设置
    hdel key field:删除
    hgetall key:获取key下所有的field以及value
    hexists key field:判断key中是否有field
    hlen key:查询key中field个数
    hmget key field1 field2
    hmset key field1 value1 field2 value2
    hkeys key:获取key下所有的field
    hvals key:获取所有的key对应的value  
    hsetnx
    hincrby
    hincrbyfloat
    

    所有哈希相关的命令都是以h开头的,这些命令操作的都是哈希的value,以上命令中,除了hmget、hmset、hgetall、hkeys、hvals是o(n)之外,其他命令都是o(1)

    5、列表相关命令:
    rpush key value1 value2:从列表右端插入
    lpush key value1 value2:
    linsert key before|after value newvalue:在value之前|后插入一个newvalue
    lpop key:从列表左边弹出一个value
    rpop key
    lrem key count value:count>0是从左到右删除count个值为value的项,count<0是从右到左,count=0是删除所有的值为value的项
    ltrim key start end(包含end,end可以是小于0):裁剪list,保留start到end的项,第一个元素的下标是0,负数表示相反方向,如-1表示最后一个元素
    lrange key start end(包含end,end可以是小于0):类似sublist
    lindex key index:取第index项
    llen key:查询list长度,时间复杂度是o(1)
    lset key index newvalue:修改某个项的值
    blpop/brpop key timeout:如果list为空,等待timeout之后返回左端/右端的值,timeout为0表示不阻塞
    
    6、列表使用方式:
    Stack=lpush+lpop
    Queue=lpush+rpop
    Capped Collection=lpush+ltrim
    Message Queue=lpush+brpop
    
    7、集合相关的命令:
    集合无序,没有重复元素,可以进行集合间的操作
    sadd key element1 element2:新增,如果已经存在就不新增
    srem key element:删除
    scard key:计算集合中的元素个数
    srandmember key count:随机返回集合中的count个元素
    smembers key:返回所有的集合元素,是无序的
    spop key:随机弹出集合中的一个元素
    sismember key element:判断一个值是否在集合中
    sdiff key1 key2:返回两个集合除了交集之外的所有元素
    sinter key1 key2:返回两个集合的交集
    sunion key1 key2:返回两个集合的并集
    
    8、有序集合相关命令:
    zadd key score1 element1 score2 element2:score可以重复,element不能重复
    zrem key element:删除
    zscore key element:获取element对应的score
    zincrby key incrScore element:给element加score
    zcard key:查询个数
    zrank key element:获取element的排名
    zrange key start end [withscores]:返回一定范围的数据,可打印分数,下标从0开始
    zrangebyscore key minScore maxScore [withscores]:返回一定分数范围的数据
    zcount key minScore maxScore:返回一定分数范围的元素个数
    zremrangebyrank key start end:删除一定范围的排名的元素  
    zremrangebyscore key minScore maxScore:删除一定分数范围的元素
    
    9、事务:多条redis命令的原子操作,redis的事务不支持回滚
    multi
    多条命令
    exec
    
    10、java操作redis
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>
    
    public static void main(String[] args) {
        Jedis jedis = new Jedis("47.96.151.125", 6381);
        jedis.set("hi", "jedis!");
        System.out.println(jedis.get("hi"));
        jedis.close();
    }
    
    11、慢查询
    获取默认配置:
    config get slowlog-max-len 128
    config get slowlog-log-slower-than 10000(微妙,即10毫秒)
    
    可以设置配置文件重启redis,也可以动态配置:
    config set slowlog-max-len 256
    config set slowlog-log-slower-than 100000
    
    slowlog-log-slower-than应该设置的小一些
    slowlog-max-len 应该设置的大一些
    定期将慢查询列表中的数据持久化,因为这个列表是定长的、先进先出的
    
    慢查询命令:
    slowlog get [n]:获取n条慢查询
    slowlog len:获取慢查询条数
    slowlog reset:清空
    
    12、pipeline

    客户端将多个命令一起发送给服务端,服务端执行完之后将结果一起返回到客户端,它在服务端执行的时候,是会进行拆分的,所以这些命令从整体来看的话,不是原子的,而m操作(mget、mset之类的)是原子的。使用pipeline的好处在于减少客户端与服务端的网络连接次数,也就大大减少了客户端的等待时间了。redis的命令的执行时间是微秒级别的

    13、RDB持久化

    触发RDB持久化的时机:

    • save:同步持久化,生成的RDB文件会将老的RDB文件替换,复杂度为o(n),客户端执行,会阻塞其他命令,但是不需要额外的内存
    • bgsave:异步持久化,fork一个子进程出来生成RDB文件,其阻塞发生在fork操作,一般会比较快,不会阻塞到其他命令,需要额外的内存
    • 自动:内部自动执行bgsave,触发条件:900s改变1条数据、300s改变10条数据、60s改变10000条数据,触发条件可配置
    模拟rdb持久化
    
    配置文件
    bind 0.0.0.0
    protected-mode no
    port 6379
    daemonize yes
    logfile "6379.log"
    dir "/Users/chenzhicheng/learn/redis/data"
    
    dbfilename dump-6379.rdb
    stop-writes-on-bgsave-error yes
    rdbcompression yes
    rdbchecksum yes
    save 30 5
    
    通过redis-cli设置5个key以上,就会触发自动持久化,生成rdb文件
    kill之后重启redis,redis会自动加载rdb文件
    

    RDB缺点:耗时、耗性能、不可控,容易丢失数据

    fork子进程相关知识点

    Redis 在持久化时会调用 glibc 的函数fork产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。这时你可以将父子进程想像成一个连体婴儿,共享身体。这是 Linux 操作系统的机制,为了节约内存资源,所以尽可能让它们共享起来。在进程分离的一瞬间,内存的增长几乎没有明显变化。

    fork函数会在父子进程同时返回,在父进程里返回子进程的 pid,在子进程里返回零。如果操作系统内存资源不足,pid 就会是负数,表示fork失败

    子进程做数据持久化,它不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是父进程不一样,它必须持续服务客户端请求,然后对内存数据结构进行不间断的修改

    这个时候就会使用操作系统的 COW 机制来进行数据段页面的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的,还是进程产生时那一瞬间的数据

    随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,内存就会持续增长。但是也不会超过原有数据内存的 2 倍大小。另外一个 Redis 实例里冷数据占的比例往往是比较高的,所以很少会出现所有的页面都会被分离,被分离的往往只有其中一部分页面。每个页面的大小只有 4K,一个 Redis 实例里面一般都会有成千上万的页面

    子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 Redis 的持久化叫「快照」的原因。接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了

    14、AOF持久化

    AOF写日志的三种策略:

    • always:写命令先放到缓冲区,然后每条命令都执行fsync写入到硬盘
    • everysec:写命令先放到缓冲区,然后每秒执行fsync写入到硬盘,默认是该配置
    • no:操作系统决定fsync

    AOF重写:bgrewriteaof

    image.png

    AOF统计:

    aof_current_size:AOF当前大小,单位字节
    aof_base_size:上次启动和重写的大小
    

    AOF重写流程:

    image.png

    AOF相关配置:

    appendonly yes
    appendfilename ".appendonly-6381.aof"
    appendfsync everysec
    no-appendfsync-on-rewrite yes
    auto-aof-rewrite-percentage  100
    auto-aof-rewrite-min-size 64mb
    
    模拟aof持久化:和rdb相似,只是配置不一样,其他操作都一样
    

    RDB和AOF的比较:

    image.png
    15、混合持久化

    采用AOF持久化策略,当需要重写AOF时,会去拿到一份RDB快照文件作为新的AOF文件的开头部分,然后将生成快照文件期间进入的命令作为新的AOF文件的结尾部分。这个AOF文件就兼备了RDB和AOF的优点了。

    16、redis主从结构
    • 单机部署redis的缺点:机器故障、容量瓶颈、QPS瓶颈
    • redis主从机制(一个master可以有多个slave)的作用:数据备份、增强读性能
    • 查看主从节点一些信息:info replication、info server
    • 主从模式下,从节点只能读数据,不能自己操作数据,比如不能删除过期数据,这可能会因为网络延迟或者自身的阻塞造成从节点读到的数据是过期的
    • 主从节点应该尽量保持机器的配置一样,redis分配到的资源要一致,redis的配置要一致

    实现主从机制的两种方式

    • 命令:在slave的客户端执行slaveof、slaveof no one,实现关联master或者取消master,取消master时slave的数据不会清除,如果重新关联master则会在同步master的数据之前删除slave之前的数据
    • 配置:
      slaveof ip port
      slave-read-only yes

    redis启动时会生成一个随机的runid,从节点会记录主节点的runid,如果主节点的runid发生了变化,那么从节点会重新全量同步主节点的数据

    全量复制:主节点的数据同步到从节点,并且同步过程中写的数据也同步到从节点,其过程如下

    image.png

    全量复制的开销:master bgsave、rdb传输、、slave 清空数据、slave 加载rdb、aof重写(可能)

    发生全量复制的时机:

    • 第一次关联主节点
    • 主节点的runid发生了变化
    • offset不在主节点的复制缓冲区中,配置复制缓冲区大小:rel_backlog_size

    部分复制的过程:


    image.png
    17、redis sentinel架构

    作用:监控主从节点,实现故障转移

    image.png
    image.png
    image.png
    image.png
    protected-mode no
    daemonize yes
    port 26379
    dir "/root/downloads/redis-4.0.8/data"
    logfile "26379.log"
    sentinel monitor mymaster 127.0.0.1 6379 2
    sentinel down-after-milliseconds mymaster 30000
    sentinel parallel-syncs mymaster 1
    sentinel failover-timeout mymaster 180000
    

    sentinel支持的命令:ping、info
    启动sentinel的过程:
    在A机器配sentinel.conf,其中配置它自己的端口,以及master的ip和端口,用redis-sentinel sentinel.conf启动,开始监控master节点
    在B机器配sentinel.conf,其中配置它自己的端口,以及master的ip和端口,用redis-sentinel sentinel.conf启动,开始监控master节点

    sentinal中的三个定时任务:
    1、sentinel每10s对master进行info操作,从而拿到slave的信息,然后把信息同步给其他sentinel,这是通过redis自己的发布/订阅功能,主题是sentinel:hello
    2、sentinel每2s进行一次发布/订阅,以便发现其他sentinel
    3、每个sentinel对其他sentinel和redis每隔1s进行一次ping,这是心跳检测的过程,失败判定的依据

    从slave中选择一个master:
    可以给slave配一个slave-priority,高的优先成为一个master
    选择偏移量较大的,更接近master的数据

    手动更换主节点:
    对随意一个sentinel执行:sentinel failover mastername

    18、redis集群

    集群解决的两个问题:写并发量、数据存储能力
    集群的特性:复制(主从机制)、高可用、分片(每个主节点都是可读写的)
    数据分布:redis cluster采用哈希分布
    集群相关命令:cluster nodes/info/slots/keyslot/countkeysinslot

    在集群中使用pub/sub广播功能,会加重集群带宽

    在集群中,从节点不能进行任何读写操作,如果对从节点读,从节点会重定向给对应的主节点,或者在从节点的客户端执行readonly命令,也可以让从节点可读,这个命令是每次连接都要设置的

    集群要实现读写分离的成本极高,不建议使用

    哈希分布的方式:

    • 节点取余:实现简单,节点伸缩时需要迁移的数据量太大
    • 一致性哈希
    • 虚拟槽分区:redis cluster采用的方式,每个主节点对应一个数字段,然后crc16(key)&槽数量(16384)得到一个hash结果,将哈希结果发送给某个主节点,该主节点判断是否在自己的数字段内,在的话就保存数据,不在的话可以计算出这个键值对应该在哪个主节点,将结果返回给客户端,客户端再发送请求给正确的主节点

    安装集群:官方提供了ruby工具可以完成以下几个操作

    a、配置集群,启动节点
    port 7000
    daemonize yes
    dir ...
    dbfilename ...
    logfile ...
    cluster-enabled yes
    cluster-config-file nodes-7000.conf
    cluster-require-full-coverage no
    
    b、节点之间互相meet
    redis-cli -p 7000 cluster meet 127.0.0.1 7001
    redis-cli -p 7000 cluster meet 127.0.0.1 7002
    redis-cli -p 7000 cluster meet 127.0.0.1 7003
    redis-cli -p 7000 cluster meet 127.0.0.1 7004
    redis-cli -p 7000 cluster meet 127.0.0.1 7005
        
    c、为每个节点分配槽段
    redis-cli -p 7000 cluster addslots 0
    redis-cli -p 7000 cluster addslots 1
    redis-cli -p 7000 cluster addslots 2
    ...
    redis-cli -p 7005 cluster addslots 16383
        
    d、分配主从关系    
    redis-cli -p 7003 cluster replicate node-id(通过cluster nodes获得) 
    

    集群扩容(以下操作可以通过redis-trib.rb来完成)

    • 节点上线
    • meet集群中的某个节点
    • 将集群中每个节点的槽分配一些给新的节点,同时将槽对应的数据迁移

    迁移数据的步骤:


    image.png

    使用官方提供的工具,命令如下:

    redis-trib.rb add-node ip:port ip:port(前一个是新节点信息,后一个是集群中某个节点信息)
    redis-trib.rb reshard ip:port(新节点)
    

    集群缩容(以下操作可以通过redis-trib.rb来完成)

    • 将要下线的节点中的数据迁移到集群的其他节点
    • 将从节点先下线,对集群中的所有节点执行forget命令
    • 将主节点先下线,对集群中的所有节点执行forget命令

    使用-c参数可以在客户端自动执行保存数据到集群的一个重定向,因为客户端要保存的数据未必会保存在所连的集群节点,节点可能会返回错误,告诉客户端真正要连接的节点是哪个

    redis-cli -c -p 7000 set key value

    ask重定向:


    image.png

    smart客户端:追求性能,JedisCluster封装了以下的操作
    从集群中选择一个可运行的节点,执行cluster slots获得槽和节点的映射关系
    在客户端保存映射关系,同时对每个集群中的节点建立连接
    当需要操作数据时,先计算出槽位,然后根据映射关系得到节点,再获得该节点的连接操作数据
    在客户端执行mget等批量操作(多个key)时,可能需要对应多个节点,可以进行如下优化:
    串行mget、串行IO、并行IO、hash_tag

    集群中的故障转移:
    a、发现故障


    image.png

    b、如果超过半数的节点认为某个节点是下线的,那么就认为该节点出故障了,就会通知从节点执行故障转移
    c、从节点故障转移过程:

    资格检查:太久以前就和主节点断开的从节点会被取消资格
    准备选举时间:让offset更大的从节点更有可能成为主节点
    选举投票:集群中的主节点对故障节点的从节点投票
    替换主节点:从节点和故障节点断开关系,删除故障节点的槽添加到从节点,向集群广播消息自己成为主节点

    cluster-node-timeout:故障节点发现时间
    节点之间ping/pong的频率(cluster-node-timeout/2发送一次ping/pong)

    造成数据倾斜的几个原因:
    key的value比较大,比如hash、set

    热点问题
    槽分配不均匀
    槽中的key数量不均匀
    内存配置不一致

    redis-trib.rb info ip:port 看节点的槽、键值对的数量信息
    redis-cli --bigkeys 查看大的value

    数据迁移:
    官方迁移工具:redis-trib.rb import
    只能从单机迁移到集群
    不支持在线迁移:对单机写的数据不一定会被同步到集群中
    不支持断点续传
    单线程迁移

    集群的限制:
    不支持批量操作
    key事务和lua的支持有限
    key是数据分区的最小粒度
    不支持多个数据库,只有一个db 0
    复制只支持一层,不支持树型结构

    19、info memory命令结果说明
    image.png
    20、redis内存开销:
    image.png
    21、五种数据结构的内部实现

    a、字符串
    Redis 的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。最大长度为 512M

    如果 value 值是一个整数,还可以对它进行自增操作。自增是有范围的,它的范围是 signed long 的最大最小值,超过了这个值,Redis 会报错

    b、列表
    Redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n),这点让人非常意外

    c、哈希
    Redis 的字典相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同 Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来

    不同的是,Redis 的字典的值只能是字符串,另外它们 rehash 的方式不一样,因为 Java 的 HashMap 在字典很大时,rehash 是个耗时的操作,需要一次性全部 rehash。Redis 为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略。

    image.png

    渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,然后在后续的定时任务中以及 hash 操作指令中,循序渐进地将旧 hash 的内容一点点迁移到新的 hash 结构中。当搬迁完成了,就会使用新的hash结构取而代之。 当 hash 移除了最后一个元素之后,该数据结构自动被删除,内存被回收。

    d、集合
    Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值NULL

    e、有序集合
    zset 可能是 Redis 提供的最为特色的数据结构,它也是在面试中面试官最爱问的数据结构。它类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。它的内部实现用的是一种叫做「跳跃列表」的数据结构。

    list/set/hash/zset 这四种数据结构是容器型数据结构,它们共享下面两条通用规则:

    • 如果容器不存在,那就创建一个,再进行操作。比如 rpush 操作刚开始是没有列表的,Redis 就会自动创建一个,然后再 rpush 进去新元素。
    • 如果容器里元素没有了,那么立即删除元素,释放内存。这意味着 lpop 操作到最后一个元素,列表就消失了。
    22、redis通信协议

    RESP 是 Redis 序列化协议的简写。它是一种直观的文本协议,优势在于实现异常简单,解析性能极好
    Redis 协议将传输的结构数据分为 5 种最小单元类型,单元结束时统一加上回车换行符号\r\n。

    单行字符串 以 + 符号开头。 
    多行字符串 以 $ 符号开头,后跟字符串长度。 
    整数值 以 : 符号开头,后跟整数的字符串形式。 
    错误消息 以 - 符号开头。 
    数组 以 * 号开头,后跟数组的长度
    

    例子

    单行字符串 hello world +hello world\r\n 
    多行字符串 hello world $11\r\nhello world\r\n 
    多行字符串当然也可以表示单行字符串。 
    整数 1024 :1024\r\n 
    错误 参数类型错误 -WRONGTYPE Operation against a key holding the wrong kind of value\r\n 
    数组 [1,2,3] *3\r\n:1\r\n:2\r\n:3\r\n 
    NULL 用多行字符串表示,不过长度要写成-1。 $-1\r\n 
    空串 用多行字符串表示,长度填 0。 $0\r\n\r\n
    注意这里有两个\r\n。为什么是两个?因为两个\r\n之间,隔的是空串。
    
    23、过期删除策略

    redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的 key。除了定时遍历之外,它还会使用惰性策略来删除过期的 key,所谓惰性策略就是在客户端访问这个 key 的时候,redis 对 key 的过期时间进行检查,如果过期了就立即删除。定时删除是集中处理,惰性删除是零散处理

    Redis 默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。

    从过期字典中随机 20 个 key; 
    删除这 20 个 key 中已经过期的 key; 
    如果过期的 key 比率超过 1/4,那就重复步骤 1; 
    

    同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。

    设想一个大型的 Redis 实例中所有的 key 在同一时间过期了,会出现怎样的结果?

    毫无疑问,Redis 会持续扫描过期字典 (循环多次),直到过期字典中过期的 key 变得稀疏,才会停止 (循环次数明显下降)。这就会导致线上读写请求出现明显的卡顿现象。导致这种卡顿的另外一种原因是内存管理器需要频繁回收内存页,这也会产生一定的 CPU 消耗。

    也许你会争辩说“扫描不是有 25ms 的时间上限了么,怎么会导致卡顿呢”?这里打个比方,假如有 101 个客户端同时将请求发过来了,然后前 100 个请求的执行时间都是 25ms,那么第 101 个指令需要等待多久才能执行?2500ms,这个就是客户端的卡顿时间,是由服务器不间断的小卡顿积少成多导致的。

    所以业务开发人员一定要注意过期时间,如果有大批量的 key 过期,要给过期时间设置一个随机范围,而不能全部在同一时间过期

    24、LRU

    当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap)。交换会让 Redis 的性能急剧下降,对于访问量比较频繁的 Redis 来说,这样龟速的存取效率基本上等于不可用。

    在生产环境中我们是不允许 Redis 出现交换行为的,为了限制最大使用内存,Redis 提供了配置参数 maxmemory 来限制内存超出期望大小。 当实际内存超出 maxmemory 时,Redis 提供了几种可选策略 (maxmemory-policy) 来让用户自己决定该如何腾出新的空间以继续提供读写服务。

    • noeviction 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。
    • volatile-lru 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。
    • volatile-ttl 跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl 越小越优先被淘汰。
    • volatile-random 跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key。
    • allkeys-lru 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。
    • allkeys-random 跟上面一样,不过淘汰的策略是随机的 key

    volatile-xxx 策略只会针对带过期时间的 key 进行淘汰,allkeys-xxx 策略会对所有的 key 进行淘汰。如果你只是拿 Redis 做缓存,那应该使用 allkeys-xxx,客户端写缓存时不必携带过期时间。如果你还想同时使用 Redis 的持久化功能,那就使用 volatile-xxx 策略,这样可以保留没有设置过期时间的 key,它们是永久的 key 不会被 LRU 算法淘汰

    Redis 使用的是一种近似 LRU 算法,它跟 LRU 算法还不太一样。之所以不使用 LRU 算法,是因为需要消耗大量的额外的内存,需要对现有的数据结构进行较大的改造。近似 LRU 算法则很简单,在现有数据结构的基础上使用随机采样法来淘汰元素,能达到和 LRU 算法非常近似的效果。Redis 为实现近似 LRU 算法,它给每个 key 增加了一个额外的小字段,这个字段的长度是 24 个 bit,也就是最后一次被访问的时间戳。

    处理 key 过期方式分为集中处理和懒惰处理,LRU 淘汰不一样,它的处理方式只有懒惰处理。当 Redis 执行写操作时,发现内存超出 maxmemory,就会执行一次 LRU 淘汰算法。这个算法也很简单,就是随机采样出 5(可以配置) 个 key,然后淘汰掉最旧的 key,如果淘汰后内存还是超出 maxmemory,那就继续随机采样淘汰,直到内存低于 maxmemory 为止。

    如何采样就是看 maxmemory-policy 的配置,如果是 allkeys 就是从所有的 key 字典中随机,如果是 volatile 就从带过期时间的 key 字典中随机。每次采样多少个 key 看的是 maxmemory_samples 的配置,默认为 5

    淘汰池是一个数组,它的大小是 maxmemory_samples,在每一次淘汰循环中,新随机出来的 key 列表会和淘汰池中的 key 列表进行融合,淘汰掉最旧的一个 key 之后,保留剩余较旧的 key 列表放入淘汰池中留待下一个循环

    25、懒惰删除

    一直以来我们认为 Redis 是单线程的,单线程为 Redis 带来了代码的简洁性和丰富多样的数据结构。不过Redis内部实际上并不是只有一个主线程,它还有几个异步线程专门用来处理一些耗时的操作

    删除指令 del 会直接释放对象的内存,大部分情况下,这个指令非常快,没有明显延迟。不过如果删除的 key 是一个非常大的对象,比如一个包含了千万元素的 hash,那么删除操作就会导致单线程卡顿。 Redis 为了解决这个卡顿问题,在 4.0 版本引入了 unlink 指令,它能对删除操作进行懒处理,丢给后台线程来异步回收内存

    > unlink key
    OK
    

    如果有多线程的开发经验,你肯定会担心这里的线程安全问题,会不会出现多个线程同时并发修改数据结构的情况存在。 关于这点,打个比方。可以将整个 Redis 内存里面所有有效的数据想象成一棵大树。当 unlink 指令发出时,它只是把大树中的一个树枝别断了,然后扔到旁边的火堆里焚烧 (异步线程池)。树枝离开大树的一瞬间,它就再也无法被主线程中的其它指令访问到了,因为主线程只会沿着这颗大树来访问

    Redis 提供了 flushdbflushall 指令,用来清空数据库,这也是极其缓慢的操作。Redis 4.0 同样给这两个指令也带来了异步化,在指令后面增加 async 参数就可以将整棵大树连根拔起,扔给后台线程慢慢焚烧

    > flushall async
    OK
    

    主线程将对象的引用从「大树」中摘除后,会将这个 key 的内存回收操作包装成一个任务,塞进异步任务队列,后台线程会从这个异步队列中取任务。任务队列被主线程和异步线程同时操作,所以必须是一个线程安全的队列

    image.png

    不是所有的 unlink 操作都会延后处理,如果对应 key 所占用的内存很小,延后处理就没有必要了,这时候 Redis 会将对应的 key 内存立即回收,跟 del 指令一样

    Redis需要每秒一次(可配置)同步AOF日志到磁盘,确保消息尽量不丢失,需要调用sync函数,这个操作会比较耗时,会导致主线程的效率下降,所以Redis也将这个操作移到异步线程来完成。执行AOF Sync操作的线程是一个独立的异步线程,和前面的懒惰删除线程不是一个线程,同样它也有一个属于自己的任务队列,队列里只用来存放AOF Sync任务

    Redis 回收内存除了 del 指令和 flush 之外,还会存在于在 key 的过期、LRU 淘汰、rename 指令以及从库全量同步时接受完 rdb 文件后会立即进行的 flush 操作。

    Redis4.0 为这些删除点也带来了异步删除机制,打开这些点需要额外的配置选项。

    slave-lazy-flush 从库接受完 rdb 文件后的 flush 操作 
    lazyfree-lazy-eviction 内存达到 maxmemory 时进行淘汰 
    lazyfree-lazy-expire key 过期删除 
    lazyfree-lazy-server-del rename 指令删除 destKey
    
    26、redis存储结构
    image.png
    typedef struct redisObject {
      unsigned type:4;
      unsigned encoding:4;
      unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
      int refcount;
      void *ptr;
    } robj;
    

    用一个dicEntry来封装key-value,其中key是sds(简单动态字符串),value是封装到redisObject结构体

    27、压缩列表(ziplist)内部实现

    因为ziplist节约内存的性质,哈希键、列表键和有序集合键初始化的底层实现皆采用ziplist

    ziplist内部结构:

    image.png

    zlbytes uint32_t 整个 ziplist 占用的内存字节数,对 ziplist 进行内存重分配,或者计算末端时使用。
    zltail uint32_t 到达 ziplist 表尾节点的偏移量。 通过这个偏移量,可以在不遍历整个 ziplist 的前提下,弹出表尾节点。
    zllen uint16_t ziplist 中节点的数量。
    entryX ? ziplist 所保存的节点,各个节点的长度根据内容而定。
    zlend uint8_t 255 的二进制值 1111 1111 (UINT8_MAX) ,用于标记 ziplist 的末端。

    entry内部结构:

    image.png

    pre_entry_length 记录了前一个节点的长度,通过这个值,可以进行指针计算,从而跳转到上一个节点。
    encoding 和 length 两部分一起决定了 content 部分所保存的数据的类型(以及长度)。

    ziplist和标准类型的转换

    hash-max-ziplist-entries 512  # hash 的元素个数超过 512 就必须用标准结构存储
    hash-max-ziplist-value 64  # hash 的任意元素的 key/value 的长度超过 64 就必须用标准结构存储
    list-max-ziplist-entries 512  # list 的元素个数超过 512 就必须用标准结构存储
    list-max-ziplist-value 64  # list 的任意元素的长度超过 64 就必须用标准结构存储
    zset-max-ziplist-entries 128  # zset 的元素个数超过 128 就必须用标准结构存储
    zset-max-ziplist-value 64  # zset 的任意元素的长度超过 64 就必须用标准结构存储
    set-max-intset-entries 512  # set 的整数元素个数超过 512 就必须用标准结构存储
    

    相关文章

      网友评论

          本文标题:redis从入门到原理

          本文链接:https://www.haomeiwen.com/subject/kyaumctx.html