【杂项】Redis

  • ~9.34K 字
  1. 1. NoSQL
  2. 2. Redis简介
    1. 2.1. 定位
  3. 3. 数据类型
    1. 3.1. String
      1. 3.1.1. 常用命令
      2. 3.1.2. 应用场景
    2. 3.2. Hash
      1. 3.2.1. 常用命令
      2. 3.2.2. 应用场景
    3. 3.3. List
      1. 3.3.1. 常用命令
      2. 3.3.2. 应用场景
    4. 3.4. Set
      1. 3.4.1. 常用命令
      2. 3.4.2. 应用场景
    5. 3.5. Sorted set(zset)
      1. 3.5.0.1. 常用命令
    6. 3.5.1. 应用场景
  4. 3.6. value设计
  5. 3.7. key设计
    1. 3.7.1. 唯一性
    2. 3.7.2. 可读性
    3. 3.7.3. 灵活性
    4. 3.7.4. 时效性
  • 4. 全局命令
  • 5. Redis事务
  • 6. Redis持久化
    1. 6.1. RDB方式
    2. 6.2. AOF方式
    3. 6.3. 混合持久化
    4. 6.4. 机制选择
  • 7. 缓存机制
    1. 7.1. 内存淘汰机制
      1. 7.1.1. Redis淘汰策略
    2. 7.2. 过期key处理
    3. 7.3. 缓存一致
      1. 7.3.1. Cache Aside
      2. 7.3.2. Read/Write Through
      3. 7.3.3. Write Behind
    4. 7.4. 缓存击穿
    5. 7.5. 缓存穿透
    6. 7.6. 缓存雪崩
  • 8. 集群
    1. 8.1. 主从复制
      1. 8.1.1. 全量复制
      2. 8.1.2. 命令传播
      3. 8.1.3. 断线后重复制
    2. 8.2. 哨兵
    3. 8.3. Cluster
      1. 8.3.1. 分区策略
      2. 8.3.2. 查询策略
      3. 8.3.3. 扩容缩容

  • NoSQL

    非关系型数据库,适合:

    • 超大规模数据
    • 高并发

    的场景

    它把关系型数据库的磁盘IO操作转换为内存操作,因此速度会大量提升

    NoSQL分为四类:

    • KV(Redis):查找速度很快
    • 列存储(HBase):容易进行分布式扩展
    • 文档型(MongoDb):操作非常灵活
    • 图形(Neo4J):社交网络常用,以图结构为数据模型

    Redis简介

    它是以KV对存储的NoSQL,不需要遵守传统数据库的基本要求

    优点:对数据高并发读写,对海量数据高效率存储访问,单线程操作,每个操作都是原子操作,没有并发问题

    为什么不是多线程:

    Redis大部分操作在内存中完成,且采用了高效数据结构

    这样可以避免多线程之间的竞争,省去了多线程切换带来的时间和性能开销,而且也不会导致死锁

    定位

    Redis实现了将磁盘IO操作转换为内存操作,即作为缓存使用,最终把数据再存入数据库

    有利于提高数据读写速度,减轻对数据库的存储和访问压力

    当数据量和并发非常大的时候,我们一般会先操作内存,后续再存入库,称为最终一致性

    注意:Redis不建议存储敏感数据

    数据类型

    Redis的命令格式类似于:类型命令 key value

    操作类似map的kv存储,key大部分为string类型,value根据缓存数据结构可以使用下面讲到的多种类型

    String

    字符串类型是包含多种类型的特殊类型,并且是二进制安全的

    常用命令

    set key value:将kv对缓存在redis中,设置相同key会被覆盖

    get key:获得key对应的值

    incr/decr key:将key对应value值±1,只能针对数值类型

    setex key seconds value:将kv对缓存到redis中,seconds秒后失效

    ttl key:查看key的存活时间,当数值变为-2表示已经失效

    del key:删除key

    setnx key value:若key已存在,不操作;若key不存在,直接添加

    应用场景

    • 计数器:如视频播放量计数使用incr计算播放量,若直接在数据库增加对数据库的压力很大

    • 共享session:

      出于负载均衡的考虑,分布式的服务会把用户的信息访问均衡到不同服务器上,这样用户每次刷新进入的服务器不同,session中可能没有之前的登录信息,需要重新登陆

      为避免这个问题,可以用redis将用户session集中管理,只需要保证redis的高可用和扩展性的,每次获取用户更新或查询登录信息都从redis集中获取

    Hash

    可以看作String类型的field和value的映射表,或者说是一个String集合,适合存储对象,类似于map<string, map<string, type>>

    常用命令

    hset key field value:将field value对缓存到对应key下

    hget key field:就从key对应的hash表中获取对应field的value值

    hexists key field:判断key对应hash表中有无field字段

    hdel key field:删除key对应hash表中的field字段

    hincrby key field increment:给key对应hash表中的field字段加1

    hlen key:查看key对应hash表中的field个数

    hkeys key:获取key对应的hash表中的所有fields

    hvals key:获取key对应的hash表中的所有values

    hgetall key:获取key对应的hash表中的所有kv对

    应用场景

    • 共享session:

    接上面,若想要将用户信息通过redis存入缓存层,有两种方案:

    1. 将User对象转换为json字符串存入redis,侧重于查看,修改麻烦
    2. 将User对象转换为hash对象存入redis,侧重于修改,查看麻烦

    List

    是一个双端链表结构的集合,可以作为栈或队列使用,类似于map<string, list>

    常用命令

    rpush key value:从右边往key集合中添加value值

    lpush key value:从左边往key集合中添加value值

    lrange key start stop:从左边开始列表key的集合,从start开始,stop结束

    rpop key:弹出key集合中最右边的数据

    lpop key:弹出key集合中最左边的数据

    llen key:获取列表长度

    应用场景

    • 用户收藏列表:

    key:user→ value:aid1、aid2、…

    Set

    是String类型的无序集合,底层使用哈希表实现,类似于map<string, set<int>>

    还可以对集合进行交集、并集和差集的集合运算操作

    常用命令

    sadd key members[...]:在key集合中添加member元素

    smembers key:遍历key集合中所有元素

    srem key members [...]:删除key中的members元素

    spop key count:随机从key集合中弹出n个元素

    sdiff key1 key2:返回两个集合的差集

    sinter key1 key2:返回两个集合的交集

    sunion key 1, key2:返回两个集合的并集

    应用场景

    • 去重
    • 抽奖

    Sorted set(zset)

    有序的set类型

    常用命令

    zadd key score member:往key集合中添加member元素,分数为score

    zincrby key increment member:将key集合中的member元素+increment分

    zrange/zrevrange key start stop [withscores]:将key集合中的元素按分数升序/降序排列,若携带withscore参数则会显示分数

    zrank/zrevrank key member:返回member元素在key集合中的正序/倒序排名

    zcard key:返回key集合元素个数

    zrem key member:删除key集合中member元素和分数

    zrangebyscore/revrangebyscore key min max [withscore]:按照分数范围正序/倒序返回key集合中元素

    zremrangebyscore key min max withscores:照分数范围删除key集合中元素

    zremrangebyrank key start stop:删除key集合正序排名落在范围元素

    应用场景

    排行榜

    value设计

    考虑:

    • 是否需要排序:使用sorted set
    • 缓存数据是多个值还是单个值
      • 多个值:重复-list,不重复-set
      • 单个值:简单值-string,对象值-hash

    也可以:

    需要排序的使用sorted set,剩下的将value转换为json字符串,使用string存储,这样可以减少泛型操作的麻烦

    key设计

    唯一性

    redis的key必须保证唯一,缓存同一个key时,后者会覆盖前者

    最常用的命名方式:使用缓存数据的主键作为key,因为主键是保证UNIQUE的

    如员工表的主键是id,使用id作为key

    可读性

    redis的key需要保证见名知意,id虽然保证唯一,但可读性非常差,因此在保证key唯一的前提下,可以给key加上前缀,规范如下:

    • 普通单值,如employee_info:id1
    • 类似关系型数据库设计:表名:主键名:主键值:列名,如employee:id:1:info
    • 通用命名:业务模块名:业务逻辑含义:其他:value类型,如employee:base.info:id1:hash
      • 模块名:表示该key属于哪个功能模块
      • 逻辑含义段:可以使用.分开,具体业务逻辑表示
      • 其他:设置唯一标识,如主键

    灵活性

    key保证唯一的时候,可以用主键,有的用一个主键不能表达全部意思,可以使用联合主键

    时效性

    redis key一定要设置过期时间,要根据自己的业务场景给key设置合理的过期时间,一般在写入key时,就要追加过期时间,也可以按需动态设置

    • 若不设置过期时间,这种key就是永久key,会一直占用内存不释放,时间久数量多后容易达到服务器的内存上限,导致宕机;一般配合key过期策略使用
    • key的时效性设置,必须根据业务场景评估,设置合理有效期

    全局命令

    全局命令针对的是所有key,用于运维管理

    keys pattern:按照pattern匹配规则列出所有keys

    exist key:判断key是否存在

    expire key seconds :给key设置过期时间

    persist key:取消key过期时间

    select index:切换数据库,共有0-15共16个,默认第0个

    move key db:移动key到其他数据库

    randomkey:随机命名key

    rename key newkey:重命名key

    Redis事务

    一个事务从开始到执行会经历:开始事务、命令入队、执行事务的三个阶段

    单个Redis命令的执行是原子性的,但Redis没有在事务上增加任何维持原子性的机制,所以Redis事务的执行不是原子性的

    Redis的事务可以理解为一个打包的批量执行脚本,但不是原子化操作;中间某个指令执行失败不会导致前面已做指令的回滚,也不会导致后续的指令不做

    使用multi开启一个事务,这之后添加的命令都会标记为queued,当执行exec命令后才会统一执行,注意:

    • 事务中任意命令执行失败,其余命令依然被执行
    • 在事务执行过程中,其他客户端提交的命令请求不会插入到事务执行命令队列中

    Redis持久化

    Redis的持久化机制类似如下:

    分为三种:

    • 快照方式(RDB)
    • 文件追加方式(AOF)
    • 混合持久化

    RDB方式

    默认方式,将内存数据以快照的方式写入到二进制文件中,默认为dump.rdb触发RDB持久化过程分为手动和自动触发

    • 手动触发
      • save命令:会阻塞当前Redis服务器,直到RDB过程完成为止,如果内存数据较多,会造成长时间阻塞,影响其他命令使用,不建议使用
      • bgsave命令:Redis进程执行fork指令创建子进程,由于进程实现RDB持久化,建议使用该命令
    • 自动触发:使用save m n表示m秒内数据集存在n次修改时会自动触发bgsave命令

    优点:

    • RDB快照文件是一个压缩的二进制文件,适用于备份、全量复制登场景,开发中可以按照每六小时执行一次备份用于容灾备份
    • Redis加载RDB回复数据远远快于AOF方式

    缺点:

    • 无法做刀持久化/秒级持久化,执行fork有时间成本
    • 快照文件不同版本格式不同,有兼容问题

    AOF方式

    AOF以独立日志方式记录每次写命令,重启时再重新执行AOF文件中命令达到恢复数据的目的,解决了数据持久化实时性的问题

    默认是不开启AOF的,需要配置appendonly yes的情况下才会开启

    AOF有三种文件同步策略:

    • appendfsync always:收到命令就立即写到磁盘,效率最慢,但是持久化最完全
    • appendfsync everysec:每秒写入磁盘一次,折中
    • appendfsync no:完全依赖os,一般周期是30s

    优点:

    • 数据安全性更高,最多损失一秒数据量
    • 不小心执行了flushall指令,也可以通过AOF方式删除最后一个命令来恢复
    • AOF是一个增量日志文件,不会存在断电时出现损坏问题,即使有问题也可以通过redis-check-aof恢复
    • AOF变得太大时,Redis能够在后台重写AOF

    缺点:

    • AOF文件体积通常大于RDB文件
    • 持久化性能来说,AOF慢

    混合持久化

    在写入时,先把当前的数据以RDB形式写入开头,再将后续的操作命令以AOF格式存入文件,即以RDB为全量备份,AOF作为增量备份来提高备份效率,这样既能保证速度,又能防止数据丢失

    机制选择

    • 若对数据安全性有非常高的要求:同时启用
    • 若能容忍数据丢失:单独使用RDB
    • 不建议单独使用AOF
    • 无特殊要求,可以使用混合方式

    缓存机制

    内存淘汰机制

    Redis启动时会加载一个配置maxmemory,表示默认设置Redis内存上限,当内存达到上限后,会执行Redis内存淘汰机制

    常见的淘汰策略:

    • LRU:最近最少使用

      使用双向链表+哈希表的方式存储,双向链表存储kv对,哈希表存储key与链表节点的指针关系,当某个key被访问到时,程序可以通过哈希表迅速定位到链表节点,并将该节点调整至链表的最开始位置

    • LFU:最不经常使用

      元素数据会附加一个属性:使用次数,通过链表串起来,被访问次数最多的放在最前面,最少的放在最后面

    • TTL:最靠近过期时间的

    • 随机淘汰

    Redis淘汰策略

    Redis通过maxmemory-policy来配置指定具体淘汰机制

    • volatile-lru:对设置过期时间的数据集,执行lru淘汰策略

    • volatile-lfu:对设置过期时间的数据集,执行lfu淘汰策略

    • volatile-ttl:对设置过期时间的数据集,执行ttl淘汰策略

    • volatile-random:对设置过期时间的数据集,随机淘汰

    • allkeys-lru:对所有数据集,执行lru淘汰策略

    • allkeys-lfu:对所有数据集,执行lfu淘汰策略

    • allkeys-random:对所有数据集,随机淘汰

    • no-enviction:报错,告诉你内存不足

    默认是报错

    过期key处理

    key过期的清理问题:

    • 惰性删除:当访问key时,才判断是否过期,若过期就直接删掉,对CPU友好,但会浪费内存
    • 定时删除:设置key过期时间的同时创建定时器,当到达过期节点时立即执行删除key操作,对CPU不友好,需要额外让出CPU维护定时器
    • 定期删除:隔一段时间对数据进行一次检查,删除里面的过期key,删多少由算法决定

    Redis实际配合使用惰性删除和定期删除两种策略,当不主动删除时都是采用惰性删除

    缓存一致

    更新数据库和更新/删除缓存是两个独立操作,无法做到原子性,因此要尽量做到:

    • 缩短不一致时间
    • 保证最终一致性
    • 避免长时间的脏数据

    Cache Aside

    核心思想:应用自己控制缓存和数据库的读写,当缓存的数据有更新值了,采用删除缓存数据而不是更新缓存数据

    模式1:先删后改

    此时写请求慢于读请求,修改数据库的操作一般晚于查询数据库更新缓存的操作,导致后面用户读缓存失败,读数据库读到旧值,并回填旧值缓存,最后导致数据库是新值而缓存是旧值

    可以使用延时双删策略:完成修改后,重新删除更新的一次老缓存数据,再查询时会把新的数据回填,这样即使有读请求抢先回填了旧值,第二次删除也会把旧值删掉

    缺点是延迟时间难以精确控制,太短删不掉,太长影响性能

    模式2:先改后删

    问题:假设数据库更新成功但删除缓存失败,会导致缓存是旧值而数据库是新值

    解决方法:

    • 重试删除:若删除失败,放入重试队列异步重试删除
    • 订阅binlog删除缓存:使用工具监听MySQLbinlog,若数据变更,自动删除对应缓存
    • 设置缓存过期时间TTL:即使删除失败,缓存最后也会过期,可作为兜底

    一般第二种模式更常用,因为先改数据库后,缓存删除前的不一致窗口更小,且删除操作幂等

    Read/Write Through

    应用不直接操作数据库,把缓存作为主要的数据读取方式

    RW Through

    特点点:

    • 对应用透明,应用代码简单
    • 一致性强,但写延迟高
    • 实现复杂,缓存层需要处理并发和双写等逻辑

    Write Behind

    核心思想:写请求先更新缓存,立即返回成功;数据库由后台异步批量更新

    实现逻辑类似于:首先更新缓存,并标记该key为脏数据,立即返回成功,后台异步线程定期定量批量将脏数据刷到数据库,一般需要如下组件:

    • 缓存存储:Redis等保存最新数据
    • 脏数据队列/Map:记录哪些key已经修改但未持久化到数据库
    • 后台Flush线程:定时触发或达到阈值时触发批量写入
    • 版本号/时间戳:防止刷盘期间又有新写入导致旧值覆盖新值
    • 失败重试机制:刷盘失败后重新入队,保证最终一致
    • 持久化WAL:先将操作写入日志,若崩溃恢复后重放

    方式1:将更新步骤放到消息队列中,异步执行

    从缓存到数据库:用户更新缓存后,发送mq消息,消费者异步写数据库

    方式2:模拟MySQL主从复制,让MySQL认为是从节点,从而把更新的binlog发出并解析,转为结构化数据给下游程序使用

    这是数据库变更捕获(CDC)的方法,数据库先更新,通过binlog等异步通知缓存或其它系统进行更新,常用于解决删除缓存失败,多实例缓存同步等问题,通常用于删除缓存

    优点:

    • 写延迟低:不需要等待数据库
    • 吞吐高:后台批量写数据库,减少连接数和IO
    • 合并写:同一key的多次更新可以合并写一次操作,减轻数据库压力

    缺点:

    • 数据可能丢失:若脏数据还没刷到数据库发生崩溃等,会丢失,需要WAL等持久化机制弥补
    • 一致性弱:只能保证最终一直性
    • 实现复杂

    缓存击穿

    查询某个数据的值,缓存中没有,数据库中有,因此想查询只能直接打到数据库上查询

    一般缓存都会设置过期时间,所以缓存击穿比较常见

    带来的问题:若对某条数据的查询突然特别大,由于缓存中无该数据,所以大量请求会打到后端数据库中,导致数据库宕机

    解决方法:

    • MySQL:通过加锁等方式减少击穿后的直接流量
    • Redis:
      • 设置热点数据不过期
      • 热点数据后台启动异步线程,重新把数据回填进缓存层

    缓存穿透

    查询一个数据,缓存查不到数据库也查不到,导致穿透

    解决方法如下:

    • 拦截非法查询请求
    • 缓存空对象
    • 布隆过滤器:设计k个各不相同的哈希映射函数,把数据库的有效时间通过这个映射函数得到数组指定位置,并设置为1;查询时,输入查询数据,通过这一系列哈希函数判断对应位置是否全为1,若是,则很大概率是有效请求,如果有一个是0,则一定是无效查询

    缓存雪崩

    一大批被缓存的数据同时失效,对于这批数据请求全打到数据库上,导致数据库宕机

    和缓存击穿的区别是单点与全面,所以可以用缓存击穿的方式解决:

    • MySQL:通过加锁减少并发量

    • Redis:

      • 设置热点数据永不过期
      • 在上线前根据情况分析,将热数据直接加载到缓存系统
      • 分析失效时间,尽量让失效时间分散

    集群

    主从复制

    多台服务器安装了Redis的情况下,将一台机器设为主库,其余设置为从库,它们之间保持数据同步,若客户端有一些写命令,会打到主库上,主库修改后会将数据同步到从库;而读命令可以直接询问从库

    首先需要修改配置文件,指定谁是主库谁是从库;上线后主从库会进行握手通信,当握手完成后,从库需要向主库发送PSYN命令,开启数据同步过程,并发送主库ID和复制进度偏移量offset

    主库收到信息后,会进行逻辑判断,判断该怎么继续执行步骤,告诉从库是进行全量复制(第一次上线)还是断线后重复制(断线重连)

    全量复制

    初次复制后进行同步:

    • 主库执行bgsave,生成对应RDB文件,并开启缓存区,记录RDB文件执行过程中,收到的新数据命令
    • RDB文件产生后,主库把它发给从库,从库通过RDB恢复数据

    命令传播

    • 当主库状态被修改(增加。修改数据),为了使得从库和主库数据状态一致,主库会把数据变更命令发给从库,从库收到后执行命令

    断线后重复制

    当从库和主库断线重连后,需要同步数据,依赖以下数据:

    • 服务器运行ID:唯一确定主库身份
    • 复制偏移量:代表主节点传输的字节数
    • 复制积压缓冲区:先进先出的队列,存储最近主节点的数据修改命令
    流程

    哨兵

    新建哨兵组,对主从库统一监控,如果主库坏了,哨兵组进行投票,从从库中重新选出主库,本质上说,哨兵系统是一个不提供数据服务的Redis服务器

    每个机器会向烧饼一定时间发送信息(心跳),哨兵系统如果一定时间收不到这个信息,就默认这个机器断联

    具体逻辑是:当某个哨兵发现主库连不上,会标记为主观下线,并通知其他哨兵连接主库

    超过半数哨兵确认连接不上后,就会标记为客观下线,并执行故障转移

    故障转移的过程中,会通过选举协议,在所有哨兵中选择一个节点主持新主库的选举工作,选取完成后,哨兵会对这个从库发送slave of no one,该从库就会变为主库,同时向其他slave发送新主库的IP和端口号

    如果此时主库重新上线,就会把他降级为从库,从属于新主库

    Cluster

    Cluster是Redis提供的分布式数据库解决方案,它会自动将你的数据切分为多个节点存储,即使一部分宕机,也可以继续执行数据操作

    分区策略

    采用虚拟槽,所有键通过CRC16校验函数,对16384取模,决定数据分配到哪个槽位,每个Redis的cluster节点负责一部分槽数据的存储,并且该节点可以结合主从复制模式,将分配给它的数据进行复制

    查询策略

    每个节点会存储整个集群节点信息,也称元信息,这个节点存储的元数据包括在该节点视角下:

    • 各个节点存储的槽数据
    • 各个节点的master和slave状态
    • 各个节点是否存活

    元数据信息的传播会通过gossip协议,每个节点都把自己的数据信息通过该协议散布出去

    • 每隔一段时间执行一次传播
    • 所有被感染的节点选择k个邻接节点散布信息

    扩容缩容

    由于集群数据可能出现服务器扩容,服务器缩容,该过程中,数据会发生迁移,因此此时查询的时候可能会出现一部分数据还在本节点,一部分数据被迁移到其他节点

    具体逻辑如下:

    扩容缩容
    赞助喵
    非常感谢您的喜欢!
    赞助喵