Redis 核心知识点归纳与详解

一、Redis 基础认识

1.1 定义与核心特性

Redis(Remote Dictionary Server,远程字典服务)是一款完全开源的高性能 Key-Value 数据库,官网地址:https://redis.io/。核心特性如下:

  • 数据结构丰富:支持字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(ZSet)、位图(Bitmap)、HyperLogLog、地理空间(Geo)、向量(Vector)等多种复杂数据类型,可满足缓存、数据库、向量搜索等多场景需求。
  • 内存存储 + 持久化:数据全量存储于内存,保证极高的读写性能;同时支持 RDB、AOF 两种持久化方式,将数据落盘到硬盘,保障数据安全性,因此 Redis 可同时作为缓存、数据库使用。
  • 高性能:基于内存操作 + 单线程核心模型 + IO 多路复用,单机 QPS 可达 10W+。

1.2 版本演进

版本 核心线程模型变化 关键特性
4.X 及以前 纯单线程(核心读写 + 后台操作均单线程) 基础的事务、Pipeline 支持
5.X 核心代码重构 为后续多线程铺路,优化底层架构
6.X 核心读写仍单线程,后台耗时操作多线程 引入多线程 IO、ACL 权限控制
7.X 核心读写单线程,更多后台操作异步化 增强 Lua 脚本、Redis Function、只读脚本支持

二、Redis 线程模型

2.1 整体线程模型(核心)

Redis 线程模型可总结为:客户端多线程,服务端核心单线程(版本差异化扩展)。

2.1.1 基础架构(Mermaid 流程图)
2.1.2 关键细节
  1. 客户端连接管理 :Redis 通过多线程维护与客户端的 Socket 连接(maxclients 配置最大连接数),避免单线程处理连接导致的瓶颈。
  2. 核心主线程 :
    • 负责处理所有网络 IO、键值对读写、指令解析执行等核心操作;
    • 基于 epoll 实现 IO 多路复用,可单线程同时响应多个客户端的 Socket 读写请求;
    • 所有核心指令串行执行,天然避免了并发读写的脏读、幻读、不可重复读等问题。

IO多路复用:简单来说就是一种让单个线程(Redis 主线程)同时监听多个网络套接字(Socket)的 IO 状态,并且仅在套接字有实际 IO 事件(如可读、可写)发生时,才对其进行相应处理的技术。

3. 后台辅助线程(Redis 5.X+):

  • 耗时操作使用多线程:RDB 生成、AOF 重写 / 刷盘、unlink 异步删除、集群数据同步、FLUSHALL/FLUSHDB 异步执行等,由独立线程处理,避免阻塞核心主线程;
  • 核心读写仍单线程:保证指令执行的原子性,避免多线程上下文切换开销(Redis 性能瓶颈不在 CPU,而在内存 / 网络)。

2.2 总结

Redis 是 "核心单线程 + 辅助多线程" 模型,核心读写单线程(保障原子性),后台耗时操作多线程(提升整体性能)。

三、Redis 指令原子性保障

Redis 核心读写单线程串行执行,天然具备指令级原子性,但复杂业务场景需额外手段保障 "一组指令" 的原子性,核心方案如下:

3.1 复合指令(原子性指令)

Redis 内置的复合指令本身是原子的,一条指令完成多个操作,无需额外控制:

指令 作用 原子性说明
MSET/HMSET 批量设置键值 / 哈希字段 一次性完成所有设置,中间不会被打断
GETSET 设置新值并返回旧值 原子性完成 "读 + 写"
SETNX 仅当键不存在时设置 分布式锁核心指令,原子性判断 + 设置
SETEX 设置值并指定过期时间 原子性完成 "设置 + 过期",避免先 SET 再 EXPIRE 的竞态
INCR/DECR 原子自增 / 自减 替代 "GET→计算→SET" 的非原子操作
复合指令的典型场景:
复制代码
// Java 中使用 SETNX 实现简单分布式锁
Jedis jedis = new Jedis("localhost", 6379);
String lockKey = "dist:lock:order";
// 原子性获取锁:不存在则设置,过期时间30s避免死锁
Boolean lockSuccess = jedis.setnx(lockKey, "locked");
if (lockSuccess) {
    jedis.expire(lockKey, 30); // 补充过期时间(Redis 2.6.12+ 可合并为 set lockKey locked NX EX 30)
    try {
        // 执行业务逻辑
    } finally {
        jedis.del(lockKey); // 释放锁
    }
}

3.2 Redis 事务

3.2.1 核心命令
命令 作用 注意事项
MULTI 开启事务 后续指令进入队列,返回 QUEUED
EXEC 执行事务 一次性执行队列中所有指令
DISCARD 放弃事务 清空队列,终止事务
WATCH key key... 监听键变化 事务执行前,若监听的键被修改,事务执行失败
UNWATCH 取消监听 仅对当前客户端有效,不影响其他客户端
3.2.2 事务特性
  • 错误认识:Redis 事务像数据库事务一样,支持回滚;
  • 正确结论:Redis 事务的核心是 "批量执行 + 串行化",而非 "原子回滚":
    1. 执行前失败(指令语法错误 / 参数错误):EXEC 时整个事务不执行;
    2. 执行中失败(如对 String 键执行 LPOP):错误指令返回异常,其他指令正常执行,不会回滚;
    3. WATCH 机制:监听键被修改时,EXEC 返回 nil,事务不执行(乐观锁思想)。
3.2.3 事务执行流程
3.2.4 事务失败处理
  1. EXEC 前失败:指令语法错误(如 LPOP 少参数),EXEC 时直接返回错误,所有指令不执行;
  2. EXEC 后失败:指令逻辑错误(如对 String 执行 LPOP),仅该指令返回错误,其他指令正常执行;
  3. 服务宕机 :EXEC 执行后,Redis 先将事务指令写入 AOF 文件,再执行操作;若宕机导致 AOF 记录不完整,重启时需用 redis-check-aof 工具修复(删除不完整事务记录)。

总结:redis中事务的作用不是像数据库一样保证整个操作的原子性(一起成功或一起失败),而只是保证这一系列的操作一起执行(错误时也不支持回滚),执行过程中不会被其他指令加塞。

3.3 Pipeline(管道)

3.3.1 核心原理

Pipeline 是优化网络 RTT(Round-Trip Time,往返时间)的机制:将多个指令打包,一次性发送到服务端,服务端批量执行后一次性返回结果,减少网络往返次数。

3.3.2 RTT 优化对比(Mermaid 对比图)
3.3.3 使用示例
复制代码
// Java 使用 Jedis 实现 Pipeline
Jedis jedis = new Jedis("localhost", 6379);
Pipeline pipeline = jedis.pipelined();
// 批量添加指令
pipeline.set("count", "1");
pipeline.incr("count");
pipeline.incr("count");
pipeline.incr("count");
// 执行并获取结果
List<Object> results = pipeline.syncAndReturnAll();
// 结果:[OK, 2, 3, 4]
System.out.println(results);
3.3.4 关键注意点
  • 错误认识:Pipeline 具备原子性;
  • 正确结论:Pipeline 仅优化网络传输,不保证原子性 ------ 服务端执行时,仍可能被其他客户端的指令 "插队";
  • 性能上限:单次 Pipeline 指令不宜过多(建议 1000 条以内),避免客户端阻塞 / 服务端内存占用过高;
  • 与事务的区别:Pipeline 仅批量发送,无 "队列化 + 监听";事务是 "队列化 + 原子执行",但无网络优化。

3.4 Lua 脚本(重点)

3.4.1 核心优势

Lua 是轻量级单线程脚本语言,Redis 中执行 Lua 脚本具备以下特性:

  • 原子性:脚本内所有指令作为一个整体执行,中间不会被其他指令打断;
  • 灵活性:支持自定义逻辑(条件判断、循环、变量等),弥补复合指令 / 事务的局限性;
  • 高性能:脚本在服务端执行,减少网络往返。
3.4.2 核心指令:EVAL/EVAL_RO
复制代码
EVAL script numkeys [key [key ...]] [arg [arg ...]]
  • script:Lua 脚本内容;
  • numkeys:键参数个数;
  • KEYS[]:脚本中访问的 Redis 键(必须通过 KEYS 数组,便于集群分片);
  • ARGV[]:脚本中使用的附加参数。
3.4.3 示例:原子调整库存
复制代码
// Java 调用 Lua 脚本
Jedis jedis = new Jedis("localhost", 6379);
String luaScript = "local initcount = redis.call('get', KEYS[1]) " +
        "local a = tonumber(initcount) " +
        "local b = tonumber(ARGV[1]) " +
        "if a >= b then " +
        "    redis.call('set', KEYS[1], a) " +
        "    return 1 " +
        "end " +
        "redis.call('set', KEYS[1], b) " +
        "return 0";
// 执行脚本:KEYS[1] = stock_1,ARGV[1] = 10
Object result = jedis.eval(luaScript, 1, "stock_1", "10");
System.out.println(result); // 输出 0(库存1<10,设置为10)
3.4.4 关键注意点
  1. 避免耗时操作 :Lua 脚本运行在核心线程,死循环 / 耗时运算会阻塞整个 Redis;Redis 配置 lua-time-limit(默认 5 秒),超时后返回 BUSY 错误;
  2. 只读脚本(Redis 7+) :使用 EVAL_RO 执行只读脚本,禁止修改数据,可通过 SCRIPT_KILL 终止,且可转移到从节点执行,减轻主节点压力;
  3. 脚本缓存 :频繁执行的脚本可通过 SCRIPT LOAD 缓存到服务端,返回 SHA1 摘要,后续用 EVALSHA 调用,减少脚本传输开销。

3.5 Redis Function(Redis 7+)

3.5.1 核心优势
  • 基于 Lua 扩展,支持将脚本封装为 "函数",提前加载到服务端,客户端直接调用;
  • 支持函数嵌套调用,代码复用性更高(Lua 脚本无法复用);
  • 支持权限控制、只读调用,更适合生产环境管理。
3.5.2 使用示例
  1. 编写 Lua 函数文件(mylib.lua):

    #!lua name=mylib -- 命名空间,必须第一行
    local function my_hset(keys, args)
    local hash = keys[1]
    local time = redis.call('TIME')[1] -- 获取当前时间戳
    -- HSET:设置哈希字段,last_modified 记录最后修改时间
    return redis.call('HSET', hash, 'last_modified', time, unpack(args))
    end
    -- 注册函数
    redis.register_function('my_hset', my_hset)

  2. 加载函数到 Redis:

    cat mylib.lua | redis-cli -a 密码 -x FUNCTION LOAD REPLACE

3.调用函数:

复制代码
127.0.0.1:6379> FCALL my_hset 1 myhash myfield "some value" another_field "another value"
(integer) 3 -- 成功设置3个字段
127.0.0.1:6379> HGETALL myhash
1) "_last_modified_"
2) "1717748001"
3) "myfield"
4) "some value"
5) "another_field"
6) "another value"
3.5.3 注意点
  • 集群环境:需在每个节点手动加载函数(Redis 不会自动同步);
  • 资源控制:函数缓存于服务端,应注意避免过多 / 过大函数占用内存;
  • 管理指令:FUNCTION LIST(查看函数)、FUNCTION DELETE(删除函数)等。

3.6 原子性方案对比

方案 原子性 灵活性 性能 适用场景
复合指令 原子 低(固定逻辑) 最高 简单批量操作(如 MSET、SETNX)
事务 弱原子(执行中不回滚) 中(仅指令队列) 中 简单批量指令,需监听键变化(WATCH)
Pipeline 无 中(仅批量发送) 高(优化 RTT) 非热点数据批量读写,无需原子性
Lua 脚本 强原子 高(自定义逻辑) 高 复杂业务逻辑(如库存扣减、分布式锁)
Redis Function 强原子 极高(函数复用) 高 Redis 7+ 环境,复杂可复用逻辑

四、BigKey 问题

4.1 定义

BigKey 指占用内存过大或元素过多的键,例如:

  • String 类型:值大于 100KB;
  • List/Set/ZSet 类型:元素数大于 1W;
  • Hash 类型:字段数大于 1W。

4.2 危害

  • 阻塞核心线程:读取 / 删除 BigKey 耗时过长,阻塞其他指令执行;
  • 网络阻塞:BigKey 传输占用大量带宽,导致网络延迟;
  • 持久化 / 同步慢:RDB/AOF 生成、主从同步时,BigKey 处理耗时久。

4.3 检测方法

复制代码
# 检测元素数量多的 BigKey
redis-cli --bigkeys
# 检测占用内存大的 BigKey
redis-cli --memkeys
# 带密码检测
redis-cli -a 密码 --bigkeys

4.4 解决方案

  1. 拆分 BigKey :
    • Hash 拆分:将大 Hash 按字段前缀拆分为多个小 Hash(如 user:info:1000 → user:info:1000:1、user:info:1000:2);
    • List/Set 拆分:按时间 / 业务维度拆分为多个小列表 / 集合;
  2. 异步删除 :使用 UNLINK 替代 DEL,异步删除 BigKey,避免阻塞;
  3. 限制写入:业务层控制单键的大小 / 元素数,避免生成 BigKey;
  4. 分批操作 :读取 BigKey 时,使用 SSCAN/HSCAN/ZSCAN 分批遍历,避免一次性读取全部数据。

五、Redis 线程模型总结

  1. 核心设计:"核心单线程 + 辅助多线程" 是 Redis 平衡性能与并发复杂度的最优解 ------ 核心读写单线程避免了并发问题,辅助多线程处理耗时操作,提升整体吞吐量;
  2. 性能瓶颈:Redis 性能瓶颈不在 CPU(单核足够处理高并发),而在内存 / 网络,因此单线程核心模型足够高效;
  3. 并发控制:基于单线程串行执行,无需锁机制,但复杂业务需通过复合指令、Lua 脚本等保障原子性;
  4. 版本演进:Redis 逐步将耗时操作异步化 / 多线程化,核心读写仍保持单线程,兼顾原子性与性能。

六、Redis 原子性与并发控制最佳实践

  1. 分布式锁 :优先使用 SET key value NX EX 过期时间(原子性设置 + 过期),避免 SETNX+EXPIRE 的非原子操作;高并发场景可结合 Lua 脚本实现锁的自动续期;
  2. 库存扣减:使用 Lua 脚本封装 "查库存→扣库存→返回结果",保证原子性;
  3. 批量操作:非原子性需求用 Pipeline(优化 RTT),原子性需求用 Lua 脚本;
  4. 避免长时间阻塞 :禁止在生产环境执行 KEYS *、FLUSHALL(同步)、HGETALL(BigKey)等指令,改用异步 / 分批替代。
相关推荐
这个DBA有点耶5 天前
MVCC深入:Read View、版本链与快照读——InnoDB并发控制的内核
数据库·mysql·架构
DBA_G5 天前
从地面到云霄:GBase数据库在民航三大场景的落地实践
数据库
自由能燃气设备5 天前
商用全预混低氮冷凝锅炉免费方案vs付费方案对比+选型避坑指南
大数据·数据库·人工智能
科创致远5 天前
科创致远 ESOP 系统核心效能与实战价值展示
大数据·数据库·人工智能·精益工程
IT大白鼠5 天前
Redis 系列 · 第 01 篇——认知入门:Redis 是什么
redis·nosql
彧azz5 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
2601_962218615 天前
万象生鲜系统称重自动多退少补算法解决生鲜非标品痛点
大数据·数据库·人工智能·python·算法
全栈弄潮儿²⁰²⁴5 天前
AI Agent 开发实战(30):限流、缓存与成本控制
人工智能·gpt·缓存·agent·限流·agi·成本控制
张洛闻Eren5 天前
k8s云原生【第十课】:水平 Pod 自动扩缩容
运维·数据库·云原生·kubernetes·github
于平安5 天前
MySQL-触发器
数据库·mysql