Redis知识点

本文最后更新于 2025年11月19日 晚上

数据类型

PS:除上述五个基础数据类型以外,redis还有支持三个特殊的类型

HyperLogLogs(基数统计):

举个例子,A = {1, 2, 3, 4, 5}, B = {3, 5, 6, 7, 9};那么基数(不重复的元素)= 1, 2, 4, 6, 7, 9;这个结构可以非常省内存的去统计各种计数,比如注册 IP 数、每日访问 IP 数、页面实时UV、在线用户数,共同好友数等。

Bitmap (位存储):

即位图数据结构,都是操作二进制位来进行记录,只有0 和 1 两个状态。统计用户信息,活跃,不活跃! 登录,未登录! 打卡,不打卡! 两个状态的,都可以使用 Bitmaps!如果存储一年的打卡状态需要多少内存呢? 365 天 = 365 bit 1字节 = 8bit 46 个字节左右!

geospatial (地理位置):

Redis 3.2 版本就推出了! 这个功能可以推算地理位置的信息: 两地之间的距离, 方圆几里的人。

🧩 一、Redis 基本数据类型(5种)

1️⃣ String(字符串)

📘 含义:
最基本的类型,可以存储 字符串、整数、浮点数。
底层实现是 SDS(Simple Dynamic String)。

📖 常用命令:

1
2
3
4
5
6
SET name "xiaobai"
GET name
INCR age # 自增整数
DECR age # 自减整数
APPEND key value # 拼接字符串
MGET key1 key2 # 批量取值

🧠 存储示意:

1
2
name → "xiaobai"
age → "18"

💡 使用场景:

缓存对象的序列化数据(如 JSON)

分布式锁(SETNX)

计数器(INCR)

热点数据缓存(商品详情、token等)

👨‍💻 开发实践:
在 Spring Boot 中:

1
2
stringRedisTemplate.opsForValue().set("user:1001", "xiaobai");
String value = stringRedisTemplate.opsForValue().get("user:1001");

2️⃣ List(列表)

📘 含义:
一个 双向链表,每个节点是一个字符串。

📖 常用命令:

1
2
3
4
5
LPUSH queue task1
RPUSH queue task2
LPOP queue
RPOP queue
LRANGE queue 0 -1 # 获取列表所有元素

🧠 存储示意:

1
queue → ["task2", "task1"]

💡 使用场景:

消息队列(FIFO)

任务列表、评论列表等

时间线(Timeline)

👨‍💻 开发实践:

1
2
redisTemplate.opsForList().leftPush("queue", "task1");
String task = redisTemplate.opsForList().rightPop("queue");

3️⃣ Set(集合)

📘 含义:
无序、去重的字符串集合。

📖 常用命令:

1
2
3
4
5
6
7
SADD users "a" "b" "c"
SREM users "b"
SMEMBERS users
SISMEMBER users "a" # 判断存在
SINTER set1 set2 # 交集
SUNION set1 set2 # 并集
SDIFF set1 set2 # 差集

🧠 存储示意:

1
users → {"a", "c"}

💡 使用场景:

用户标签、去重统计(如活跃用户)

共同好友(交集)

推送系统去重

👨‍💻 开发实践:

1
2
redisTemplate.opsForSet().add("online_users", "uid:1001");
boolean exists = redisTemplate.opsForSet().isMember("online_users", "uid:1001");

4️⃣ Hash(哈希)

📘 含义:
类似于 Map / HashMap,用于存储对象的字段值对。
结构为:key → field → value

📖 常用命令:

1
2
3
4
5
HSET user:1001 name "xiaobai"
HSET user:1001 age 25
HGET user:1001 name
HGETALL user:1001
HDEL user:1001 age

🧠 存储示意:

1
2
3
4
user:1001 → {
"name": "xiaobai",
"age": "25"
}

💡 使用场景:

对象缓存(用户信息、配置)

存储结构化数据

统计类数据(点赞数、评论数)

👨‍💻 开发实践:

1
2
redisTemplate.opsForHash().put("user:1001", "name", "xiaobai");
Map<Object, Object> user = redisTemplate.opsForHash().entries("user:1001");

5️⃣ ZSet(有序集合)

📘 含义:
带权重(score)的有序集合,元素按分数排序。

📖 常用命令:

1
2
3
4
5
ZADD rank 100 "xiaobai"
ZADD rank 200 "laowang"
ZRANGE rank 0 -1 WITHSCORES
ZREVRANGE rank 0 10
ZINCRBY rank 10 "xiaobai"

🧠 存储示意:

1
2
3
4
rank → {
"xiaobai": 110,
"laowang": 200
}

💡 使用场景:

排行榜(游戏分数、热度)

延时任务(score = 时间戳)

优先级队列

👨‍💻 开发实践:

1
2
redisTemplate.opsForZSet().add("rank", "xiaobai", 100);
Set<ZSetOperations.TypedTuple<Object>> top = redisTemplate.opsForZSet().reverseRangeWithScores("rank", 0, 9);

🧮 二、三种特殊数据类型

1️⃣ HyperLogLog(基数统计)

用途: 统计去重后元素的数量(近似值)。
优点: 内存极省(12KB 可统计上亿数据)。

1
2
PFADD uv:2025 "user1" "user2" "user3"
PFCOUNT uv:2025

场景: 网站UV统计、独立访客数。

2️⃣ Bitmap(位图)

用途: 用二进制位表示状态(0/1)。

1
2
3
SETBIT sign:20251104 1001 1   # 用户1001签到
GETBIT sign:20251104 1001
BITCOUNT sign:20251104 # 当天签到人数

场景: 用户签到、活跃状态、布隆过滤器基础。

3️⃣ Geospatial(地理位置)

用途: 存储经纬度,并可计算两点距离、查找范围内位置。

1
2
3
GEOADD shop 120.1 30.2 "starbucks"
GEORADIUS shop 120.1 30.2 5 km
GEODIST shop "starbucks" "kfc" km

场景: 附近的人、附近的商家、地图服务。

RedisObject

redisObject 是 Redis 的“元对象”,负责描述「这是个什么类型、怎么存的、上次用过什么时候、被多少地方引用」,真正的数据存放在 ptr 指向的结构里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* Redis 对象
*/
typedef struct redisObject {

// 类型
unsigned type:4;

// 编码方式
unsigned encoding:4;

// LRU - 24位, 记录最末一次访问时间(相对于lru_clock); 或者 LFU(最少使用的数据:8位频率,16位访问时间)
unsigned lru:LRU_BITS; // LRU_BITS: 24

// 引用计数
int refcount;

// 指向底层数据结构实例
void *ptr;

} robj;

1️⃣ type —— 对象类型(4 位)

表示这个对象是什么类型的数据结构,对应我们平时用的 String / List / Set 等。

含义
0 STRING
1 LIST
2 SET
3 ZSET
4 HASH
5 MODULE(模块类型)
👉 Redis 用 4 位表示类型,最多支持 16 种。

2️⃣ encoding —— 编码方式(4 位)

同一种类型可以有不同的内部实现方式,比如:

类型 编码方式 说明
STRING int 纯数字,小整数直接保存
STRING embstr 小字符串(≤44字节)嵌入对象
STRING raw 普通 SDS 动态字符串
LIST quicklist 快速链表(双端压缩)
SET intset 小整数集合
SET hashtable 普通哈希表
HASH ziplist / listpack 小哈希对象(紧凑存储)
HASH hashtable 普通哈希表
ZSET ziplist / listpack 小型有序集合
ZSET skiplist 跳表实现的有序集合

🔸 举个例子:

1
2
3
4
5
6
7
8
9
10
11
# 当你执行:
SET key 123
# Redis 会存成:
type = STRING
encoding = int

# 如果你执行:
SET key "Hello World!"
# 则可能为:
type = STRING
encoding = embstr 或 raw

3️⃣ lru —— LRU / LFU 时间戳字段(24 位)

这个字段记录对象的「最近访问信息」,主要用于内存淘汰策略(LRU / LFU)。

💡 Redis 支持两种模式:

  • LRU 模式(Least Recently Used)
    lru 存放对象最后一次被访问的时间戳(相对 lru_clock)
    用于 volatile-lru、allkeys-lru 策略。

  • LFU 模式(Least Frequently Used)
    lru 被拆成:
    8 位:访问频率(freq)
    16 位:最后访问时间(ldt)
    用于 volatile-lfu、allkeys-lfu 策略。
    也就是说,这个字段既可能是一个时间戳,也可能是一个复合的访问计数。

4️⃣ refcount —— 引用计数

表示这个对象被引用的次数。

用途:

防止内存重复释放。

当对象被多个地方共享(比如小整数对象),通过引用计数管理内存生命周期。

常见情况:

refcount 值 含义
1 正常使用,仅被一个地方引用
2+ 被多个 key 共享(比如共享整数池)
0 引用清零,等待释放(free)

5️⃣ ptr —— 数据指针

指向实际的数据结构实例,比如:

如果是 STRING 类型,可能指向一个 SDS(简单动态字符串)

如果是 HASH 类型,可能指向 哈希表结构 dict

如果是 ZSET 类型,可能指向 跳表结构 zskiplist

也就是说:

robj.ptr 才是实际存储数据的“内核”。

字段 位宽 含义 示例
type 4 bit 数据类型(String、List等) STRING
encoding 4 bit 编码方式(int、embstr、ziplist等) embstr
lru 24 bit LRU 时间戳或 LFU 信息 123456
refcount int 引用计数 1
ptr void* 指向底层数据结构 SDS、dict、zskiplist等

持久化

RDB(记录数据)

RDB全称RedisDatabaseBackupfile(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据

AOF(记录操作命令)

AOF全称为AppendOnly File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。

从持久化中恢复数据

  1. redis重启时判断是否开启aof,如果开启了aof,那么就优先加载aof文件;
  2. 如果aof存在,那么就去加载aof文件,加载成功的话redis重启成功,如果aof文件加载失败,那么会打印日志表示启动失败,此时可以去修复aof文件后重新启动;
  3. 若aof文件不存在,那么redis就会转而去加载rdb文件,如果rdb文件不存在,redis直接启动成功;
  4. 如果rdb文件存在就会去加载rdb文件恢复数据,如加载失败则打印日志提示启动失败,如加载成功,那么redis重启成功,且使用rdb文件恢复数据;

过期策略

惰性删除

惰性删除:设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key

优点:对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查
缺点:对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放

定期删除

定期删除:每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)。

定期清理有两种模式:
●SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf的hz选项来调整这个次数
●FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms

优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响。另外定期删除,也能有效释放过期键占用的内存。
缺点:难以确定删除操作执行的时长和频率。

Redis的过期删除策略:情惰性删除+定期删除两种策略进行配合使用

数据淘汰策略

数据的淘汰策略:当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。

集群相关

主从复制(1 主+N从)

在Redis 2.8之前只支持全量复制(比如第一次同步时),在2.8之后新增支持增量复制(只会把主从库网络断连期间主库收到的命令,同步给从库)。

全量复制

  • 第一阶段是主从库间建立连接、协商同步的过程,主要是为全量复制做准备。在这一步,从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了。
  • 第二阶段,主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。
  • 第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。

增量复制

  • repl_backlog_buffer:它是为了从库断开之后,如何找到主从差异数据而设计的环形缓冲区,从而避免全量复制带来的性能开销。如果从库断开时间太久,repl_backlog_buffer环形缓冲区被主库的写命令覆盖了,那么从库连上主库后只能乖乖地进行一次全量复制,所以repl_backlog_buffer配置尽量大一些,可以降低主从断开后全量复制的概率。
  • replication buffer:每个client连上Redis后会分配一个client buffer,所有数据交互都是通过这个buffer进行的:Redis先把数据写到这个buffer中,然后再把buffer中的数据发到client socket中再通过网络发送出去,这样就完成了数据交互。所以主从在增量同步时,从库作为一个client,也会分配一个buffer,只不过这个buffer专门用来传播用户的写命令到从库,保证主从数据一致,我们通常把它叫做replication buffer。

哨兵机制(1 主+N从+N哨兵)

分片集群cluster(N主+N从)

hash tag

可以通过hash tag指定数据存在特定的节点中

Redis 规定了一个特殊规则:如果 key 含有 {},则 只对大括号中的内容 计算 CRC16。

举例:

key 参与哈希部分 计算结果(slot)
user:{1001}:cart 1001 slot = CRC16(“1001”) % 16384
user:{1001}:profile 1001 slot = CRC16(“1001”) % 16384
user:{2002}:cart 2002 slot = CRC16(“2002”) % 16384

🔹 所以 {} 实际是“告诉 Redis:只对这部分算哈希”。
🔹 这样两个 key {1001}{2002} 分别落到不同槽上,可能不同节点

Hash Tag 的目的不是让你“自由选择节点”,而是为了解决 Redis Cluster 的限制:

Redis Cluster 不允许跨 slot 执行多 key 操作(如 MGET、MSET、Lua)。

有了 {} 之后:
MGET user:{1001}:profile user:{1001}:cart ✅ 可行(同slot)
MGET user:{1001}:profile user:{2002}:profile ❌ 不行(跨slot)

缓存

缓存穿透

缓存穿透:查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库

解决方案一:缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存

解决方案二:布隆过滤器

缓存击穿

缓存击穿:给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮

解决方案一:互斥锁(强一致性、性能差)

解决方案二:逻辑过期(高可用、性能优)

缓存雪崩

缓存雪崩:在同一时段大量的缓存key同时失效或者Redis服务岩机,导致大量请求到达数据库,带来巨大压力

解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性 哨兵模式、集群模式
  • 给缓存业务添加降级限流策略 nginx或springcloud gateway 降级可做为系统的保底策略,适用于穿透、击穿、雪崩
  • 给业务添加多级缓存 Guava或Caffeine

双写一致性

双写一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致

读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定超时时间
写操作:延迟双删 ,先删缓存,更新数据库后再延时删除一次缓存,延时是因为数据库一般是主从结构,延时时间不确定,还是有脏数据风险

一致性要求高:共享锁+排他锁

允许最终一致性:

  1. 异步通知mq:数据库更新完成后,发送消息通知缓存删除或更新
  2. 基于 Binlog 的缓存同步(canal):
    1️⃣ MySQL 写入事务提交;
    2️⃣ Binlog 记录被 Canal 捕获;
    3️⃣ Canal 推送更新事件;
    4️⃣ Redis 缓存同步更新或删除;

常见问题

Redis为什么快

Redis之所以能够实现高性能,主要有以下几个方面的原因:

  1. 内存数据结构设计:Redis 将数据存储在内存中,摆脱了磁盘 I/O 的瓶颈,大大提高了读写速度。 Redis 提供了多种数据结构,如字符串、哈希表、列表、集合等,针对不同场景进行优化设计,能够高效地支持各种数据操作。
  2. 单线程模型: Redis 使用单线程模型处理客户端请求,避免了线程切换和竞争带来的性能开销。 通过高效的事件循环,Redis 能够快速地响应大量并发请求。
  3. 高效的网络 I/O 模型:Redis 使用非阻塞的 I/O 模型,配合 epoll/kqueue 等高性能的事件处理机制,能够高效地处理网络 I/O。 Redis 采用了 Redis 协议(RESP)作为通信协议,相比 HTTP 等通用协议,RESP 更加简单高效。
  4. 内存管理优化:Redis 采用自定义的内存分配器,避免了标准 malloc 分配器的性能损耗。 通过对内存进行预分配和复用,Redis 能够减少内存申请和释放的开销。
  5. 异步操作:Redis 将一些耗时的操作如 BGSAVE、BGREWRITEAOF 等异步化处理,避免阻塞主线程,提高了响应速度。
  6. 持久化优化:Redis 的 RDB 和 AOF 两种持久化机制都进行了优化,尽量减少持久化过程对性能的影响。

大key如何删除

在Redis中,删除大key(如大型哈希表、列表、集合或有序集合)时,直接使用DEL命令会导致Redis阻塞,影响性能。

解决方案

  • 使用UNLINK命令
    • UNLINK命令从Redis 4.0开始引入,它的工作原理是异步删除key。UNLINK命令会立即将key从数据库中删除,但实际的内存释放工作会在后台线程中进行,不会阻塞主线程。
    • 示例:
1
UNLINK my_large_key
  • 分批删除
    • 使用SCANHSCANSSCANZSCAN等命令分批删除大key中的元素,减少每次操作的负载。
    • 示例:
1
SCAN 0 MATCH my_large_hash:* COUNT 100
    这个命令会返回100个匹配的key,然后你可以逐个删除这些key。
  • 选择在业务低峰期执行删除操作
    • 在业务低峰期执行删除操作,可以减少对正常业务的影响。
    • 例如,可以选择在夜间或周末进行大key的删除操作。
  • 使用RENAME命令
    • 先将大key重命名,使其不再被业务访问,然后再逐步删除。
    • 示例:
1
2
RENAME my_large_key my_large_key_to_delete
DEL my_large_key_to_delete

分布式锁

setnx

1
2
3
4
5
6
7
8
SETNX lock_key unique_value
EXPIRE lock_key 10

# 或推荐使用 Redis 2.6.12+ 的原子命令:
# `NX`:不存在才设置(防止覆盖)
# `EX 10`:设置过期时间(防止死锁)
# `unique_value`:用随机值标识锁归属(防止误删)
SET lock_key unique_value NX EX 10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 加锁示例
String lockKey = "lock:order";
String uuid = UUID.randomUUID().toString();
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 执行业务逻辑
} finally {
// 解锁前检查归属
String val = redisTemplate.opsForValue().get(lockKey);
if (uuid.equals(val)) {
redisTemplate.delete(lockKey);
}
}
}

问题 描述
❌ 非原子操作 早期 SETNX + EXPIRE 是两条命令,可能中途宕机导致死锁
❌ 没有自动续期 如果业务执行超过 10s,锁过期被别人拿走。
❌ 没有可重入性 同一线程内重入锁会阻塞自己。
❌ 没有公平性控制 谁抢到算谁的,无法控制顺序。
❌ 不支持 RedLock 多节点冗余 单实例 Redis 挂掉锁失效。
👉 所以,SETNX 方案 能用但不安全,适合简单场景(如单机测试、非核心任务)。

redisson

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 加锁示例
RLock lock = redisson.getLock("lock:order");
try {
// 等待最多 3 秒,自动续期 30 秒(看门狗机制)
// - 默认自动续期时间:`lockWatchdogTimeout = 30s`
// - 当锁未手动释放且业务仍在执行时,Redisson 会每隔 10s 自动刷新 TTL;
// - 避免业务执行超时导致锁提前过期。
boolean success = lock.tryLock(3, 30, TimeUnit.SECONDS);
if (success) {
// 执行业务逻辑
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
特性 说明
✅ 原子性 使用 Lua 脚本确保加锁/释放锁原子操作
✅ 自动续期(看门狗机制) 业务未结束前自动延长锁租期(默认 30s)
✅ 可重入 同一线程可多次获得同一锁
✅ 公平锁 / 读写锁 支持多种锁类型(getFairLock(), getReadWriteLock()
✅ 分布式多节点支持 可用 RedLock 算法跨多个 Redis 实例冗余加锁
✅ 超时等待机制 支持 tryLock(waitTime, leaseTime) 模式
✅ 可靠解锁 只删除当前线程持有的锁,防止误删

红锁

普通分布式锁,锁的可靠性依赖于单个 Redis 实例
如果这个 Redis 实例宕机或主从切换(未同步 key),
锁数据可能丢失,从而出现:

  • A 持锁的节点宕机;
  • Redis 主节点切换后锁丢失;
  • B 再次获取锁成功;
  • 两个客户端同时操作共享资源 ❌(锁失效)。
    注意:Redis Cluster 模式下不能、也不需要使用 RedLock(红锁)算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://192.168.1.11:6379");
RedissonClient redisson1 = Redisson.create(config1);

Config config2 = new Config();
config2.useSingleServer().setAddress("redis://192.168.1.12:6379");
RedissonClient redisson2 = Redisson.create(config2);

Config config3 = new Config();
config3.useSingleServer().setAddress("redis://192.168.1.13:6379");
RedissonClient redisson3 = Redisson.create(config3);

// 获取每个节点的锁(相同 key)
RLock lock1 = redisson1.getLock("order_lock");
RLock lock2 = redisson2.getLock("order_lock");
RLock lock3 = redisson3.getLock("order_lock");

// 组装红锁
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

try {
// 等待最多 500ms,加锁成功后 10s 自动释放
boolean locked = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
if (locked) {
System.out.println("加锁成功!");
// 执行业务逻辑
} else {
System.out.println("加锁失败!");
}
} finally {
redLock.unlock();
}

两者对比

对比项 SETNX 手写锁 Redisson
原子性 ✅(仅 SET NX EX) ✅ Lua 脚本保证
自动续期 ✅ 看门狗机制
可重入 ✅ 支持
锁类型 仅独占锁 可重入、公平、读写锁
超时等待 手动轮询 内置 tryLock(waitTime, leaseTime)
解锁安全 需自行校验 UUID 自动校验持有线程
多节点冗余(RedLock) ✅ 支持
实现复杂度 ⭐ 简单 ⭐⭐⭐ 稳定生产可用
适用场景 简单任务、单实例 分布式系统、并发高业务

Redis知识点
http://blog.baicat.eu.org/2025/11/19/Redis知识点/
作者
liuxiaobai5201314
发布于
2025年11月19日
更新于
2025年11月19日
许可协议