云计算百科
云计算领域专业知识百科平台

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 关键细节
  • 客户端连接管理:Redis 通过多线程维护与客户端的 Socket 连接(maxclients 配置最大连接数),避免单线程处理连接导致的瓶颈。
  • 核心主线程:
    • 负责处理所有网络 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 事务的核心是 “批量执行 + 串行化”,而非 “原子回滚”:
    • 执行前失败(指令语法错误 / 参数错误):EXEC 时整个事务不执行;
    • 执行中失败(如对 String 键执行 LPOP):错误指令返回异常,其他指令正常执行,不会回滚;
    • WATCH 机制:监听键被修改时,EXEC 返回 nil,事务不执行(乐观锁思想)。
    3.2.3 事务执行流程

    3.2.4 事务失败处理
  • EXEC 前失败:指令语法错误(如 LPOP 少参数),EXEC 时直接返回错误,所有指令不执行;
  • EXEC 后失败:指令逻辑错误(如对 String 执行 LPOP),仅该指令返回错误,其他指令正常执行;
  • 服务宕机: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 关键注意点
  • 避免耗时操作:Lua 脚本运行在核心线程,死循环 / 耗时运算会阻塞整个 Redis;Redis 配置 lua-time-limit(默认 5 秒),超时后返回 BUSY 错误;
  • 只读脚本(Redis 7+):使用 EVAL_RO 执行只读脚本,禁止修改数据,可通过 SCRIPT_KILL 终止,且可转移到从节点执行,减轻主节点压力;
  • 脚本缓存:频繁执行的脚本可通过 SCRIPT LOAD 缓存到服务端,返回 SHA1 摘要,后续用 EVALSHA 调用,减少脚本传输开销。
  • 3.5 Redis Function(Redis 7+)

    3.5.1 核心优势
    • 基于 Lua 扩展,支持将脚本封装为 “函数”,提前加载到服务端,客户端直接调用;
    • 支持函数嵌套调用,代码复用性更高(Lua 脚本无法复用);
    • 支持权限控制、只读调用,更适合生产环境管理。
    3.5.2 使用示例
  • 编写 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 解决方案

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

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

  • 分布式锁:优先使用 SET key value NX EX 过期时间(原子性设置 + 过期),避免 SETNX+EXPIRE 的非原子操作;高并发场景可结合 Lua 脚本实现锁的自动续期;
  • 库存扣减:使用 Lua 脚本封装 “查库存→扣库存→返回结果”,保证原子性;
  • 批量操作:非原子性需求用 Pipeline(优化 RTT),原子性需求用 Lua 脚本;
  • 避免长时间阻塞:禁止在生产环境执行 KEYS *、FLUSHALL(同步)、HGETALL(BigKey)等指令,改用异步 / 分批替代。
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » Redis 核心知识点归纳与详解
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!