红魔咖啡馆

头发越掉越多,头发越掉越少

0%

【杂项】Redis

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]:按照\([min,max)\)分数范围正序/倒序返回key集合中元素

zremrangebyscore key min max withscores:照\([min,max)\)分数范围删除key集合中元素

zremrangebyrank key start stop:删除key集合正序排名落在\([start, stop)\)范围元素

应用场景

排行榜

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个邻接节点散布信息

扩容缩容

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

具体逻辑如下:

扩容缩容