安装和配置Redis

mac平台可以直接使用brew安装redis, 客户端需要参考上述链接进行安装

Redis功能速览

Redis支持多种数据类型,具体包括字符串(简单键值对)、哈希表、列表、集合以及有序集合。其中各种数据结构支持的功能如下

  1. 字符串:键值对增删改查,数字加减操作,任意长度比特位运算
  2. 哈希表:哈希表内的键值对增删改查
  3. 列表:栈和队列,切片操作
  4. 集合:集合运算
  5. 有序集合:可排序的集合

Redis 5.0新增流类型数据结构,可以视为列表和有序集合的补充,该结构具有如下特定

  1. 只能在末尾追加写入
  2. 每一个条目可以是多个键值对

Redis提供SORT指令对数据进行排序,可以按照数值大小,字典顺序对列表等数据结构进行排序

Redis脚本

由于Redis的每一种命令都比较简单, 因此为了支持复杂逻辑的实现, Redis支持执行Lua脚本. 使用

1
EVAL script numkeys [key [key ...]] [arg [arg ...]]

执行脚本, 其中script为任意的lua脚本, numkeys表示该指令访问的key数量, 后续是numkeys个key, 以及其余的参数. 例如

1
2
> EVAL "return ARGV[1]" 0 hello
"hello"

注意: 不建议在脚本中访问没有在指令中声明的key

上述方案每次都需要附带脚本, 因此对网络带宽消耗更多, 此时可以选择先将脚本上传到服务器, 然后按照哈希值进行调用, 例如

1
2
3
4
5
redis> SCRIPT LOAD "return 'hello moto'"
"232fd51614574cf0867b83d384a5e898cfd24e5a"

redis> EVALSHA 232fd51614574cf0867b83d384a5e898cfd24e5a 0
"hello moto"

Redis事务

Redis的事务涉及四个指令, 分别是MULTI 、 EXEC 、 DISCARD 和 WATCH. 其中MULTI表示开启事务, EXEC表示执行事务, DISCARD表示取消事务.

Redis的事务实现并没有采取类似数据库的方案, 而是采取了队列的方案. 当Redis收到MULTI指令后, 后续的所有指令都会先放入一个队列之中, 直到收到EXEC指令后才一次性执行所有指令. 因为Redis的执行过程是单线程的, 保证了这一系列操作的原子性.

如果指令中存在错误, Redis并不会回滚, 其他指令还是会正常执行

由于指令是一次性全部执行, 因此不能在事务中根据条件执行不同的操作. 如果有这种需求, 则需要使用Redis脚本.


使用WATCH指令可以在事务中监控某些变量是否发生变化. 如果没有发生变化则执行整个事务, 否则不执行事务. 这一设计有点类似与乐观锁的思想. 通过这一设置可以分步的执行Reids指令, 使得在事务中能够进行一些判断.

Redis分布式锁

Redis分布式锁有两种实现方式, 分别是单节点模式和集群模式(也被称为RedLock).

单节点实现

单节点模式的核心是使用setnx指令, 一般具有如下的格式:

1
2
3
4
5
6
7
8
9
-- 获取锁(unique_value可以是UUID等)
SET resource_name unique_value NX PX 30000

-- 释放锁(lua脚本中, 一定要比较value, 防止误解锁)
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

注意, 在实现分布式锁的时候, 加锁操作一定要原子的设置锁并指定超时时间, 否则有可能因为指定超时时间的语句没有执行导致锁的释放出现问题. 加锁的时候需要设置一个唯一值, 在解锁的时候需要判断值是否相同, 以免出现前一个锁因为到达超时时间, 已经被释放的情况下, 释放了其他线程新加的锁.

此实现下的锁显然是不可重入的, 业务上要避免重入

RedLock实现

RedLock是用于集群环境下的分布式锁. 假设集群中具有N个独立的节点(不存在主从复制关系), RedLock的核心思想是在这N个节点上, 使超过半数的节点获得锁, 针对每一个节点, 还是按照上面的单节点模式获取锁.

假设在具有5个独立节点的集群上获取锁, 则需要经过如下的步骤

  1. 获得当前时间T0
  2. 依次尝试从5个实例, 使用相同的key和具有唯一性的value(例如UUID)获取锁.
  3. 如果从3个节点上获取了锁, 且当前时间小于失效时间, 则锁获取成功
  4. 锁的实际使用时间等于失效时间减去获取锁的时间

注意:假定锁的超时时间是10秒, 那么从每个节点请求时指定的超时时间可能是50毫秒, 以避免无限的等待节点响应时间. 如果获取锁失败, 则需要在所有的实例上取消获得锁.

Redis持久化

Redis有两种持久化方法, 分别是RDB(Redis Database)和AOF(Append Only File). 其中RDB可以视为Redis服务器在某个时刻的快照, 其中完整的存储类这个时刻的全部信息. 而AOF采用记录所有的写指令的方式保存数据, 依次执行这些写指令即可恢复数据.

RDB文件的写入利用类fork函数的特性. 当Redis需要创建RDB文件时, 直接fork一个当前进程, 由fork出来的子进程进行相关的写入操作, 而父进程可以继续原本的操作. 由于父子进程拥有独立的内存空间, 因此后续的父进程的修改操作也不会体现在子进程中, 从而保证两个操作互不干扰.

操作系统在创建子进程的时候并没有复制父进程的内存, 而是通过类似写时复制的方式, 在父进程修改数据时复制一个新的内存页

AOF文件就是简单的顺序写入指令, 由于每次写入的数据量小, 因此可以以一个非常高的频率写入(例如每秒写入一次), 因此如果出现服务器故障, AOF文件丢失的数据也相对较少.

为了防止AOF文件过大, Redis还提供量AOF文件重写功能, 可以定期的将AOF文件进行处理, 去除已经无效的指令. AOF的重写过程是安全的, Reids会先创建另外一个文件来保存重写结果, 等重写完毕后再替换原本的AOF文件.

由于指令分析的复杂性, AOF文件重写功能显然不是通过分析现有的AOF文件实现的,而是通过直接分析当前的内存数据,通过尽可能短的指令存储数据实现的。

Redis单线程模型

Redis采用Reactor模式来设计事件处理模型. 整个处理流程是单线程的, 从而使得代码实现上保持简单的同时也避免来上下文切换带来的成本. 因为Redis是内存操作, 所以性能瓶颈主要来自内存和网络, 多线程对性能的提升不大.

Redis采用IO多路复用技术实现单线程同时处理大量网络连接. IO复用可以使Redis同时监听多个Socket, 并在某个Socket准备好时发送收到通知.

高版本的Redis对大键值对的删除操作, 网络请求的读写操作采取了多线程优化, 但核心部分还是单线程模型.

过期数据删除策略

惰性删除 :只会在取出 key 的时候才对数据进行过期检查
定期删除 :每隔一段时间抽取一批 key 执行删除过期 key 操作

惰性删除策略对CPU消耗较少, 但可能会导致大量过期数据没有被删除. 定期删除策略能够保证过期数据最终一定会被删除, 但需要消耗更多CPU.

Redis采用两种方式结合的策略

内存淘汰机制

Redis 提供多种种数据淘汰策略, 假设A表示全体数据, E表示设定了过期时间的数据, 有如下的几类数据

算法 对象 含义
LRU A or E 淘汰最近最少使用的数据
LFU A or E 淘汰最不经常使用的数据
random A or E 随机选择数据移除
TTL E 淘汰将要过期的数据
NO N/A 不淘汰数据, 直接报错

LRU统计数据中最久未被使用的数据, LFU统计一个时段内使用频率最低的数据

缓存穿透和缓存雪崩

缓存穿透:查询了一个不存在的key, 导致需要进行数据库查询
缓存雪崩:大量key同时过期, 导致大量请求直接访问数据库
缓存击穿:某个key过期后, 大量请求同时访问数据库尝试加载数据

针对缓存穿透, 可以使用布隆过滤器进行过滤, 或者无论数据是否存在, 查询一次后都进行短时间的缓存. 针对缓存雪崩, 可以在设置过期时间时引入1~5分钟的随机值, 使得过期时间均匀分布.

针对缓存击穿, 可以采取互斥锁. 即尝试从数据库读取数据更新缓存前先尝试获得锁, 获得锁的线程执行更新操作, 其余线程直接重试从缓存获取数据操作. 这可以保证只有一个线程查询数据库.

缓存读写策略

旁路缓存模式

写入操作: 先写DB, 再删除缓存
读取操作: 先读缓存, 缓存未命中再读DB并设置缓存

注意:必须先写DB再删缓存, 否则其他线程可能读取了DB的旧值并设置缓存. 由于设置缓存分为两步, 所以还是有概率出现不一致的情况. 例如A服务发现缓存不存在读取了DB, 再写入缓存之间B服务器写入数据库, 之后A再执行写入缓存操作. 此时A写入的缓存数据与数据库中实际数据不一致. 但由于写入缓存速度明显快于数据库写入速度, 因此这一情况出现的概率非常小.

读写穿透

此模式将cache视为主要数据存储, 从中读写数据. cache服务负责将数据同步到DB. 这一模式一般比较少见, 因为Redis不支持这一功能.

写入操作:先查询缓存, 缓存中不存在则写入DB, 否则直接写入缓存, 由缓存同步到DB
读取操作:先查询缓存, 缓存命中则直接返回, 否则读取DB并设置缓存

异步缓存写入

与Read/Write Through Pattern很相似, 只是写入操作改为异步操作. 这一设计可能对数据的一致性带来更大的挑战, 但如果对一致性不太关心, 则此模式具有最佳的性能.

Redis集群

Redis有三种集群模式, 分别是主从复制模式, Sentinel模式和Cluster模式.

主从复制模式

Redis通过持久化已经实现了服务器重启也几乎不会丢失数据, 但如果服务器出现故障, 会导致Redis服务不可用. 主从复制模式解决了如下的问题

  1. 通过读写分离降低主节点的压力
  2. 实现多机备份, 提高数据安全性

  1. 从服务器发送SNYC指令, 表示希望获取主服务器的全量数据
  2. 主服务器生成全量备份, 并发送到从服务器, 从服务器解析备份数据
  3. 主服务器发送生成备份到从服务器解析完成位置积累的新指令
  4. 此后主服务器同步发送接收的新的写指令

优缺点

可以使用读写分离结构降低主服务器压力. 从服务器可以接下级的从服务器, 减少主服务器同步压力. 但主从结构不具备自动切换能力, 而且如果主服务器故障可能导致一定量的数据丢失.

Sentinel模式

使用独立的哨兵服务器来监控主服务器的状态, 为了保证哨兵的高可用, 也可以设置多个哨兵. 当哨兵监测到主服务器故障, 可以自动切换服务器, 并通知其他从服务器切换配置.


由于网络是不稳定的, 因此如果一个哨兵发现主服务器下线并不能立即认为主服务器出现故障, 此时哨兵将主服务器标记为主观下线. 当一定量的其他哨兵也认为主服务器下线后, 哨兵之间通过投票决定确定主服务器是否下线. 此时主服务器下线被成为客观下线.


哨兵通过PING指令确定服务器状态, 通过INFO指令通知服务器信息. 哨兵模式与主从模式结构一致, 但是具备自动切换功能, 因此具有更好的可用性.

Cluster模式

在Cluster模式下, 每个Redis服务器可以存储不同的数据, 从而实现真正的分布式存储. Redis采用哈希槽来决定数据的存储位置.

为了提高可用性, Cluster中的每个节点也可以是主从复制结构的, 从而在某个节点故障时能够自动切换从服务器.

最后更新: 2024年04月18日 13:26

版权声明:本文为原创文章,转载请注明出处

原始链接: https://lizec.top/2021/07/06/Redis%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/