Redis 简介

Redis(Remote Dictionary Server)是一个开源的内存数据库,遵守 BSD 协议,它提供了一个高性能的键值(key-value)存储系统,常用于缓存、消息队列、会话存储等应用场景。

特点:

  • 性能极高:能够支持每秒数十万次的读写操作
  • 丰富的数据结构:Redis 不仅支持基本的键值存储,还提供了丰富的数据类型,包括字符串、列表、集合、哈希表、有序集合等。
  • 原子性操作:Redis 的所有操作都是原子性的,这意味着操作要么完全执行,要么完全不执行。
  • 持久化:Redis 支持数据的持久化,可以将内存中的数据保存到磁盘中,以便在系统重启后恢复数据
  • 支持发布/订阅模式:Redis 内置了发布/订阅模式(Pub/Sub),允许客户端之间通过消息传递进行通信。
  • 主从复制:Redis 支持主从复制,可以通过从节点来备份数据或分担读请求,提高数据的可用性和系统的伸缩性。

使用方式

  • CLI:命令行
  • API:java 或 python
  • GUI:用户界面

安装

Windows

  1. wsl 来安装,使用 wsl 来安装 linux 系统,在通过 linux 系统的方式来安装 redis

    • powershell 执行wsl --install
    • 进入 unbuntu 终端
    • sudo apt update
    • sudo apt install redis-server -y
    • 启动 Redis sudo systemctl start redis-server
    • 检查状态 sudo systemctl status redis-server
    • 设置开机自启sudo systemctl enable redis-server
    • 客户端连接 redis-cli
  2. Docker,下载 redis 镜像

  3. github 文件地址下载

RedisInsight

是一个 redis 的图形化操作界面,从官网安装即可

Redis 常用命令

  • 设置一个键值对:SET key value
  • 获取一个键的值:GET key
  • 删除一个键:DEL key
  • 查询一个键是否存在:EXISTS key
  • 查看存在哪些键:KEYS *
  • 删除所有键:FLUSHALL
  • 查询键的过期时间:TTL key
  • 设置过期时间:EXPIRE key 过期时间
  • 设置带有过期时间的键值对:SETEX key 过期时间 value
  • 只有当键不存在才设置设置键的值:SETNX key value

数据结构

列表

类似于数组

  • 在列表头部添加元素:LPUSH list_name value
  • 在列表尾部添加元素:RPUSH list_name value
  • 查询区间列表元素:LRANGE list_name 左区间 右区间
  • 删除头部元素:LPOP list_name 元素个数(默认为一)
  • 删除尾部元素:RPOP list_name 元素个数
  • 查看元素个数:LLEN list_name
  • 删除指定区间以外的元素:LTRIM list_name 左区间 右区间

Set(无序集合)

不可以重复,支持集合运算

  • 添加一个元素:SADD key value
  • 删除一个元素:SREM key value
  • 查看集合元素:SMEMBERS key
  • 判断一个元素是否在集合中:SISMEMBER key value

SortedSet(有序集合)

每个元素会关联一个浮点数类型的分数,来对集合中的元素进行从小到大的排序

  • 添加一个元素:ZADD key score value1
  • 删除一个元素:ZREM key value
  • 查看集合元素:ZRANGE key 左区间 右区间 withscores(添加此属性会将分数值一同输出)
  • 查看某个元素的分数:ZSCORE key value
  • 查看某个元素的排名:ZRANK key value
  • 反转一个集合后查看排名:ZREVERANK key value

hash(哈希)

字符类型的字段和值的一个映射表,一个键值对的集合

  • 添加一个键值对:HSET key field value
  • 获取一个字段的值:HGET key field
  • 获取映射表的所有键值对:HGETALL key
  • 删除某个键值对:HDEL key field
  • 判断某个键是否存在:HEXISTS key field

发布订阅

  • 将消息发送一个指定的频道:publish channel
  • 订阅一个频道:subscribe channel

示例:

1
2
3
4
// 打开一个终端,订阅一个频道
subscribe geekhour
// 打开另一个终端,发布消息
publish geekhour message

存在的问题:消息无法持久化,无法记录仪历史消息

Stream(消息队列)

  • 添加一个消息:XDD key (自定义 id 格式:时间搓-序列号) field value(表示自动生成一个消息 id)
  • 查看消息的数量:XLEN key
  • 查看消息的详细内容:XRANGE key - +
  • 删除消息:XDEL key id
  • 保留某条消息:XTRIM key MAXLEN 0(作用是删除所有消息)
  • 读取一个消息:XREAD count 个数(表示一次读取几个消息)block 时间(表示如果没有消息阻塞多长时间)streams key(就是消息队列的名称)0(表示从哪个位置开始读取)
  • 创建消费者组:XGROUP create key 组名 id
  • 查看消费者组信息:XINFO groups key
  • 添加组的消费者:XGROUP createconsumer key 组名 消费者名

Geospatial(地理空间)

提供了一种存储地理信息的一种数据结构

  • 添加地理位置信息:GEOADD city 经度 维度 城市名字
  • 获取地理信息的经纬度:GEOPOS city 城市名字
  • 返回两个地理位置的距离:GENDIST city 城市名字 1 城市名字 2 距离单位
  • 搜索一个地理位置的附近地理信息:GEOSEARCH city FROMMEMBER 城市名字 BYRADIUS 半径 半径单位

HyperLogLog(基数统计算法)

求出集合中不计算重复元素的个数

  • 添加一个元素:PFADD key value1 value2 value3
  • 查看计数的个数:PFCOUNT key

位图(Bitmap)

bit 数组:数据的下标就是偏移量(offset),值只有 0 和 1 的一个数组,支持位运算
可以将偏移量表示成一个个成员,而 0 和 1 看成两个状态,那么这样我们就可以统计像点赞收藏等等,只有两个状态的抽象了

  • 设置某个偏移量的值:SETBIT key offset value
  • 获取某个偏移量的值:GETBIT key offset
  • 使用 set 来设置:SET key “十六进制数表示”
  • 统计某个 key 中含有 1 的个数:BITCOUNT key
  • 第一个出现 0 或 1 的位置:BITPOS key 0|1

位域(Bitfield)

将很多小的整数存储到一个较大的位图中。

  • 设置值:BITFIELD key:id set u8(数的类型) #0(表示第一个位置) value
  • 获取值:BITFIELD key:id get u8 #0
  • 修改值:BITFIELD key:id incrby u32 #1 newvalue

事务

在一个事务中,在 redis 的事务中,我们不能保证事务的原子性,即不能保证事务中的命令全部执行成功,在 redis 中,执行 EXEC 命令之前,所有的命令回放入一个队列缓存起来,不会立即执行,在收到 EXEC 命令执行成功后,开启执行缓存队列中的命令。

  • 开启一个事务:MUTI,开启后将事务放入一个队列中
  • EXEC:提交事务
  • DISCARD,取消事务,放弃执行事务块内的所有命令。
  • watch key:监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
  • unwatch:取消 WATCH 命令对所有 key 的监视。

乐观锁(Watch 命令)

作用:监控一个或多个 key,若事务执行前这些 key 被修改,则事务取消

1
2
3
4
WATCH balance  # 监控balance
MULTI
DECRBY balance 100
EXEC # 若balance未被修改,执行成功;否则返回nil

分布式锁

用 SET 命令的 NX(不存在才设置)和 PX(过期时间)特性

1
2
3
4
5
6
7
8
9
# 获取锁(lock_key为锁名,uuid为唯一标识,避免释放别人的锁)
SET lock_key uuid NX PX 30000 # 30秒过期,防止死锁

# 释放锁(必须用Lua脚本,保证原子性)
if redis.call("get", "lock_key") == "uuid" then
return redis.call("del", "lock_key")
else
return 0
end

持久化

redis 是将缓存存入到内存当中,当我们因为意外事故,而能会导致数据丢失,这是非常致命的,所以我们需要持久化。
方式一:RDB 方式,在指定时间内,将内存中的数据快照写入磁盘,是某一个时间点数据的完整副本,可以通过配置文件的 save 参数来进行配置;还可以通过 save 命令来手动触发快照;


适合来做备份,因为如果在保存前宕机,那么还是会丢失数据,在传输过程中,由于 redis 是单线程的,会使 redis 处于一个阻塞的状态所以这是不合适的。


redis 提供了一个 bgsave 的命令,会单独创建一个子进程来保存数据,但是在创建子进程时,redis 也是处于阻塞的状态。
方式二:AOF 持久化,在执行写命令时,不仅会将内容写到内存当中还会将内容写到一个文件当中,这个文件会以一个日志的形式来记录每一个写操作,当 redis 重启时,redis 会根据这个文件的内容重新来重建内存当中整个数据库的内容。


开启方式,在配置文件中将 appendonly 的值改为 yes。

主从复制

主从复制,是指将一台 redis 服务器的数据来复制到其他 redis 服务器,一个主节点可以有多个从节点,而一个从节点只能有一个主节点。数据的复制是单向的,只能有主节点到从节点,一般来说,主节点进行写操作,而从节点进行读操作,主节点会将自己的数据变化,通过异步的方式来发送到从节点。从节点接收到主节点的数据后,更新自己的数据,从而达到了数据的一致性。

配置:(配置从节点)

  1. 命令行方式:
    • replicaof host port
  2. 配置文件方式(一般都用这个):
    • 进入从节点配置文件cd /opt/homebrew/etc
    • 将 redis.conf 复制到根目录cp redis.conf ~
    • 将配置文件复制一个起名为 redis-port(port 为从节点的端口号).conf 作为从节点配置文件
    • 将端口号改为自己的从节点端口号
    • 将 dbfilename 这个配置项也加上端口号
    • 将 replicaof 这个配置项来指定主节点端口号

全量复制(首次复制):

复制过程:

  1. 从节点向主节点请求复制
  2. 主节点执行 bgsave 生成 RDB 文件,同时记录此期间的写命令复制到缓冲区
  3. 主节点发送 RDB 文件给从节点,从节点加载 RDB
  4. 主节点发送复制缓冲区的写命令,从节点执行(保证数据一致)。

增量复制(后续同步):

  1. 主节点每执行一个写命令,会记录到复制偏移量(主从节点各自维护,通过对比偏移量判断是否需要同步)。
  2. 从节点定期发送 REPLCONF ACK 告知主节点自己的偏移量,主节点若发现从节点落后,发送增量命令。

哨兵模式

当我们的主节点发生故障时,那么我们的主从配置就会故障,而我们还需要重新配置,这是一件麻烦且不合理的问题。
我们可以使用哨兵模式来自动监听我们的 redis 集群,哨兵会以一个独立的进程运行在 redis 集群中,来监听我们的 redis 集群,主要功能:

  1. 监控
  2. 通知,如果某个节点除了问题,哨兵会通过订阅模式来通知其它节点
  3. 自动故障转移,当主节点不能正常工作时,它就会开始自动转移工作,将一个从节点自动升级为主节点,然后将其它从节点指向新的主节点。

配置:

  1. 在 redis 集群中添加一个哨兵节点,使用redis -sentinel sentinel.conf命令来添加。需要创建一个 sentinel.conf 文件,配置`entinel monitor master 主节点 ip 地址 端口号 1(表示只需要一个哨兵节点同意就可以进行转移)
    在实际生产中,通常会使用 3 个哨兵节点来监听 redis 集群,因为每个哨兵也可能发生故障,当哨兵节点发生故障时,哨兵节点之间会通过选举的方式来选举出一个新的哨兵来监听 redis 集群。

过期键删除策略(为什么过期键不会立即消失?)

Redis 中设置过期时间的键(如 EXPIRE key 10)不会在到期时立即删除,而是通过三种策略协同处理:

  • 惰性删除:只有当用户主动访问该键时,才检查是否过期,过期则删除(优点:不浪费 CPU;缺点:可能占用大量内存)。
  • 定期删除:Redis 每隔一段时间(默认 100ms)随机抽查部分过期键,删除已过期的(平衡 CPU 和内存消耗)。
  • 内存淘汰触发:当内存达到 maxmemory 限制时,触发内存淘汰策略,可能删除过期键(见下文)。

内存淘汰策略(内存满了怎么办?)

当 Redis 内存达到配置的 maxmemory 阈值时,会根据 maxmemory-policy 配置删除数据,共 8 种策略:

针对过期键:

  • volatile-lru:删除过期键中最近最少使用的(优先保留热点过期数据)。
  • volatile-lfu:删除过期键中最不经常使用的。
  • volatile-ttl:删除过期键中剩余时间最短的。
  • volatile-random:随机删除过期键。

针对所有键:

  • allkeys-lru:删除所有键中最近最少使用的(适合缓存场景,不分是否过期)。
  • allkeys-lfu:删除所有键中最不经常使用的。
  • allkeys-random:随机删除所有键。

特殊策略:

  • noeviction:默认策略,不删除任何键,新写入会返回错误(适合不允许丢失数据的场景)。

Redis Cluster(分片集群,解决海量数据存储)

主从和哨兵只能解决高可用,无法解决 “单节点内存上限” 问题,Redis Cluster 通过分片实现水平扩展

核心原理:将数据分散到 16384 个哈希槽(slot) 中,每个节点负责部分槽(如 3 节点集群,每节点负责~ 5461 个槽)。
键的槽位计算:slot = CRC16(key) % 16384。

集群搭建步骤

  • 启动 3 个主节点(端口 6379、6380、6381)和 3 个从节点(端口 6382、6383、6384)。
  • 初始化集群:redis-cli —cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 —cluster-replicas 1(每个主节点配 1 个从节点)。
  • 检查集群状态:redis-cli -c -p 6379 cluster info(-c 表示集群模式连接)。
  • 故障转移:主节点故障后,其从节点自动升级为主节点,接管槽位(无需哨兵,Cluster 内置故障检测)。

缓存穿透(查询不存在的数据,穿透到 DB)

恶意请求查询不存在的 key(如 ID=-1 的用户),缓存未命中,直接访问数据库,导致 DB 压力骤增。
解决方案:

  1. 缓存空值:对不存在的 key,缓存一个空值(如 SET key “” EX 60),过期时间设短(1-5 分钟),避免长期占用内存。
  2. 布隆过滤器:将所有可能存在的 key 提前存入布隆过滤器,请求先过过滤器,不存在则直接返回(误判率极低,适合海量数据)

缓存击穿(热点 key 过期瞬间,大量请求穿透到 DB)

某热点 key(如秒杀商品)过期瞬间,大量请求同时命中缓存未命中,全部访问 DB,导致 DB 崩溃。
解决方案

  1. 互斥锁:缓存未命中时,用 Redis 的 SET lock_key 1 NX PX 5000 获取锁,只有获取锁的请求去 DB 查询并更新缓存,其他请求等待重试(避免并发查 DB)。
  2. 热点 key 永不过期:不设置过期时间,后台定时任务异步更新缓存(如每 10 分钟更新一次)。

缓存雪崩(大量 key 同时过期或缓存集群宕机)

大量 key 集中过期,或缓存集群故障,所有请求瞬间穿透到 DB,导致 DB 压垮。

解决方案:

  1. 过期时间加随机值:设置过期时间时加随机数(如 EXPIRE key 3600 + rand(0, 1800)),避免集中过期。
  2. 多级缓存:结合本地缓存(如 Caffeine)和分布式缓存(Redis),即使 Redis 宕机,本地缓存可临时抗住部分请求。
  3. 缓存高可用:Redis Cluster 部署(多主多从),避免单节点故障;哨兵模式自动故障转移。
  4. 服务熔断降级:用 Sentinel/Hystrix 等工具,当 DB 压力过大时,返回默认值

缓存与数据库一致性(如何保证缓存和 DB 数据同步?)

  1. Cache Aside Pattern(旁路缓存):
    读:先查缓存,未命中查 DB,再回写缓存。
    写:先更 DB,再删缓存(而非更新缓存,避免并发写导致的不一致)。
    👉 优点:简单可靠;缺点:可能存在短暂不一致
  2. Write Through(写透):写操作先更缓存,缓存再同步更 DB(适合缓存和 DB 必须强一致的场景,性能较低)
  3. Write Back(写回):写操作只更缓存,缓存异步批量更 DB(性能高,适合非核心数据,有数据丢失风险)。

性能优化

  1. 避免大 key(单 key 存储过大数据)
    大 key 会导致:读写耗时(网络传输慢、内存分配耗时),删除 / 过期时阻塞主线程。
    解决:
  • 拆分大 key(如将大 Hash 拆分为多个小 Hash,按 ID 范围分片)。
    (用一个 hash 函数将 key 转换为一个数字,比如使用 crc32 hash 函数。对 key foobar 执行 crc32(foobar)会输出类似 93024922 的整数。
    对这个整数取模,将其转化为 0-3 之间的数字,就可以将这个整数映射到 4 个 Redis 实例中的一个了。93024922 % 4 = 2,就是说 key foobar 应该被存到 R2 实例中。注意:取模操作是取除的余数,通常在多种编程语言中用%操作符实现。)
  • 用 SCAN 代替 KEYS *(KEYS 会阻塞主线程,SCAN 分批迭代)。
  1. 批量操作优化
    Pipeline(管道):将多个命令打包发送,减少网络往返次数
    批量删除:用 UNLINK 代替 DEL(UNLINK 异步删除,不阻塞主线程)