Redis是一个纯C语言实现的开源项目, 且项目中已经自带了所有的依赖, 因此在Linux平台可以非常简单的从源码编译得到Redis可执行文件. 配合Vscode可以轻松的阅读Redis源码. 在Mac平台可直接使用brew安装Redis服务端和客户端. 不建议在Windows平台查看和编译源码, 由于环境变量与系统的实现不同, 很难完成项目编译.

功能速览

Redis 支持多种数据结构,每种都设计用于特定场景,以高性能地解决不同问题。下面我用一个表格汇总它们的主要操作和典型使用场景,希望能帮助你更好地理解和选用。

数据结构 支持的操作 适用场景
String(字符串) 设置、读取、删除键值对;批量设置或获取多个键值;数字的自增/自减;向字符串尾部追加内容;获取字符串长度;进行任意长度的位级别操作(设置、获取、统计位值)。 缓存会话信息、页面内容;计数器(如浏览量、点赞数);分布式锁;位图(如用户签到)。
Hash(哈希表) 设置、读取、删除单个或多个字段(Field)的值;批量获取多个字段的值;判断字段是否存在;获取所有字段或所有值;对数字类型的字段值进行自增/自减。 存储对象信息(如用户信息、商品详情,可单独操作字段);聚合统计(如用户标签)。
List(列表) 向列表头部或尾部插入元素;从列表头部或尾部弹出元素;获取指定范围内的元素列表;获取列表长度;修剪列表只保留指定范围内的元素;阻塞式地从列表头部或尾部弹出元素。 消息队列;最新消息排行(如朋友圈时间线);记录日志。
Set(集合) 添加、删除元素;判断元素是否存在;获取所有元素;获取集合元素数量;随机获取元素;求多个集合的交集、并集、差集,并将结果存储。 标签系统;好友关系(如共同好友);抽奖系统(随机获取);去重(如点赞用户ID)。
Sorted Set(有序集合) 添加(带分数)、删除元素;按分数或排名范围获取元素(正序或逆序);获取元素分数;获取集合大小;统计指定分数范围内的元素数量;按排名或分数范围删除元素。 排行榜(如游戏积分榜、热搜榜);带权重的消息队列;按时间范围检索(如时间序列数据)。
Bitmap(位图) 进行位级别的设置、读取和统计操作。 用户签到、活跃状态统计等大量布尔值场景。
HyperLogLog 添加元素进行基数统计;估算基数;合并多个 HyperLogLog。 UV(独立访客)统计、大规模去重估算(有极小误差)。
Stream(流) 添加消息;读取消息;以消费者组形式读取消息;确认消息已被处理;查看消息范围。 消息队列(支持多消费者组和消息持久化)、事件溯源、日志流处理。

新版本的Redis支持如下几种种新的数据结构

GEO操作: 经纬度信息相关的操作, Geohash转换, 距离计算等.

HyperLogLog操作: 基于概率的基数统计, 可参考HyperLogLog 简单介绍

数据流: 新增或删除消息, 消费者组操作,


最后, 对于任意一个键, 均支持如下的键操作

键操作: 查看键属性, 类型或过期时间. 修改过期时间, 判断键存在, 重命名键, 同步或异步删除键

可通过官方文档快速查阅指令的详细信息.

执行Lua脚本

由于Redis的每一种命令都比较简单, 因此为了支持复杂逻辑的实现, Redis支持执行Lua脚本. Lua是一种较为简单的脚本语言, 可浏览Learn Lua in Y Minutes快速学习或复习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的管道(Pipeline)是一种用于提高命令批量执行效率的机制. 它允许客户端将多个命令一次性发送到服务器, 并在一次网络通信中接收多个命令的响应. 执行一次Redis命令的网络往返时间称为RTT, 如果一条一条的发送和执行指令, 则每条指令均需要1个RTT. 由于Redis执行指令的时间消耗远小于网络传输时间的消耗, 因此逐条执行大量指令时, 大部分时间都消耗在网络传输和等待上.

采用管道模式可以节约大量网络传输和等待时间, 减少了网络通信的开销, 提高了命令执行的效率. 此外将多条指令一同发送还可以避免某些指令由于网络丢包导致执行不符合预期.

例如设置Key的指令正常执行, 但设置过期时间的指令丢失, 导致Key未设置过期时间. 使用管道可以一定程度保证两条指令的原子性.


各种语言的Redis客户端均会提供Pipeline机制, 使用时注意评估如下的事项

  1. Pipeline是否存在缓存池, 如果存在需要妥善的设置参数, 否则容易出现容量不足导致失败的问题.
  2. Pipeline中执行的指令消耗的时间是否合适, 指令过多导致执行时间过长可能影响其他业务执行.

事务

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

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

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

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


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

分布式锁

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

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

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

过期数据删除策略

对于过期数据, Redis采用以下两种方式结合的策略

惰性删除:在取出key的时候对数据进行过期检查, 发现过期则删除key并返回空
定期删除:每隔一段时间随机抽取一批key, 检查过期时间并执行已经过期得key

惰性删除策略对CPU消耗较少, 但可能会导致大量过期数据没有被删除. 定期删除策略能够保证过期数据最终一定会被删除, 但需要消耗更多CPU. 因此实际的实现是两种方式均采用.

内存淘汰机制

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

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

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

缓存穿透和缓存雪崩

缓存穿透:查询了一个不存在的key, 导致需要进行数据库查询. 此问题可以使用布隆过滤器进行过滤, 或者无论数据是否存在, 查询一次后都进行短时间的缓存.

缓存雪崩:大量key同时过期, 导致大量请求直接访问数据库. 此问题可以在设置过期时间时引入1~5分钟的随机值, 使得过期时间均匀分布.

缓存击穿:某个key过期后, 大量请求同时访问数据库尝试加载数据. 此问题可以采取互斥锁. 即尝试从数据库读取数据更新缓存前先尝试获得锁, 获得锁的线程执行更新操作, 其余线程直接重试从缓存获取数据操作.

缓存读写策略

旁路缓存模式

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

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

读写穿透

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

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

异步缓存写入

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

Redis集群

Redis集群模式

Redis通过持久化已经实现了服务器重启也几乎不会丢失数据, 但如果服务器出现故障, 会导致Redis服务不可用. Redis通过引入集群的方式提高服务的可用性与性能. Redis有三种集群模式, 分别是主从复制模式, Sentinel模式和Cluster模式.

主从复制模式: 主从复制模式通过创建一个保持一致的备份实例来提高可用性, 当主节点故障时可切换到备节点. 此外通过将读请求分离到备节点还可以实现更高的读写性能. 主从复制模式默认不支持自动切换, 且主节点故障时可能存在一定量的数据丢失.

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

Cluster模式: Redis集群将数据分为16384个槽位, 每个节点负责其中一部分槽位. 每个节点存储不同的数据, 实现真正的分布式存储. 数据Key使用Hash函数确定存储位置. 为了提高可用性, 每个节点也可以使用主从复制模式.

一致性哈希与哈希槽

Redis采用Cluster模式时, 需要引入一致性哈希技术, 使得对于某一条数据, 能够确定唯一的节点. 最常见的实现方式是将所有节点编号组成一个环, 然后对Key进行哈希处理计算在环中的位置, 并按照顺序找到负责的节点.

由于加入和删除节点可能导致节点在环上的分布不均衡, 因此可以将每个节点分裂为多个虚拟节点, 将环划分的更细, 实现更好的均衡效果.

Redis云服务

在实际生产中, 通常采用购买Redis云服务的方式使用Redis, 从而获得专业的运维和备份恢复服务. 云服务采取Cluster模式+主从复制模式, 可自由选择分片的数量和副本的数量, 实现0.25GB至8TB的容量范围, 并实现自动主备切换, 自动备份和恢复等能力.

由于存储服务非常重要, 对运维有较高的要求, 因此无论是大厂还是小厂, 几乎都采取上云的方式了.

最后更新: 2026年04月16日 15:06

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

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