3.深入理解Redis线程模型

3.深入理解Redis线程模型 深入理解Redis线程模型 知识体系总览一、Redis是什么有什么用✅1. Redis是什么✅2. 2024年的Redis是什么样的二、Redis到底是单线程还是多线程三、Redis如何保证指令原子性✅1. 复合指令✅2. Redis事务Redis事务总结✅3. Pipeline1、什么是管道2、使用案例3、有什么用4、pipeline 注意点✅4. lua脚本1、什么是 lua为什么 Redis 支持 lua2、Redis 中如何执行 lua3、使用 lua 注意点✅5. Redis Function1、什么是 Function2、Function 案例3、Function 注意点✅6. Redis指令原子性总结 指令原子性方案对比四、Redis中的Bigkey问题五、Redis线程模型总结 全文总结✅1. Redis 线程模型核心客户端多线程服务端单线程✅2. 保证指令原子性的五种方式✅3. Bigkey 问题✅4. 关键版本变化 知识体系总览深入理解Redis线程模型 ├── 一、Redis是什么有什么用 │ ├── ✅1. Redis是什么 │ └── ✅2. 2024年的Redis是什么样的 ├── 二、Redis到底是单线程还是多线程 ├── 三、Redis如何保证指令原子性 │ ├── ✅1. 复合指令 │ ├── ✅2. Redis事务 │ ├── ✅3. Pipeline │ ├── ✅4. lua脚本 │ ├── ✅5. Redis Function │ └── ✅6. Redis指令原子性总结 ├── 四、Redis中的Bigkey问题 └── 五、Redis线程模型总结 核心Redis整体线程模型为「客户端多线程服务端单线程」。核心读写操作由单线程串行执行不存在MySQL那样的并发问题。但严格来说Redis后端的线程模型与版本有关——Redis4.X以前纯单线程5.x/6.x/7.x逐步引入多线程处理耗时操作。一、Redis是什么有什么用✅1. Redis是什么Redis 全称REmote DIctionary Server远程字典服务是一个完全开源的、高性能的Key-Value 数据库。官网地址https://redis.io/ 核心总结数据结构复杂Redis 相比于传统的 K-V 型数据库能够支撑更复杂的数据类型已经远远超出了缓存的范围可以实现很多复杂的业务场景。数据保存在内存但是持久化到硬盘数据全部保存在内存读写性能极高同时持久化到硬盘数据安全可靠完全可以当做一个数据库来用。官方对 Redis 的作用定位为三个方面Cache缓存、Database数据库、Vector Search向量搜索。✅2. 2024年的Redis是什么样的在 2023 年之前Redis 是一个纯粹的开源数据库。但从最近两年开始Redis 正在从一个缓存产品变成一整套生态服务。Redis Cloud基于 AWS、Azure 等公有云提供完整的企业服务并提供了Redis Enterprise企业级收费产品。Redis InsightRedis 官方推出的图形化客户端以往需要第三方客户端现在不需要了并且可以在 Redis Cloud 上直接使用。在功能层面目前已经形成了Redis OSS和Redis Stack两套服务体系Redis OSS以前常用的开源服务体系。Redis Stack基于 Redis OSS 打造的一套更完整的技术栈基于 Redis Cloud 提供服务在 Redis OSS 功能的基础上提供了很多高级的扩展功能。二、Redis到底是单线程还是多线程这是 Redis 面试过程中最喜欢问的问题几乎伴随着 Redis 的整个发展过程。 面试要点首先整体来说Redis 的线程模型可以简单解释为客户端多线程服务端单线程。Redis 为了能够与更多的客户端进行连接使用多线程来维护与客户端的 Socket 连接。在redis.conf中有一个参数maxclients维护了最大的客户端连接数# Redis is mostly single threaded, however there are certain threaded# operations such as UNLINK, slow I/O accesses and other things that are# performed on side threads.## Now it is also possible to handle Redis clients socket reads and writes# in different I/O threads. Since especially writing is so slow, normally# Redis users use pipelining in order to speed up the Redis performances per# core, and spawn multiple instances in order to scale more. Using I/O# threads it is possible to easily speedup two times Redis without resorting# to pipelining nor sharding of the instance.## By default threading is disabled, we suggest enabling it only in machines# that have at least 4 or more cores, leaving at least one spare core.# Using more than 8 threads is unlikely to help much. We also recommend using# threaded I/O only if you actually have performance problems, with Redis# instances being able to use a quite big percentage of CPU time, otherwise# there is no point in using this feature.## So for instance if you have a four cores boxes, try to use 2 or 3 I/O# threads, if you have a 8 cores, try to use 6 threads. In order to# enable I/O threads use the following configuration directive:## io-threads 4# Set the max number of connected clients at the same time. By default# this limit is set to 10000 clients, however if the Redis server is not# able to configure the process file limit to allow for the specified limit# the max number of allowed clients is set to the current file limit# minus 32 (as Redis reserves a few file descriptors for internal uses).## Once the limit is reached Redis will close all the new connections sending# an error max number of clients reached.## IMPORTANT: When Redis Cluster is used, the max number of connections is also# shared with the cluster bus: every node in the cluster will use two# connections, one incoming and another outgoing. It is important to size the# limit accordingly in case of very large clusters.## maxclients 10000 关键理解在服务端Redis 响应网络 IO 和键值对读写的请求是由一个单独的主线程完成的。Redis 基于epoll实现了 IO 多路复用这就可以用一个主线程同时响应多个客户端 Socket 连接的请求。在这种线程模型下Redis 将客户端多个并发的请求转成了串行的执行方式。因此完全不用考虑诸如 MySQL 的脏读、幻读、不可重复读之类的并发问题。这种串行化的线程模型 基于内存工作的极高性能让 Redis 成为很多并发问题的解决工具。 版本演进Redis 4.X 以前纯单线程。2018年10月 Redis 5.x进行一次大的核心代码重构。Redis 6.x / 7.x开始用全新的多线程机制来提升后台工作。持久化 RDB/AOF 文件、unlink 异步删除、集群数据同步等耗时操作都由额外线程执行。例如FLUSHALL已经提供了异步方式。 为什么核心保持单线程对于现代 RedisCPU 通常不是性能瓶颈瓶颈大部分是内存和网络所以核心改为多线程的要求并不急切。单线程为主的工作机制可以减少线程上下文切换的性能消耗。如果核心改为多线程并发执行必然带来资源竞争反而会极大增加 Redis 的业务复杂性影响执行效率。三、Redis如何保证指令原子性对于核心的读写键值操作Redis 是单线程处理的。如果多个客户端同时进行读写请求Redis 只会排队串行。也就是说针对单个客户端Redis并没有类似 MySQL 的事务那样保证同一个客户端的操作原子性。如何控制 Redis 指令的原子性呢这在不同业务场景下Redis 提供了不同的思路。✅1. 复合指令Redis 内部提供了很多复合指令它们是一个指令但明显干着多个指令的活复合指令说明MSET/HMSET批量设置键值GETSET设置新值并返回旧值SETNX不存在时才设置SETEX设置带过期时间的值这些复合指令都能很好地保持原子性。✅2. Redis事务像 MySQL 一样Redis 也提供了事务机制。127.0.0.1:6379helptransactions DISCARD(null)-- 放弃事务 summary: Discards a transaction. since:2.0.0 EXEC(null)-- 执行事务 summary: Executes all commandsina transaction. since:1.2.0 MULTI(null)-- 开启事务 summary: Starts a transaction. since:1.2.0 UNWATCH(null)-- 去掉监听 summary: Forgets about watched keys of a transaction. since:2.2.0 WATCH key[key...]-- 监听某一个key的变化key有变化后就执行当前事务 summary: Monitors changes to keys to determine the execution of a transaction. since:2.2.0使用方式也很典型开启事务后接入一系列操作然后根据执行情况选择执行事务还是回滚事务127.0.0.1:6379MULTI OK127.0.0.1:6379(TX)setk22QUEUED127.0.0.1:6379(TX)incr k2 QUEUED127.0.0.1:6379(TX)get k2 QUEUED127.0.0.1:6379(TX)EXEC --执行事务1)OK2)(integer)33)3127.0.0.1:6379DISCARD -- 放弃事务 关键区分Redis 事务 ≠ 数据库事务看下面的例子127.0.0.1:6379MULTI OK127.0.0.1:6379(TX)setk22QUEUED127.0.0.1:6379(TX)incr k2 QUEUED127.0.0.1:6379(TX)get k2 QUEUED127.0.0.1:6379(TX)lpop k2 QUEUED127.0.0.1:6379(TX)incr k2 QUEUED127.0.0.1:6379(TX)get k2 QUEUED127.0.0.1:6379(TX)exec1)OK2)(integer)33)34)(error)WRONGTYPE Operation against a key holding the wrong kind of value5)(integer)46)4lpop指令是针对 list 的操作针对 string 类型的 k2 操作报错了。但是错误的指令没有让整个事务回滚后面的指令没有受到影响 核心Redis 的事务并不是像数据库事务那样保证一起成功或一起失败。Redis 的事务作用仅仅是保证事务中的原子操作是一起执行而不会在执行过程中被其他指令加塞。 关键理解开启事务后所有操作的返回结果都是QUEUED表示这些操作只是排好了队等到EXEC后一起执行。更多说明参考官网https://redis.io/docs/latest/develop/interact/transactions/Redis事务总结1、Redis 事务可以通过 Watch 机制进一步保证在某个事务执行前某一个 key 不被修改。 注意UNWATCH取消监听只在当前客户端有效。比如下图只有在左侧客户端步骤3之前执行UNWATCH才能让事务执行成功。在右侧客户端执行UNWATCH是不生效的。2、Redis 事务失败如何回滚Redis 中的事务回滚不是回滚数据而是回滚操作如果事务在EXEC 执行前失败比如事务中的指令敲错了或者指令的参数不对那么整个事务的操作都不会执行。如果事务在EXEC 执行之后失败比如指令操作的 key 类型不对那么事务中的其他操作都会正常执行不受影响。3、事务执行过程中出现失败了怎么办只要客户端执行了EXEC指令那么就算之后客户端的连接断开了事务就会一直进行下去。事务有可能造成数据不一致。当EXEC指令执行后Redis 会先将事务中的所有操作都记录到 AOF 文件中然后再执行具体的操作。如果保存了 AOF 记录后事务的操作在执行过程中服务出现了非正常宕机服务崩溃或被kill -9就会造成 AOF 中记录的操作与数据不符合。Redis 发现这种情况后下次启动时会报错无法正常启动。这时需要使用redis-check-aof工具修复 AOF 文件将不完整的事务操作记录移除掉这样下次服务就可以正常启动了。4、事务机制优缺点什么时候用事务开放性问题无标准答案✅3. Pipeline1、什么是管道使用redis-cli --help查看客户端指令中两个不太起眼的参数--pipeTransfer raw Redis protocol from stdin to server. --pipe-timeoutnIn--pipemode, abort with errorifafter sending all data. no reply is received withinnseconds. Default timeout:30. Use0towaitforever.2、使用案例在 Linux 上编辑一个文件command.txt包含一系列指令setcount1incr count incr count incr count然后在客户端执行 redis-cli 时直接执行这个文件中的指令[root192-168-65-214 ~]# cat command.txt | redis-cli -a 123qweasd --pipeWarning: Using a password with-aor-uoption on thecommandline interface may not be safe. All data transferred. Waitingforthe last reply... Last reply received from server. errors:0, replies:4[root192-168-65-214 ~]# redis-cli -a 123qweasdWarning: Using a password with-aor-uoption on thecommandline interface may not be safe.127.0.0.1:6379get count43、有什么用 结论如果你有大批量的数据需要快速写入到 Redis 中pipeline 方式可以一定程度提高执行效率。参考官网https://redis.io/docs/latest/develop/use/pipelining/核心作用优化 RTTRound-Trip TimeRTT 是什么当客户端执行一个指令数据包需要通过网络从 Client 传到 Server然后再从 Server 返回到 Client。这个中间的时间消耗就称为 RTTRount Trip Time。可以看到如果客户端的指令非常频繁RTT 消耗就会非常可观。Redis 提供了 pipeline 机制将客户端的多个指令打包一起往服务端推送。 关键理解pipeline 就是客户端和服务端之间的一层优化。将多个指令打包一起发送减少网络往返。客户端把多个命令一次性发送服务端统一处理返回。官网案例[root192-168-65-214 ~]# printf AUTH 123qweasd\r\nPING\r\nPING\r\nPING\r\n | nc localhost 6379OK PONG PONG PONG4、pipeline 注意点 关键区分对比项复合指令事务Pipeline原子性有有一起执行不具备原子性是否阻塞其他命令会会不会Pipeline 不具备原子性只是将多条命令打包发送最终还是可能会被其他客户端的指令加塞虽然概率通常比较小。Pipeline 中通常不建议进行复杂的数据操作。Pipeline 的执行需要客户端和服务端同时完成执行过程中会阻塞当前客户端。Pipeline 中不建议拼装过多的指令指令过多会使客户端阻塞时间太长同时服务端需要回复这个很繁忙的客户端占用很多内存。 应用场景Pipeline 机制适合做一些在非热点时段进行的数据调整任务。✅4. lua脚本 为什么需要 Lua事务和 Pipeline 对于指令原子性问题都有水土不服的地方并且它们都只是对 Redis 现有指令进行拼凑无法添加更多自定义的复杂逻辑。企业中用到更多的是lua 脚本同时也是 Redis 7 版本着重调整的功能。1、什么是 lua为什么 Redis 支持 luaLua 是一种小巧的脚本语言拥有很多高级语言的特性参数类型、作用域、函数等。语法非常简单熟悉 Java 后基本上可以零门槛上手。lua 参考网站https://wiki.luatos.com/可直接在线调试但注意 Redis 7.x 支持的是 lua 5.1 版本网站是 5.3 版本 核心Lua 语言最大的特点是线程模型是单线程的这使其天生就非常适合 Redis、Nginx 等单线程模型的中间件。所以在 Redis 中执行一段 lua 脚本天然就是原子性的。2、Redis 中如何执行 luaLua API 参考官网https://redis.io/docs/latest/develop/interact/programmability/lua-api/Redis 对 lua 开始了127.0.0.1:6379helpevalEVAL script numkeys[key[key...]][arg[arg...]]summary: Executes a server-side Lua script. since:2.6.0 group: scripting 参数说明script一段 Lua 脚本程序运行在 Redis 服务器上下文中不必也不应该定义为一个 Lua 函数。numkeys键名参数的个数。key [key ...]从 EVAL 的第三个参数开始算起表示在脚本中用到的那些 Redis 键在 Lua 中通过全局变量KEYS数组以 1 为基址访问KEYS[1]、KEYS[2]。arg [arg ...]附加参数在 Lua 中通过全局变量ARGV数组访问ARGV[1]、ARGV[2]。示例127.0.0.1:6379evalreturn {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}2key1 key2 first second1)key12)key23)first4)second在 lua 脚本中使用redis.call函数来调用 Redis 的命令127.0.0.1:6379setstock_11OK -- 调整1号商品的库存。如果库存小于10就设置为10127.0.0.1:6379evallocal 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 1stock_110(integer)0127.0.0.1:6379get stock_110 注意注意其中 keys 和 args 是如何传参的。3、使用 lua 注意点1》不要在 Lua 脚本中出现死循环和耗时的运算否则 Redis 会阻塞。Redis 中有一个配置参数来控制 Lua 脚本的最长执行时间默认 5 秒钟。当 lua 脚本执行时间超过了这个时长Redis 会对其他操作返回一个BUSY错误################ NON-DETERMINISTIC LONG BLOCKING COMMANDS ###################### Maximum time in milliseconds for EVAL scripts, functions and in some cases# modules commands before Redis can start processing or rejecting other clients.## If the maximum execution time is reached Redis will start to reply to most# commands with a BUSY error.## In this state Redis will only allow a handful of commands to be executed.# For instance, SCRIPT KILL, FUNCTION KILL, SHUTDOWN NOSAVE and possibly some# module specific allow-busy commands.## SCRIPT KILL and FUNCTION KILL will only be able to stop a script that did not# yet call any write commands, so SHUTDOWN NOSAVE may be the only way to stop# the server in the case a write command was already issued by the script when# the user doesnt want to wait for the natural termination of the script.## The default is 5 seconds. It is possible to set it to 0 or a negative value# to disable this mechanism (uninterrupted execution). Note that in the past# this config had a different name, which is now an alias, so both of these do# the same:# lua-time-limit 5000# busy-reply-threshold 5000 管道 vs Lua管道 pipeline 不会阻塞 Redis而 lua 脚本会阻塞。2》尽量使用只读脚本只读脚本是 Redis 7 中新增的一种脚本执行方法表示那些不修改 Redis 数据集的只读脚本。需要在脚本上加上一个只读的标志并通过指令EVAL_RO触发。在只读脚本中不允许执行任何修改数据集的操作并且可以随时使用SCRIPT_KILL指令停止。 好处一方面可以限制某些用户的操作另一方面这些只读脚本通常都可以转移到备份节点执行从而减轻 Redis 的压力。3》热点脚本可以缓存到服务端✅5. Redis Function1、什么是 Function如果你觉得开发 lua 脚本有困难Redis 7 之后提供了另一种方法——Redis Function。Redis Function 允许将一些功能声明成一个统一的函数提前加载到 Redis 服务端可以由熟悉 Redis 的管理员加载。客户端可以直接调用这些函数而不需要再去开发函数的具体实现。 核心优势Function 中可以嵌套调用其他 Function更有利于代码复用。相比之下lua 脚本就无法进行复用。2、Function 案例在服务器上新增一个mylib.lua文件定义函数#!lua namemyliblocalfunctionmy_hset(keys,args)localhashkeys[1]localtimeredis.call(TIME)[1]returnredis.call(HSET,hash,_last_modified_,time,unpack(args))endredis.register_function(my_hset,my_hset) 注意脚本第一行#!lua namemylib是指定函数的命名空间不是注释不能少使用 Redis 客户端将函数加载到 Redis 中[root192-168-65-214 myredis]# cat mylib.lua | redis-cli -a 123qweasd -x FUNCTION LOAD REPLACEWarning: Using a password with-aor-uoption on thecommandline interface may not be safe.mylib其他客户端可以直接调用这个函数函数的调用以及传参方式跟 lua 脚本一样127.0.0.1:6379FUNCTION LIST1)1)library_name2)mylib3)engine4)LUA5)functions6)1)1)name2)my_hset3)description4)(nil)5)flags6)(empty array)127.0.0.1:6379FCALL my_hset1myhash myfieldsome valueanother_fieldanother value(integer)3127.0.0.1:6379HGETALL myhash1)_last_modified_2)17177480013)myfield4)some value5)another_field6)another value3、Function 注意点Function 同样可以进行只读调用。如果在集群中使用 Function目前版本需要在各个节点都手动加载一次。Redis 不会在集群中进行 Function 同步。Function 是要在服务端缓存的所以不建议使用太多太大的 Function。Function 和 Script 一样也有一系列的管理指令使用指令help scripting自行了解。✅6. Redis指令原子性总结以上介绍的各种机制其实都是 Redis 改变指令执行顺序的方式。 面试要点在这几种工具中Lua 脚本通常是项目中使用最多的方式。在很多追求极致性能的高并发场景Lua 脚本都会担任很重要的角色。但其他方式也需要有了解这样面临真实业务场景才有更多方案可以选择。 指令原子性方案对比方案原子性是否阻塞自定义逻辑适用场景复合指令有不会不支持简单的组合操作事务MULTI/EXEC有一起执行不会不支持需要批量串行执行Pipeline无不阻塞不支持大批量数据写入非热点时段Lua 脚本有会阻塞支持复杂业务逻辑最常用Redis Function有会阻塞支持Lua 脚本的升级版支持复用四、Redis中的Bigkey问题 核心Bigkey 指那些占用空间非常大的 key。比如一个 list 中包含 200W 个元素或者一个 string 里放一篇文章。基于 Redis 的单线程为主的核心工作机制这些 Bigkey非常容易造成 Redis 的服务阻塞。因此在实际项目中一定需要特殊关照。在 Redis 客户端指令中提供了两个扩展参数可以帮助快速发现 BigKey[root192-168-65-214 myredis]# redis-cli --help...--bigkeysSample Redis keys lookingforkeys with many elements(complexity).--memkeysSample Redis keys lookingforkeys consuming a lot of memory.参数说明--bigkeys采样查找元素数量很多的 key复杂度维度--memkeys采样查找占用内存很多的 key内存维度 注意关于 BigKey 的处理在后续课程中继续深入介绍。五、Redis线程模型总结 核心总结Redis 的线程模型整体还是多线程的只是后台执行指令的核心线程是单线程的。整个线程模型可以理解为还是以单线程为主。基于这种单线程为主的线程模型不同客户端的各种指令都需要依次排队执行。Redis 这种以单线程为主的线程模型相比其他中间件还是非常简单的。这使得 Redis 处理线程并发问题要简单高效很多。甚至在很多复杂业务场景下Redis 都是用来进行线程并发控制的很好的工具。但这并不意味 Redis 就没有线程并发问题选择合理的指令执行方式非常重要。Redis 这种比较简单的线程模型本身是不利于发挥多线程的并发优势的而且 Redis 的应用场景又通常与高性能深度绑定。所以在使用 Redis 的时候要时刻思考 Redis 的这些指令执行方式这样才能最大限度发挥 Redis 高性能的优势。 全文总结✅1. Redis 线程模型核心客户端多线程服务端单线程Redis 基于 epoll 实现 IO 多路复用核心读写单线程串行执行无并发问题从 Redis 4.X 纯单线程 → 5.x/6.x/7.x 逐步引入多线程处理持久化、异步删除等耗时操作CPU 通常不是瓶颈内存和网络才是✅2. 保证指令原子性的五种方式复合指令MSET、SETNX等最简单事务MULTI/EXEC保证一起执行不保证一起成功。失败不回滚数据只回滚操作Pipeline优化 RTT不保证原子性不阻塞Lua 脚本最常用的方案天然原子性支持自定义逻辑Redis FunctionLua 的升级支持复用Redis 7 引入✅3. Bigkey 问题大 key 容易造成服务阻塞需要特殊关注使用--bigkeys和--memkeys参数排查✅4. 关键版本变化Redis 2.6.0支持 Lua 脚本Redis 5.x2018.10核心代码重构Redis 6.x/7.x引入 IO 多线程、异步操作Redis 7.xFunction 机制、只读脚本、lua 5.1