「Redis」概述

  1. 概述

    1. Redis是由C编写的key-val型内存数据库, 可以被当做数据结构服务器

    2. value提供了如下数据结构: string(字符串), list(链表), set(集合), zset(有序集合), hash(字典)

    3. 与memcache比较

      1. 都是key-value型数据库, 都存储在内存中

  2. memcache的value仅支持string

    1. redis支持数据持久化, 重启时可重新加载, memcache不支持持久化

  3. redis速度远快于memcache

    1. memcache是多线程, redis是单线程, IO复用

  4. redis支持事务

    1. 使用redis的string做的事, 都可以换为memcache, 以此换取更好性能

  5. 具体结构

    1. 基础结构

      1. string (字符串)

        1. 常用方法: set, get, incr, decr

        2. 应用: 计数, 分布式锁

        3. 底层结构

          1. 动态字符串, 字符串小于1M时, 每次扩容空间翻倍. 超过1M时, 每次扩容都增加1M, 最大为512M

          2. struct {

            int capacity; 容量

            int len; 实际长度

            byte[] content; 内容

            }

      2. list (列表)

        1. 常用方法: lpush, lpop, rpush, rpop

        2. 应用: 异步消息队列

        3. 双向链表, 插入/删除时效为O(1), 查找时效为O(n)

        4. 底层结构

          1. 当元素较少, 即元素个数少于512时, 使用压缩列表. 当元素较多时, 使用快速链表.

          2. 压缩列表 (ziplist)

            1. 压缩列表占用一块连续的内存, 构成没有前后指针的普通数组. 增加元素时, 可能在原地址上向外扩展, 也可能需要分配新的内存空间, 将内容从原地址拷贝至新地址. 故压缩列表不能存储过多元素

            2. struct entry {

              int prelen; 前一个元素的字节长度

              int encoding;

              byte[] content; 该元素内容

              }

              struct ziplist {

              int bytes; 整个压缩列表占用的字节数

              int tail_offset; 最后一个元素距离压缩列表起始位置的字节数, 便于反向遍历

              Int length; 元素总数

              T[] entires; 元素列表

              int end; 压缩列表结束标志

              }

          3. 快速链表 (quicklist)

            1. 将多个"压缩列表"通过双向指针相连, 称为"快速链表". 其避免内存碎片过多, 又兼顾效率

            2. struct quickListNode {

              quickListNode* prev; 指向前一个压缩列表

              quickListNode* next; 指向后一个压缩列表

              ziplist* p; 指向当前压缩列表头

              int count; 当前压缩列表中元素总数

              int size; 当前压缩列表占用的字节总数

              }

              struct quickList {

              quickListNode* head; 快速链表头

              quickListNode* tail; 快速链表尾

              int count; 元素总数

              int nodes; 压缩列表个数

              }

      3. hash (字典)

        1. 常用方法: hget, hset

        2. 应用: 适合存储对象, 避免全部序列化, 可以仅部分获取

        3. 底层结构

          1. 当元素较少, 即键值对数量少于512时, 使用压缩列表. 元素较多时, 使用字典

          2. 压缩列表 (ziplist)

            1. key先作为一个元素存入压缩列表末尾, val作为另一个元素存入压缩列表末尾, 同个键值对总是紧挨着

          3. 字典 (dict)

            1. 字典内部包含两个哈希表, 一般只有一个有值, 当字典需要扩容/缩容时, 就需要用到另一个哈希表, 并进行渐进式搬迁, 搬迁完后删除旧的

            2. 哈希表由第一维数组+第二维链表组成, 第一维数组存储第二维链表的首元素地址

            3. 渐进式rehash

              1. 因Redis是单线程的, 扩容/缩容时不会一次性rehash, 而是使用渐进式rehash

              2. 开始rehash时, rehashindex置为0, 在任何对字典的操作后, 将把旧哈希表中rehashindex对应的key=>val搬迁到新哈希表中, 并且rehashindex+1

            4. struct dict {

              hash hash[2]; 两个哈希表

              int rehashindex = -1

              }

            5. 字典在Redis里用的很多, Redis本身也是一个字典结构

              struct Redis {

              dict dict; Redis中所有的"key=>val"对

              dict expires; 设置过过期时间的"key=>过期时间"对

              }

      4. set (集合)

        1. 常用方法: sadd, spop, smembers, sismember, scard

        2. 应用: 异步消息队列, 中奖名单

        3. 底层结构

          1. 当元素较少时, 使用intset. 元素较多时, 使用字典 (val为None)

      5. zset (有序集合)

        1. 是一个set, 可以给每个value赋予一个score, 代表该value的排序权重

        2. 常用方法: zadd,zrem, zcard, zremrangebyscore

        3. 应用: 限流

        4. 底层结构

          1. 当元素较少时, 使用压缩列表. 当元素较多时, 使用字典+跳跃列表

          2. 字典+跳跃列表

            1. 字典存储value和score, 跳跃列表会按score排序. 两结构结合, 使得查找时效为O(1), 排序/范围获取时效为O(logN)

            2. struct zset {

              dict A; 所有val=>score对

              skiplist A; 跳跃列表

              }

          3. 跳跃列表 (skiplist)

            1. struct skiplistnode {

              string value;

              double score;

              skiplistnode*[] forwards; 多层指针

              }

              struct skiplist {

              skiplistnode* header; 首元素指针

              int maxLevel; 当前最高层数

              }

            2. 跳跃列表是在有序链表上附加指针数组组成的. 因查找/增加/删除元素时可快速跳过部分元素而得名.

            3. 有序链表有头结点, 头结点val为None, score为最小值

            4. 每个更高层都充当下层的快速通道, 层i中的元素按概率出现在层i+1中

            5. 查找/增加/删除元素的时间复杂度都是O(logN)

            6. 跳表使用概率性均衡, 而不是平衡二叉树的强制性均衡, 在插入/删除元素时更简洁高效. 跳表效率和平衡二叉树差不多, 但在并行计算下, 更新数据时, 需要锁的东西比较少, 相比平衡树不需要全局的重新平衡

            7. 查找X

              1. 一共有64层, 一般通过maxLevel确定目前顶层的位置, 节省从最顶层开始查找的时间

              2. 先通过每个节点最上层的指针查找, 当找到比X大的节点时, 表明X可能再前一个节点至该节点间, 进入下一层. 如此反复, 找到对应节点

            8. 插入X

              1. 查找X的位置

              2. 申请新节点

              3. 调整底层指针, 按概率调整上层指针. 拿一个数组保存每一层的前继指针

            9. 删除X

              1. 查找X的位置

              2. 调整指针

              3. 删除节点

            10. 更新X为Y

              1. 删除X

              2. 增加Y

    2. 高级结构

      1. HyperLogLog

        1. 类似于set, 用于不精确统计

        2. 常用方法: pfadd, pfcount, pfmerge(合并多个页面的uv)

        3. 应用: 统计页面uv

      2. Bloomfilter (布隆过滤器)

        1. 类似于set, 用于不精确判断. 已存入的一定能判断为"已存入", 未存入的也有一定几率能判断为"已存入"

        2. 应用: 用户推荐时去除已浏览, 爬虫去重, 垃圾邮件过滤, 预先排除查询HBase时不存在的row

        3. 原理:

          1. 初始化一个数组. 当一个val存入时, 会被多个无偏hash函数分别hash算得一个整数索引值并求余数组的长度从而得到一个位置, 再将初始数组中对应的位置都标记为1. 判断val是否存在时, 逻辑一致. 可见初始数组越大, 精度越高.

          2. 自行实现

            1. hash算法使用MurmurHash3, 非加密哈希算法, 计算速度快, 计算均匀

            2. 上述hash算法, 可使用3个随机种子, 变成3个hash函数

            3. 使用管道, 合并多个val的存入和读取, 减少连接耗时

      3. redis-cell (分布式令牌桶限流)

      4. GeoHash (地理位置, 附近的人)

  6. 应用

    1. 分布式锁 (string)

      1. set 键 值 ex 5 nx

      2. 该操作是原子性的, 不会被进程调度算法中断

      3. 必须有nx, 即不存在再set

      4. 必须有超时时间, 超时时间需大于加锁与释放锁间的逻辑执行时间

      5. 缺点是当对主节点加锁后, 主节点挂了, 但从节点还没同步就变为主节点, 使主节点可再次落锁. 解决是使用RedLock算法, 多个主节点组成集群, 对过半节点发送setnx命令, 收到过半节点响应就认为加锁成功, 释放锁时, 向所有节点发送del命令, 缺点是性能低

    2. 位图 (string)

      1. 应用: 用户签到 (1个用户1年1个对应的string, 365位, 即46字节), 布隆过滤器

      2. 位图不是redis专门的数据结构, 而是针对string结构, 提供了位操作setbit/getbit而已

    3. 异步消息队列 (list)

      1. 相较于消息队列, 优点是更简单, 缺点是没有ACK

      2. 消费者用lpop, rpop不停读取, 可用sleep降低qps, 但会有延时. 可用blpop/brpop阻塞读取解决延时, 即当队列为空时会自动休眠, 有数据到来, 会立即醒来. 休眠过久会超时, 服务器会断开连接, 注意捕获异常并不断尝试

    4. 限流

      1. 普通限流: 限定某一行为在某一段时间内的次数. 用zset制作滑动窗口, value和score都是timestamp

      2. 漏斗限流: 无法应对短时间的突发流量

      3. 令牌桶限流: 允许短时间的突发流量

  7. Redis原理

    1. redis是个单线程服务器, 其是高性能服务器的典范. 读10万/秒, 写8万/秒. 小心O(n)时效的命令, 会造成卡顿

    2. 高性能原因

      1. 数据在内存

      2. IO多路复用

      3. 单线程, 避免了上下文切换, 不存在加锁/释放锁

    3. 每个进程开启一个socket连接, 都会占用一个文件描述符 redis会为每个客户端套接字关联一个指令队列, 客户端指令先放到队列中, redis服务器按顺序先到先服务 redis会为每个客户端套接字关联一个响应队列, redis服务器通过响应队列将指令处理结果返回给客户端

  8. Redis持久化

    1. 3种方式

      1. RDB (快照)

        1. 全量数据的二进制写入磁盘

        2. 原理: 父进程fork子进程, 共享内存. 子进程不断读取内存进行存盘, 此时父进程继续处理客户端请求, 当需要修改数据时, 会将数据复制到新的内存进行写入, 保证原有内存上数据不变

        3. 缺点: 重启时会丢失数据

        4. 优点: 重启时加载数据速度快

        5. 通过bgsave命令可触发全量备份. 另外还有save命令, 该命令不fork子进程, 而是父进程来进行全量备份, 会暂停服务

      2. AOF日志

        1. 对数据修改命令进行增量备份

        2. 原理

          1. 先在内存中执行命令, 再存盘日志, 不同于MySQL的WAL技术

          2. AOF日志在长期运行中会越来越大, 需要定期重写AOF, 缩减大小. 重写时, 父进程fork子进程遍历内存数据, 将数据转换成一系列Redis命令, 并顺序写入新AOF日志, 完毕后再将操作期间的增量AOF日志追加到新AOF日志中, 追加完毕后用新AOF替代旧AOF.

          3. IO同步函数不用write, 而用fsync. 相较于write先写入缓冲再随机写入磁盘, fsync会将内容强制写入磁盘. Redis一般1秒调用1次fsync

        3. 缺点: 重启时需要加载AOF日志进行重放, 很耗时

        4. 优点: 重启时基本不会丢失数据

      3. 混合方式

        1. AOF不再存储全量日志, 而是上一次持久化开始后的增量日志

        2. 重启时, 先通过快照加载数据, 然后重放增量AOF日志

    2. 持久化一般在从节点上进行. 若数据很重要又必须放在Redis中, 建议从节点开启AOF持久化做备份, 策略调整为一次命令调用一次fsync

  9. Redis过期策略

    1. 惰性过期

      1. 客户端访问key时, 如果该key已过期, 就立即删除

      2. 优点: 对CPU友好

      3. 缺点: 对内存不友好

    2. 定时扫描

      1. redis会将设置过期时间的key放入独立字典中, 每秒进行十次扫描,每次扫描随机从独立字典中取20个key, 删除这些key中过期的, 每次定时扫描处理不超过25ms, 以避免读写请求卡顿

      2. 优点: 对内存友好

      3. 缺点: 要避免同一时间大量key过期

    3. 主库同时用以上2种策略, 主库删除后通过在AOF中追加del命令, 从库通过同步与主库保持数据一致

  10. Redis淘汰策略

    1. 当Redis内存超出物理内存限制时, 内存中的数据会频繁和磁盘产生交换. 生成环境要杜绝交换, 当Redis使用内存超出限制时, 选用策略淘汰数据

    2. 具体策略

      1. noeviction: 不继续服务写请求, 只继续服务读请求, 默认策略

      2. volatile-xxx: 对带过期时间的key淘汰. xxx为ttl, 剩余寿命越小先淘汰; xxx为random, 随机淘汰; xxx为lru, 最少使用先淘汰

      3. allkeys-xxx: 对所有key淘汰. xxx可为lru, random

    3. redis用于缓存时, 用allkey-xxx系列策略. redis用于持久化存储时, 用volatile-xxx策略

  11. 管道 (pipeline)

    Redis底层是TCP连接. 管道并不是服务器特性, 而是客户端通过改变命令顺序, 将几次命令一并向服务器发送, 节省原本需要单独请求的时间, 从而大幅提升性能.

  12. 事务

    1. Redis事务保证事务中的多个命令能被连续执行, 中间不会插入其他命令

    2. 事务的四大特性, Redis事务仅支持隔离性. 因无法回滚而没有原子性. 当持久化为AOF模式, 且一个命令一个fsync时, Redis事务也具有持久性

    3. multi: 开启事务

      exec: 提交并执行命令

      discard: 回滚, 该回滚只是丢弃事务队列中的命令. 比如事务中对字符串自增, 虽然该操作失败, 但后续操作仍可生效

      watch: 用来监控某个key, 写在multi开启事务前. exec时会检查key从watch后是否发生变化, 有变化则报错, 否则执行成功. watch是乐观锁, redis分布式锁是悲观锁

    4. 没exec前, 命令被按序存到服务器的事务队列中, exec后开始执行整个队列, 执行完后一次性返回结果

  13. Redis主从

    1. CAP原理

      1. C: 一致性, 所有节点都有相同的数据

      2. A: 可用性, 服务能对外正常提供服务, 不保证数据是最新的

      3. P: 分区容忍性

      4. 三项中只能同时满足两项. 如坚持分区容忍性, 当网络分区时, 即节点间无法同步数据, 一致性和可用性两难全. 要么关闭服务牺牲可用性, 要么放弃节点间数据同步牺牲一致性.

    2. 主从复制

      1. Redis同步支持主从同步, 从从同步(避免主压力太大), 过程是异步的, 不能保持一致性, 仅能保持最终一致性

      2. 当从节点首次连接主节点, 其会给主节点发送PSYNC命令, 会先做快照同步, 接着再做增量同步

      3. 同步方式

        1. 增量同步: 主节点会将修改数据的命令存储在内存的循环队列上, 并异步将命令同步到从节点上

        2. 快照同步: 从节点长时间无法同步后, 主节点会产生全量数据快照, 从节点根据快照重放, 重放后进行增量同步

      4. 主从架构下当主节点宕机后, 不要立即拉起, 而要先确认主节点是否有持久化, 如果没有则需要从节点的快照文件, 以避免主节点重启后, 将空数据同步给从节点

    3. Sentinel集群 (哨兵集群)

      1. 类似于ZooKeeper集群, 由3至5节点构成, 是整个Redis集群的心脏

      2. 哨兵集群的出现解决了故障转移, 但本质上还是一台机器完成写操作

      3. 客户端连接集群时, 先连接哨兵集群, 通过哨兵集群获取Redis主节点地址, 再连接主节点. 当主节点异常时, 哨兵集群会进行主从切换, 客户端会通过哨兵集群获取新的主节点地址. 同时哨兵集群将监控异常节点

      4. 客户端通过哨兵集群获取新的主节点地址的情况: 1) 主节点不可用, 连接异常 2) 主节点可用, 但被哨兵集群降为从节点, 修改命令会报ReadOnlyError

    4. 客户端分片 (Redis Sharding)

      1. 客户端使用普通哈希算法(murmur hash)或一致性哈希算法将key进行散列到对应的Redis节点. 是一种客户端数据分区技术

      2. 优点是配置简单, 服务器各Redis节点间无关联. 缺点是客户端无法动态增删Redis节点, 可维护性差.

      3. 一致性hash

        1. 多个服务器节点用主机名或IP做hash, 求余后放置在圆环上

        2. 当要确定某个key需要存取到哪个节点时, 对key做hash并求余, 确定环上的位置. 按顺时针方向在环上行走, 遇到的第一个节点就是服务器节点

        3. 当部分服务器崩溃时, 少量key会漂移到下一个节点, 会对下一个节点造成压力, 故引入虚拟节点. 对服务器节点用主机名或IP增加数字后缀后hash, 求余后放置在圆环上, 即使一个服务器崩溃, 也会映射到不同的位置.

    5. 代理服务器分片

      1. 客户端将请求发送到代理服务器, 代理服务器将请求转发到某Redis节点, 并将结果返回给客户端

      2. 优点是便于增删节点, 缺点是耗时

      3. 业界方案: Codis, Twemproxy

    6. 查询路由 (Redis Cluster)

      1. 官方分区方案. Cluster是去中心化的, 客户端可以通过一个节点来发现所有节点地址, 用key请求任一节点, 该节点会告知目标节点地址

      2. 将数据划分为16384个槽, 每个节点负责一部分槽, 需要对key做crc16后对16384取模, 得到对应的槽位. 槽位信息存储在各节点中, 当客户端连接时, 它也会得到一份槽位信息.

      3. 客户端之后查找key时, 可以直接向槽位对应的目标节点请求. 当客户端请求了错误的节点后, 节点会返回moved指令, 并将key所对应槽位的服务器地址返回给客户端. 客户端纠正本地槽位信息, 并重发请求

      4. 迁移

        1. Redis迁移的单位是槽, 当一个槽正在迁移时, 该槽在原节点与目标节点都会被设为迁移状态. Redis会获取该槽下的所有key, 再对key进行迁移, 迁移key时原节点是阻塞的, 所有对原节点的访问都会被阻塞, 故要尽量避免大key产生.

        2. 客户端访问在迁移中的槽, 会首先访问原节点. 如果key还在原节点中, 正常处理. 否则原节点向客户端返回"ask 目标节点"指令. 客户端先向目标节点发出"asking"指令, 告知目标下个指令不要向客户端返回moved指令, 以避免重定向循环. 然后客户端再向目标节点发送原先命令

      5. 一个主节点可有多个从节点, 数据并不保持一致性, 详见主从复制. 自动管理主从切换. 节点间通过gossip协议进行通信

  14. 缓存异常

    1. 缓存雪崩

      1. 定义: 缓存在同一时间大面积过期. 请求将直接落在数据库上, 造成数据库崩溃

      2. 解决: 过期时间随机设置

    2. 缓存穿透

      1. 定义: 缓存和数据库中都没有的数据. 请求直接落在数据库上, 造成数据库崩溃

      2. 解决

        1. 对数据库也不存在的数据, 设为key-null, 过期时间短点

        2. 入口检验

    3. 缓存击穿

      1. 定义: 热点数据过期, 对这个热点数据的请求将落在数据库上, 造成数据库崩溃

      2. 解决

        1. 热点数据永不过期

        2. 使用互斥锁 (写锁)

          当缓存中没有数据需要访问数据库时, 先对该数据加分布式互斥锁, 加锁成功后再去从数据库查询数据到缓存中. 加锁不成功, 则重新读缓存. 这样保证对数据库中的同个key同时只有一个进程在访问.

    4. 缓存预热

      1. 定义: 服务即将上线前, 将数据先加载到缓存中, 避免请求落在数据库上

    5. 缓存降级

      1. 系统异常时, 缓存返回默认值

  15. 扩展

    1. Redis默认有16个数据库, 编号从db0至db15, 默认为db0库, 可通过select命令切换, 库之间相互隔离, 不建议修改库.

    2. 数据库缓存双写时, 如何保证数据一致性

      先写数据库, 写完后删除缓存. 下次读取时, 缓存中不存在数据, 则读取数据库, 之后更新到缓存

    3. 找出特定前缀的key

      1. keys 简单正则

        找到所有与正则符合的键值对, 时效为O(n), 不能用于生产环境, 会造成卡顿

      2. scan 游标值 正则模式 正则式 count 数量限制

        1. redis中存储结构是一个一维数组和二维链表. scan返回的游标是一维数组的位置索引

        2. scan可通过类型和大小定位大key

    4. 考虑Redis扩容时, 应使用的哈希策略

      1. Redis用于缓存: 一致性哈希

      2. Redis用于数据存储, 使用固定的key2node映射

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值