文章目录异步机制与阻塞规避Redis 单线程模型的生存之道Redis 实例的四类交互对象五大阻塞点阻塞点一集合全量查询和聚合操作阻塞点二bigkey 删除阻塞点三清空数据库阻塞点四AOF 日志同步写阻塞点五从库加载 RDB 文件关键路径与异步执行异步子线程机制惰性删除的命令lazy-free 的配置项lazy-free 的触发条件不能异步的阻塞点怎么办4.0 之前版本的 bigkey 删除策略写操作是否在关键路径上总结异步机制与阻塞规避Redis 单线程模型的生存之道Redis 的高性能建立在单线程模型之上主线程负责网络 IO 和所有键值对的读写操作。这个设计避免了多线程的锁竞争和上下文切换开销但也意味着任何一个耗时操作都会直接阻塞整个实例所有客户端的请求都得排队等着。理解 Redis 的阻塞点在哪里、哪些可以异步化、哪些必须在主线程完成是保障 Redis 性能稳定的基本功。Redis 实例的四类交互对象Redis 运行时要和四类对象打交道每类交互都可能产生阻塞客户端网络 IO、键值对增删改查、数据库级操作磁盘RDB 快照生成、AOF 日志写入、AOF 重写主从节点主库生成和传输 RDB、从库接收 RDB 并加载切片集群实例哈希槽信息传递、数据迁移逐一分析这四类交互中的阻塞风险。五大阻塞点阻塞点一集合全量查询和聚合操作网络 IO 本身不是问题——Redis 用 IO 多路复用epoll/kqueue处理连接主线程不会阻塞在等待连接上。真正的问题出在命令执行阶段。判断一个命令是否有阻塞风险最直接的标准是看复杂度是否为 O(N)。涉及集合的全量操作几乎都是 O(N)HGETALL返回 Hash 的所有 field-valueSMEMBERS返回 Set 的所有成员LRANGE key 0 -1返回 List 的所有元素ZRANGE key 0 -1返回 Sorted Set 的所有元素SINTERSTORE、SUNIONSTORE、SDIFFSTORE集合聚合运算当集合元素达到百万级一条 HGETALL 就可能耗时几百毫秒甚至秒级直接把主线程卡住。阻塞点二bigkey 删除删除操作看起来简单但本质是释放内存。操作系统在释放内存时需要把内存块插入空闲链表这个过程本身有开销。如果一次性释放大量内存比如删除一个包含百万元素的 Hash空闲链表操作的累积耗时会非常可观。实测数据很有说服力集合类型10 万元素100 万元素Hash约 51ms约 1980msList约 29ms约 570msSet约 48ms约 820msSorted Set约 52ms约 980msRedis 正常响应时间在微秒级一个 DEL 操作耗时近 2 秒对在线业务来说是灾难性的。阻塞点三清空数据库FLUSHDB和FLUSHALL本质上是删除所有键值对和 bigkey 删除是同一类问题只是规模更大。一个几十 GB 的实例执行 FLUSHALL阻塞时间可能达到分钟级。阻塞点四AOF 日志同步写Redis 的 AOF 持久化有三种写回策略always每条命令都同步写盘、everysec每秒写盘、no交给 OS 决定。always 策略下每条写命令执行后都要等 fsync 完成才能处理下一条。单次 fsync 耗时约 1-2ms高并发写入时这个延迟会累积。everysec 策略虽然是异步的但如果上一秒的 fsync 还没完成新的写操作也会被阻塞。阻塞点五从库加载 RDB 文件主从同步时从库收到 RDB 文件后需要先 FLUSHDB 清空自己的数据撞上阻塞点三然后加载 RDB 到内存。RDB 文件越大加载越慢。一个 4GB 的 RDB 文件加载可能需要几十秒这段时间从库无法对外服务。关键路径与异步执行五个阻塞点不是都能异步化的。判断标准是这个操作是否在关键路径上。关键路径操作是指客户端发出请求后必须等待结果返回才能继续的操作。典型的关键路径操作是读操作——客户端发了 GET必须等到值返回才能做后续处理。非关键路径操作是指客户端不需要等待具体结果的操作。比如删除操作客户端只需要知道删除请求已接受就够了至于内存什么时候真正释放客户端并不关心。按这个标准分类阻塞点是否关键路径能否异步集合全量查询和聚合是客户端等数据不能bigkey 删除否客户端不等释放结果能清空数据库否客户端不等释放结果能AOF 日志同步写否不返回数据给客户端能从库加载 RDB是加载完才能服务不能三个可以异步化的操作Redis 通过子线程机制来处理。异步子线程机制Redis 主线程启动后通过pthread_create创建三个后台子线程分别负责AOF 日志写操作键值对删除惰性删除文件关闭主线程和子线程之间通过一个任务队列交互。工作流程是主线程收到删除或清空命令主线程把操作封装成任务放入任务队列主线程立即给客户端返回 OK后台子线程从队列取出任务执行实际的内存释放这种模式叫惰性删除lazy free——主线程标记要删但实际删除延后到子线程执行。AOF 的异步写也类似everysec 策略下主线程把写 AOF封装成任务放入队列子线程负责实际的 fsync 操作。主线程不用等 fsync 完成就能继续处理下一条命令。惰性删除的命令异步删除是 Redis 4.0 引入的功能对应的命令是# 异步删除 key替代 DELUNLINK key1 key2 key3# 异步清空数据库替代 FLUSHDB / FLUSHALLFLUSHDB ASYNC FLUSHALL ASYNCUNLINK和DEL的语义完全一致区别只在于内存释放是异步的。对客户端来说UNLINK 返回后 key 就已经不可见了从键空间中移除但底层内存可能还没释放。lazy-free 的配置项Redis 4.0 提供了四个 lazy-free 配置开关lazyfree-lazy-expire yes # key 过期删除时异步释放 lazyfree-lazy-eviction yes # 内存淘汰时异步释放 lazyfree-lazy-server-del yes # RENAME 等命令覆盖旧 key 时异步释放 replica-lazy-flush yes # 从库全量同步清空数据时异步释放默认都是关闭的需要手动开启。lazy-free 的触发条件一个容易被忽略的细节即使开启了 lazy-freeRedis 也不是所有删除都走异步。Redis 会先评估释放内存的代价如果代价很低就直接在主线程释放避免跨线程传递数据的开销。具体的判断逻辑源码中的lazyfreeGetFreeEffort函数Hash / Set 底层是哈希表非 ziplist / intset且元素超过 64 个 → 异步Sorted Set 底层是跳表非 ziplist且元素超过 64 个 → 异步List 的链表节点数超过 64 个 → 异步其他情况String 不管多大、小集合、ziplist 编码的集合→ 主线程直接释放这意味着String 类型的 bigkey 即使开了 lazy-free删除时仍然会阻塞主线程。因为 String 的内存是连续的一块释放代价被认为是低的。所以最根本的建议还是不要在 Redis 里存 bigkey。不能异步的阻塞点怎么办对于集合全量查询和聚合操作建议用 SCAN 系列命令分批读取# 分批遍历 HashHSCAN myhash0COUNT200# 分批遍历 SetSSCAN myset0COUNT200每次只取一小批数据单次命令耗时可控不会长时间阻塞主线程。聚合计算放到客户端做或者用从库来跑。对于从库加载 RDB 文件核心策略是控制 RDB 文件大小。主库数据量建议控制在 2-4GB这样 RDB 文件能在可接受的时间内加载完成。如果数据量更大应该用切片集群把数据分散到多个实例。4.0 之前版本的 bigkey 删除策略如果 Redis 版本低于 4.0没有 UNLINK 和 lazy-free删除 bigkey 只能用渐进式删除# Hash 类型的渐进式删除HSCAN bigkey0COUNT200# 拿到一批 field 后HDEL bigkey field1 field2... field200# 重复直到 HSCAN 返回 cursor 为 0# 最后 DEL bigkey 删除空壳每次只删 200 个元素单次 HDEL 耗时很短不会阻塞主线程。Set 用 SSCAN SREMSorted Set 用 ZSCAN ZREMList 用 LTRIM 逐段裁剪。写操作是否在关键路径上这是一个值得思考的问题。SET、HSET、SADD 这些写操作客户端通常需要知道是否写入成功。但成功的含义因场景而异如果客户端只关心数据写进去了且命令是幂等的多次执行结果一样那写操作可以不算关键路径。如果客户端需要根据返回值做分支判断比如 SADD 返回 1 表示新增、0 表示已存在那就是关键路径。如果 Redis 配置了 maxmemory 但没设淘汰策略写入可能返回 OOM 错误客户端必须感知这个错误此时也是关键路径。所以写操作是否在关键路径上取决于业务对返回值的依赖程度。总结Redis 的阻塞规避策略可以归纳为三层架构层用 IO 多路复用避免网络阻塞用子进程处理 RDB 和 AOF 重写命令层用 SCAN 替代全量查询用 UNLINK 替代 DEL用 ASYNC 选项替代同步清空运维层控制单 key 大小避免 bigkey控制实例数据量避免 RDB 过大合理配置 AOF 写回策略单线程模型不是 Redis 的弱点而是它的设计选择。只要把阻塞操作识别出来并妥善处理单线程的 Redis 完全能支撑百万级 QPS。关键是对每一个可能耗时的操作都保持警觉在设计阶段就把风险消灭掉。
Redis - 异步机制与阻塞规避:Redis 单线程模型的生存之道
文章目录异步机制与阻塞规避Redis 单线程模型的生存之道Redis 实例的四类交互对象五大阻塞点阻塞点一集合全量查询和聚合操作阻塞点二bigkey 删除阻塞点三清空数据库阻塞点四AOF 日志同步写阻塞点五从库加载 RDB 文件关键路径与异步执行异步子线程机制惰性删除的命令lazy-free 的配置项lazy-free 的触发条件不能异步的阻塞点怎么办4.0 之前版本的 bigkey 删除策略写操作是否在关键路径上总结异步机制与阻塞规避Redis 单线程模型的生存之道Redis 的高性能建立在单线程模型之上主线程负责网络 IO 和所有键值对的读写操作。这个设计避免了多线程的锁竞争和上下文切换开销但也意味着任何一个耗时操作都会直接阻塞整个实例所有客户端的请求都得排队等着。理解 Redis 的阻塞点在哪里、哪些可以异步化、哪些必须在主线程完成是保障 Redis 性能稳定的基本功。Redis 实例的四类交互对象Redis 运行时要和四类对象打交道每类交互都可能产生阻塞客户端网络 IO、键值对增删改查、数据库级操作磁盘RDB 快照生成、AOF 日志写入、AOF 重写主从节点主库生成和传输 RDB、从库接收 RDB 并加载切片集群实例哈希槽信息传递、数据迁移逐一分析这四类交互中的阻塞风险。五大阻塞点阻塞点一集合全量查询和聚合操作网络 IO 本身不是问题——Redis 用 IO 多路复用epoll/kqueue处理连接主线程不会阻塞在等待连接上。真正的问题出在命令执行阶段。判断一个命令是否有阻塞风险最直接的标准是看复杂度是否为 O(N)。涉及集合的全量操作几乎都是 O(N)HGETALL返回 Hash 的所有 field-valueSMEMBERS返回 Set 的所有成员LRANGE key 0 -1返回 List 的所有元素ZRANGE key 0 -1返回 Sorted Set 的所有元素SINTERSTORE、SUNIONSTORE、SDIFFSTORE集合聚合运算当集合元素达到百万级一条 HGETALL 就可能耗时几百毫秒甚至秒级直接把主线程卡住。阻塞点二bigkey 删除删除操作看起来简单但本质是释放内存。操作系统在释放内存时需要把内存块插入空闲链表这个过程本身有开销。如果一次性释放大量内存比如删除一个包含百万元素的 Hash空闲链表操作的累积耗时会非常可观。实测数据很有说服力集合类型10 万元素100 万元素Hash约 51ms约 1980msList约 29ms约 570msSet约 48ms约 820msSorted Set约 52ms约 980msRedis 正常响应时间在微秒级一个 DEL 操作耗时近 2 秒对在线业务来说是灾难性的。阻塞点三清空数据库FLUSHDB和FLUSHALL本质上是删除所有键值对和 bigkey 删除是同一类问题只是规模更大。一个几十 GB 的实例执行 FLUSHALL阻塞时间可能达到分钟级。阻塞点四AOF 日志同步写Redis 的 AOF 持久化有三种写回策略always每条命令都同步写盘、everysec每秒写盘、no交给 OS 决定。always 策略下每条写命令执行后都要等 fsync 完成才能处理下一条。单次 fsync 耗时约 1-2ms高并发写入时这个延迟会累积。everysec 策略虽然是异步的但如果上一秒的 fsync 还没完成新的写操作也会被阻塞。阻塞点五从库加载 RDB 文件主从同步时从库收到 RDB 文件后需要先 FLUSHDB 清空自己的数据撞上阻塞点三然后加载 RDB 到内存。RDB 文件越大加载越慢。一个 4GB 的 RDB 文件加载可能需要几十秒这段时间从库无法对外服务。关键路径与异步执行五个阻塞点不是都能异步化的。判断标准是这个操作是否在关键路径上。关键路径操作是指客户端发出请求后必须等待结果返回才能继续的操作。典型的关键路径操作是读操作——客户端发了 GET必须等到值返回才能做后续处理。非关键路径操作是指客户端不需要等待具体结果的操作。比如删除操作客户端只需要知道删除请求已接受就够了至于内存什么时候真正释放客户端并不关心。按这个标准分类阻塞点是否关键路径能否异步集合全量查询和聚合是客户端等数据不能bigkey 删除否客户端不等释放结果能清空数据库否客户端不等释放结果能AOF 日志同步写否不返回数据给客户端能从库加载 RDB是加载完才能服务不能三个可以异步化的操作Redis 通过子线程机制来处理。异步子线程机制Redis 主线程启动后通过pthread_create创建三个后台子线程分别负责AOF 日志写操作键值对删除惰性删除文件关闭主线程和子线程之间通过一个任务队列交互。工作流程是主线程收到删除或清空命令主线程把操作封装成任务放入任务队列主线程立即给客户端返回 OK后台子线程从队列取出任务执行实际的内存释放这种模式叫惰性删除lazy free——主线程标记要删但实际删除延后到子线程执行。AOF 的异步写也类似everysec 策略下主线程把写 AOF封装成任务放入队列子线程负责实际的 fsync 操作。主线程不用等 fsync 完成就能继续处理下一条命令。惰性删除的命令异步删除是 Redis 4.0 引入的功能对应的命令是# 异步删除 key替代 DELUNLINK key1 key2 key3# 异步清空数据库替代 FLUSHDB / FLUSHALLFLUSHDB ASYNC FLUSHALL ASYNCUNLINK和DEL的语义完全一致区别只在于内存释放是异步的。对客户端来说UNLINK 返回后 key 就已经不可见了从键空间中移除但底层内存可能还没释放。lazy-free 的配置项Redis 4.0 提供了四个 lazy-free 配置开关lazyfree-lazy-expire yes # key 过期删除时异步释放 lazyfree-lazy-eviction yes # 内存淘汰时异步释放 lazyfree-lazy-server-del yes # RENAME 等命令覆盖旧 key 时异步释放 replica-lazy-flush yes # 从库全量同步清空数据时异步释放默认都是关闭的需要手动开启。lazy-free 的触发条件一个容易被忽略的细节即使开启了 lazy-freeRedis 也不是所有删除都走异步。Redis 会先评估释放内存的代价如果代价很低就直接在主线程释放避免跨线程传递数据的开销。具体的判断逻辑源码中的lazyfreeGetFreeEffort函数Hash / Set 底层是哈希表非 ziplist / intset且元素超过 64 个 → 异步Sorted Set 底层是跳表非 ziplist且元素超过 64 个 → 异步List 的链表节点数超过 64 个 → 异步其他情况String 不管多大、小集合、ziplist 编码的集合→ 主线程直接释放这意味着String 类型的 bigkey 即使开了 lazy-free删除时仍然会阻塞主线程。因为 String 的内存是连续的一块释放代价被认为是低的。所以最根本的建议还是不要在 Redis 里存 bigkey。不能异步的阻塞点怎么办对于集合全量查询和聚合操作建议用 SCAN 系列命令分批读取# 分批遍历 HashHSCAN myhash0COUNT200# 分批遍历 SetSSCAN myset0COUNT200每次只取一小批数据单次命令耗时可控不会长时间阻塞主线程。聚合计算放到客户端做或者用从库来跑。对于从库加载 RDB 文件核心策略是控制 RDB 文件大小。主库数据量建议控制在 2-4GB这样 RDB 文件能在可接受的时间内加载完成。如果数据量更大应该用切片集群把数据分散到多个实例。4.0 之前版本的 bigkey 删除策略如果 Redis 版本低于 4.0没有 UNLINK 和 lazy-free删除 bigkey 只能用渐进式删除# Hash 类型的渐进式删除HSCAN bigkey0COUNT200# 拿到一批 field 后HDEL bigkey field1 field2... field200# 重复直到 HSCAN 返回 cursor 为 0# 最后 DEL bigkey 删除空壳每次只删 200 个元素单次 HDEL 耗时很短不会阻塞主线程。Set 用 SSCAN SREMSorted Set 用 ZSCAN ZREMList 用 LTRIM 逐段裁剪。写操作是否在关键路径上这是一个值得思考的问题。SET、HSET、SADD 这些写操作客户端通常需要知道是否写入成功。但成功的含义因场景而异如果客户端只关心数据写进去了且命令是幂等的多次执行结果一样那写操作可以不算关键路径。如果客户端需要根据返回值做分支判断比如 SADD 返回 1 表示新增、0 表示已存在那就是关键路径。如果 Redis 配置了 maxmemory 但没设淘汰策略写入可能返回 OOM 错误客户端必须感知这个错误此时也是关键路径。所以写操作是否在关键路径上取决于业务对返回值的依赖程度。总结Redis 的阻塞规避策略可以归纳为三层架构层用 IO 多路复用避免网络阻塞用子进程处理 RDB 和 AOF 重写命令层用 SCAN 替代全量查询用 UNLINK 替代 DEL用 ASYNC 选项替代同步清空运维层控制单 key 大小避免 bigkey控制实例数据量避免 RDB 过大合理配置 AOF 写回策略单线程模型不是 Redis 的弱点而是它的设计选择。只要把阻塞操作识别出来并妥善处理单线程的 Redis 完全能支撑百万级 QPS。关键是对每一个可能耗时的操作都保持警觉在设计阶段就把风险消灭掉。