目录
一、Redis 事务
什么是 Redis 事务?
你可以将 Redis 中的事务理解为:Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。
Redis 事务实际开发中使用的非常少,功能比较鸡肋,不要将其和我们平时理解的关系型数据库的事务混淆了。
除了不满足原子性和持久性之外,事务中的每条命令都会与 Redis 服务器进行网络交互,这是比较浪费资源的行为。明明一次批量执行多个命令就可以了,这种操作实在是看不懂。
因此,Redis 事务是不建议在日常开发中使用的。
事务生命周期与关键命令
MULTI
: 开启事务。
执行
MULTI
命令后,客户端连接进入事务状态。后续发送的所有命令(除了
EXEC
,DISCARD
,WATCH
,UNWATCH
以及可能导致入队失败的命令)不会被立即执行,而是被服务器按顺序放入一个队列(事务队列)中。服务器返回
QUEUED
表示命令已成功进入队列。
命令入队:
在
MULTI
之后,客户端发送需要一起执行的命令(如SET
,GET
,INCR
,SADD
,HSET
等)。服务器检查命令语法。语法检查发生在入队时。
如果命令语法正确,服务器返回
QUEUED
,命令进入队列。如果命令语法错误(例如命令不存在、参数个数错误),整个事务会被标记为失败。此时即使发送
EXEC
,事务中的所有命令也不会被执行,EXEC
会返回错误(例如(error) EXECABORT Transaction discarded because of previous errors.
)。
EXEC
: 执行事务。
当客户端发送
EXEC
命令时,服务器按入队顺序依次执行事务队列中的所有命令。执行完成后,退出事务状态。
返回值是一个数组,包含事务中每个命令的执行结果,结果的顺序与命令入队的顺序一致。
原子性保证: 在执行
EXEC
期间,服务器是单线程处理命令的,因此队列中的所有命令会连续执行,不会被其他客户端的命令打断。这保证了“全做或全不做”的原子性(在命令执行层面,而非错误处理层面)。
DISCARD
: 取消事务。
在
MULTI
之后、EXEC
之前,发送DISCARD
命令会清空事务队列,并退出事务状态。客户端放弃执行队列中的所有命令。
通常在决定不执行已入队的命令时使用。
Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 事务是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的。
Redis 事务也不满足持久性。
如何解决 Redis 事务的缺陷?
Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常类似。我们可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减少了网络开销。
一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。
不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此,严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。
如果想要让 Lua 脚本中的命令全部执行,必须保证语句语法和命令都是对的。
另外,Redis 7.0 新增了 Redis functions 特性,你可以将 Redis functions 看作是比 Lua 更强大的脚本。
二、Redis 线程模型
Redis 是单线程吗?
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)的:
Redis 在 2.6 版本,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;
Redis 在 4.0 版本之后,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。例如执行 unlink key / flushdb async / flushall async 等命令,会把这些删除操作交给后台线程来执行,好处是不会导致 Redis 主线程卡顿。因此,当我们要删除一个大 key 的时候,不要使用 del 命令删除,因为 del 是在主线程处理的,这样会导致 Redis 主线程卡顿,因此我们应该使用 unlink 命令来异步删除大 key。
之所以 Redis 为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理,是因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 Redis 主线程就很容易发生阻塞,这样就无法处理后续的请求了。
后台线程相当于一个消费者,生产者把耗时任务丢到任务队列中,消费者(BIO)不停轮询这个队列,拿出任务就去执行对应的方法即可。
关闭文件、AOF 刷盘、释放内存这三个任务都有各自的任务队列:
BIO_CLOSE_FILE,关闭文件任务队列:当队列有任务后,后台线程会调用 close(fd),将文件关闭;
BIO_AOF_FSYNC,AOF刷盘任务队列:当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到队列中。当发现队列有任务后,后台线程会调用 fsync(fd),将 AOF 文件刷盘;
BIO_LAZY_FREE,lazy free 任务队列:当队列有任务后,后台线程会 free(obj) 释放对象 / free(dict) 删除数据库所有对象 / free(skiplist) 释放跳表对象;
Redis 单线程模式是怎样的?
Redis 采用单线程为什么还这么快?
官方使用基准测试的结果是,单线程的 Redis 吞吐量可以达到 10W/每秒,如下图所示:
Redis 采用单线程(网络 I/O 和执行命令)却如此高效的原因如下:
内存操作:Redis 的大部分操作都在内存中完成,并采用高效的数据结构,因此 Redis 的瓶颈通常是内存容量或网络带宽,而非 CPU。既然 CPU 不是瓶颈,采用单线程方案更为合适;
避免竞争开销:单线程模型避免了多线程竞争,省去了线程切换的时间和性能开销,同时不会产生死锁问题;
I/O 多路复用:Redis 采用 I/O 多路复用机制(如 select/epoll)处理大量客户端 Socket 请求。该机制允许单个线程同时管理多个监听 Socket 和已连接 Socket。内核会持续监听这些 Socket 上的请求,一旦有请求到达,就交给 Redis 线程处理,从而实现单线程高效处理多 I/O 流的效果。
Redis 6.0 之前为什么使用单线程?
CPU 并不是制约 Redis 性能的瓶颈所在,更多情况下是受到内存大小和网络I/O的限制,所以 Redis 核心网络模型使用单线程并没有什么问题。如果你想要利用服务的多核CPU,可以在一台服务器上启动多个节点或者采用分片集群的方式。
使用了单线程后,可维护性高。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度,同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。
Redis 6.0 之后为什么引入了多线程?
虽然 Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,但是在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件性能的提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。
所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理,所以大家不要误解 Redis 有多线程同时执行命令。
Redis 官方表示,Redis 6.0 版本引入的多线程 I/O 特性对性能提升至少是一倍以上。
Redis 6.0 版本支持的 I/O 多线程特性,默认情况下 I/O 多线程只针对发送响应数据(write client socket),并不会以多线程的方式处理读请求(read client socket)。