Redis线程模型
1. 线程模型
1.1 Redis 到底是单线程还是多线程?
「Redis 是单线程还是多线程」是面试中最喜欢问的问题之一,答案也几乎伴随着 Redis 的整个发展过程在不断变化。
很多资料会简单地说「Redis 是单线程的」,这个说法其实不够准确。整体来说,Redis 的线程模型可以概括为:客户端多线程,服务端单线程。
- 客户端:Redis 为了能够与更多的客户端建立连接,使用多线程来维护与客户端之间的 Socket 连接。
redis.conf中的maxclients参数维护了最大的客户端连接数,默认是 10000。 - 服务端:Redis 响应网络 IO 和键值对读写的请求,由一个单独的主线程完成。Redis 基于 IO 多路复用实现(Linux 下是 epoll),一个主线程就可以同时响应多个客户端 Socket 连接的请求。
在这种线程模型下,Redis 把客户端多个并发的请求转成了串行的执行方式。因此,在 Redis 中完全不用考虑 MySQL 中诸如脏读、幻读、不可重复读之类的并发问题。并且,这种串行化的线程模型,加上 Redis 基于内存工作的极高性能,也让 Redis 成为很多并发问题的解决工具。
redis.conf 中关于线程的注释也印证了这一点:
# 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.
#
# io-threads 4
# maxclients 10000面试怎么回答?
Redis 的整体线程模型可以简单总结为「客户端多线程,服务端单线程」。服务端由一个主线程处理网络 IO 和命令执行;6.0 之后虽然引入了 IO 线程,但 IO 线程只负责网络读写,真正执行命令的核心线程依然是单线程。
1.2 服务端的线程演进
严格来说,Redis 服务端的线程模型与版本有关:
| 版本 | 线程模型变化 |
|---|---|
| 4.x 之前 | 纯单线程 |
| 4.0 | 引入后台线程,处理 UNLINK 异步删除、FLUSHALL ASYNC 等耗时操作 |
| 5.x | 2018 年 10 月进行了一次大的核心代码重构 |
| 6.0 | 引入 IO 线程,由多个线程分担客户端 Socket 的读写 |
| 7.x | RDB/AOF 持久化、UNLINK 异步删除、集群数据同步等比较费时的操作,都由额外的线程执行 |
按职责划分,Redis 中的线程可以分为三类:
| 线程 | 职责 |
|---|---|
| 主线程 | 命令执行、网络 IO 事件的接收与分发 |
| IO 线程 | 6.0 引入,协助主线程完成 Socket 的读写与协议解析,不参与命令执行 |
| 后台线程 | 4.0 引入,处理 UNLINK 异步删除、AOF 刷盘、关闭文件、FLUSHALL ASYNC 等耗时操作 |
注意
6.0 的多线程只用于网络读写,命令执行依然是单线程。也正因为如此,Redis 指令的原子性并没有被破坏。
IO 线程默认关闭,官方建议只在确实遇到性能问题、CPU 占用比较高的情况下开启:
# 开启 IO 多线程,默认关闭
io-threads 4
# 让 IO 线程也处理读请求,默认 no
io-threads-do-reads yes官方给出的经验值是:机器至少 4 核再考虑开启,并留一个空闲核;4 核建议使用 2~3 个 IO 线程,8 核建议使用 6 个,超过 8 个线程收益不大。
1.3 单线程服务端如何扛住并发?
单线程之所以能同时处理成千上万个客户端连接,靠的是 IO 多路复用。Redis 会根据操作系统选择不同的实现:Linux 下是 epoll,macOS 下是 kqueue,Solaris 下是 evport,其他平台则回退到 select,编译时会自动选择性能最好的一个。
select、poll、epoll 的对比如下:
| 对比项 | select | poll | epoll |
|---|---|---|---|
| 数据结构 | 数组 | 链表 | 红黑树 + 就绪链表 |
| 最大连接数 | 有上限(默认 1024) | 无上限 | 无上限 |
| 查找就绪连接 | 轮询所有连接 | 轮询所有连接 | 回调触发,只返回就绪的连接 |
| 时间复杂度 | O(n) | O(n) | O(1) |
基于 IO 多路复用,Redis 使用 Reactor 模式实现了文件事件处理器(file event handler),它由四部分组成:
- 多个 Socket:客户端连接产生的套接字。
- IO 多路复用程序:同时监听多个 Socket,把就绪的事件放入队列。
- 文件事件分派器:从队列中取出事件,分派给对应的事件处理器。
- 事件处理器:处理具体事件,包括连接应答处理器、命令请求处理器、命令回复处理器等。
事件类型只有两种:AE_READABLE(可读事件)和 AE_WRITABLE(可写事件)。当两者同时就绪时,Redis 会优先处理读事件。

三种事件处理器的配合流程如下:
- 建立连接:客户端连接 Redis 时产生
AE_READABLE事件,触发连接应答处理器,Redis 接受连接并创建客户端状态。 - 发送命令:客户端发送命令后产生
AE_READABLE事件,触发命令请求处理器,Redis 读取命令并交给主线程执行,执行结果写入回复缓冲区。 - 返回结果:如果结果无法一次性写回,Redis 会为客户端绑定
AE_WRITABLE事件;客户端可以接收数据时触发命令回复处理器,把回复缓冲区的内容写入 Socket,写完后解绑AE_WRITABLE。
1.4 为什么核心线程坚持单线程?
既然现代 CPU 早就是多核架构,一直使用单线程就无法发挥多核 CPU 的性能优势。但 Redis 的核心线程依然坚持单线程,主要有三点原因:
- 性能瓶颈不在 CPU。对于现在的 Redis 来说,CPU 通常不会成为性能瓶颈,真正影响 Redis 性能的大部分是内存和网络,因此把核心线程改成多线程的需求并不急切。
- 减少线程上下文切换的性能消耗。单线程模型下,不存在多线程之间的切换开销。
- 避免资源竞争。如果核心线程改为多线程并发执行,就必然带来资源竞争,反而会极大增加 Redis 的业务复杂性,影响业务执行效率。
当然,多线程是一个必然结果,只不过对于 Redis 来说,为了保持快速,多线程走得非常谨慎:核心命令保持单线程,把网络 IO 和耗时操作交给其他线程。
1.5 单线程的代价
单线程让 Redis 简单高效,但也意味着任何耗时操作都会阻塞后续所有命令:
- 阻塞命令:
keys *、flushall等命令在生产环境要慎用。4.0 之后,flushall可以通过FLUSHALL ASYNC交给后台线程异步执行。 - Bigkey:一个 list 中放了 200 万个元素,或者一个 string 里存了一整篇文章,这类占用空间非常大的 key 在处理时同样会造成阻塞,实际项目中需要特殊关照,可以通过
redis-cli --bigkeys和redis-cli --memkeys快速发现(Bigkey 的处理会在后续章节深入介绍)。 - 多命令不保证原子性:Redis 单线程保证的是单个命令的原子性,多条命令组合执行时,仍然可能被其他客户端的命令加塞。
因此,如何控制多条指令的原子性,是 Redis 使用中必须掌握的内容,这正是下一章要讨论的问题。
2. 原子性保证
对于核心的读写键值操作,Redis 是单线程处理的。如果多个客户端同时进行读写请求,Redis 只会排队串行执行。也就是说,Redis 单线程保证的是单个命令的原子性;针对单个客户端,Redis 并没有类似 MySQL 那样的事务机制来保证同一客户端一组操作的原子性。
例如客户端 A 执行 get k1 拿到 10,准备修改后再 set k1;在 A 执行 set 之前,客户端 B 先执行了 set k1 20,那么 A 的写入就会覆盖 B 的修改。这类问题在并发场景下很常见,如何控制 Redis 指令的原子性,是使用 Redis 时必须掌握的内容。
针对不同的业务场景,Redis 提供了不同的思路,我们需要在项目中灵活选择:
| 方式 | 是否原子 | 特点 |
|---|---|---|
| 复合指令 | 是 | 单条指令完成多个操作,逻辑固定 |
| 事务(MULTI/EXEC) | 弱原子 | 保证指令执行时不被加塞,但不保证一起成功或失败 |
| Pipeline | 否 | 只是打包发送、减少 RTT,不具备原子性 |
| Lua 脚本 | 是 | 脚本整体原子执行,可以编写复杂逻辑 |
| Redis Function | 是 | 7.0 引入,服务端预加载的函数,可以嵌套复用 |
2.1 复合指令
Redis 内部提供了很多复合指令,它们是一个指令,却明显干着多个指令的活,比如 MSET(HMSET)、GETSET、SETNX、SETEX。这些复合指令都能很好地保持原子性。
MSET key1 value1 key2 value2 # 批量设值
MGET key1 key2 # 批量取值
GETSET counter 0 # 获取旧值并设置新值,常用于重置计数
SETNX product:10001 true # 不存在才设置,常用于分布式锁
SETEX session:1 60 token # 设置值并指定过期时间常用的复合指令总结如下:
| 指令 | 作用 | 典型场景 |
|---|---|---|
MSET / MGET | 批量设置 / 获取字符串键值 | 对象的多字段缓存 |
HMSET / HMGET | 批量设置 / 获取 Hash 字段 | 对象缓存(4.0 之后推荐用 HSET) |
GETSET | 设置新值并返回旧值 | 计数器重置、状态切换 |
SETNX | key 不存在时才设置 | 分布式锁 |
SETEX | 设置值并指定过期时间 | 缓存、验证码 |
INCR / DECR | 原子加减 | 计数器、库存 |
此外,SET 指令还支持 EX、NX 等选项,可以把「设置值 + 过期时间 + 不存在才设置」合并成一条原子指令:
SET product:10001 true EX 10 NX # 防止程序意外终止导致死锁注意
复合指令虽然原子,但只能完成固定的逻辑。如果需要「先判断再修改」这类复杂逻辑,还是要借助事务或 Lua 脚本。
2.2 Pipeline
大多数情况下,我们都会通过请求-响应机制去操作 Redis。只用这种模式的一般的步骤是:先获得 Jedis 实例,然后通过 Jedis 的 get/put 方法与 Redis 交互。由于 Redis 是单线程的,下一次请求必须等待上一次请求执行完成后才能继续执行。

然而批量操作下这样网络开销很大,导致速度慢,使用 Pipeline 模式,客户端可以一次性的发送多个命令,无需等待服务端返回。这样就大大的减少了网络往返时间,提高了系统性能。

上面提到的网络往返时间,就是 RTT(Round Trip Time)。当客户端执行一个指令时,数据包需要通过网络从 Client 传到 Server,再从 Server 返回到 Client,中间消耗的时间就是 RTT。单个指令的 RTT 消耗不起眼,但如果指令非常频繁,RTT 累加起来就非常可观了。Pipeline 的思路很直接:把客户端的多个指令打包,一起推送到服务端,从而摊薄 RTT。
示例代码,初始化 10000 个 key,批量删除 5000 个。
/**
* 初始化数据,批量设值
*/
public class RedisTools {
public static int arraylength = 10000;
public static String ip = "127.0.0.1";
public static int port = 6379;
public static String auth = "123456";
public static String[] keys = new String[arraylength / 2];
/**
* 初始化数据,批量设值
* redis提供的批量设值mset 批量取值 mget,但没有批量删除mdel指令
*/
public static void initRedisData() {
Jedis jedis = new Jedis(ip, port);
jedis.auth(auth);
String[] str = new String[arraylength];
int j = 0;
for (int i = 0; i < str.length / 2; i++) {
str[j] = "key:" + i;
str[j + 1] = "v" + i;
j = j + 2;
keys[i] = "key:" + i;
}
jedis.mset(str);
jedis.close();
}
}
public static void delWithPipe(String... keys){
Jedis jedis = new Jedis(RedisTools.ip,RedisTools.port);
Pipeline pipelined = jedis.pipelined();
for(String key : keys){
// 将所有要删除的key封装到pipelined
pipelined.del(key);
}
// 发送请求,执行删除操作
pipelined.sync();
jedis.close();
}
public static void main(String[] args) {
RedisTools.initRedisData();
long t = System.currentTimeMillis();
delWithPipe(RedisTools.keys);
System.out.println(System.currentTimeMillis()-t);
}除了在代码中使用 Pipeline,redis-cli 也提供了 --pipe 参数,可以把原生 Redis 协议从标准输入传输到服务端。先准备一个 command.txt 文件:
# command.txt
set count 1
incr count
incr count
incr count然后通过管道批量执行:
cat command.txt | redis-cli --pipe
# All data transferred. Waiting for the last reply...
# Last reply received from server.
# errors: 0, replies: 4Pipeline 注意点
- Pipeline 不具备原子性。它只是把多条命令打包发送到服务端,执行过程中仍然可能被其他客户端的指令加塞,只是概率比较小,因此不建议在 Pipeline 中做复杂的数据操作。
- 复合指令和事务会阻塞其他命令的执行,而 Pipeline 不会。
- 不要在 Pipeline 中拼装过多指令。指令过多会使客户端阻塞时间太长,同时服务端需要回复这个很繁忙的客户端,占用较多内存。
- 总体来说,Pipeline 适合做一些非热点时段进行的数据调整任务。
2.3 弱事务
Pipeline 只是把多条命令打包发送,并不保证原子性;为了让它们原子执行,Redis 提供了简单的事务。
什么是事务?事务是指一组动作的执行,这一组动作要么成功,要么失败。
但是 Redis 的事务没有 MySQL 等关系型数据库强大,为什么这么说呢?因为 Redis 的事务只能保证语法错误的多条命令的原子性,而没办法保证多条命令语法正确,但是发生其他错误的情况。
2.3.1 Redis 弱事务的基本使用
将需要执行的命令放入 multi 和 exec 两个命令之间。
127.0.0.1:6379> flushall // 线上环境慎用
OK
127.0.0.1:6379> multi // 事务开始
OK
127.0.0.1:6379> set user:name lisi // 业务操作1
QUEUED
127.0.0.1:6379> set user:age 22 // 业务操作2
QUEUED
127.0.0.1:6379> sadd user:list user:1 user:2 // 业务操作3
QUEUED
127.0.0.1:6379> exec //事务结束,执行操作及返回结果
1) OK
2) OK
3) (integer) 22.3.2 Redis 弱事务的两种情况
语法错误可以保证原子性
127.0.0.1:6379> flushall // 线上环境慎用
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name lisi
QUEUED
127.0.0.1:6379> set age 22
QUEUED
127.0.0.1:6379> sett name zhangsan
(error) ERR unknown command `sett`, with args beginning with: `name`, `zhangsan`,
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> get age
(nil)但是如果其他错误则无法保证原子性
127.0.0.1:6379> flushall // 线上环境慎用
OK
127.0.0.1:6379> set user:name lisi
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set user:age 22
QUEUED
127.0.0.1:6379> sadd user:name zhangsan
QUEUED
127.0.0.1:6379> get user:name
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) "lisi"
127.0.0.1:6379> get user:name
"lisi"从上面的例子可以看出,Redis 事务回滚的不是数据,而是操作:
- 如果事务在 EXEC 执行前失败(比如指令敲错、参数不对),整个事务的操作都不会执行。
- 如果事务在 EXEC 执行后失败(比如指令操作的 key 类型不对),事务中的其他操作会正常执行,不受影响。
注意
只要客户端执行了 EXEC 指令,即使之后连接断开,事务也会一直进行下去。另外,EXEC 执行后,Redis 会先将事务中的所有操作记录到 AOF 文件中,然后再执行具体的操作。如果记录完成后、操作执行过程中服务非正常宕机(比如被 kill -9),就可能造成 AOF 中记录的操作与数据不一致,导致下次启动失败,此时需要使用 redis-check-aof 工具修复 AOF 文件,将这些不完整的事务操作记录移除。
2.3.3 停止事务
127.0.0.1:6379> flushall // 线上环境慎用
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set tt 1
QUEUED
127.0.0.1:6379> discard // 停止事务
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI
127.0.0.1:6379> get tt
(nil)2.3.4 watch 让事务失效
WATCH 可以在事务开始前监听一个或多个 key:如果被监听的 key 在 EXEC 执行前被其他客户端修改,整个事务就会被放弃,EXEC 返回 nil。
# 客户端 A
127.0.0.1:6379> watch k1 // 监听 k1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 100
QUEUED
# 此时客户端 B 执行了 set k1 200
127.0.0.1:6379> exec
(nil) // k1 被修改过,事务未执行UNWATCH 可以取消监听,但它只在当前客户端有效;WATCH 的监听也只对当前客户端(连接)有效。基于 WATCH 可以实现乐观锁:当 EXEC 返回 nil 时,由业务代码重新读取数据并重试整个事务。

2.4 Lua 脚本
Redis 的事务和 Pipeline 机制对指令原子性都有一定帮助,但它们都只是对现有指令的拼凑,无法添加更多自定义的复杂逻辑,而且事务还不保证一起成功或失败。因此在企业中,用到更多的是 Lua 脚本。
2.4.1 为什么 Redis 支持 Lua?
Lua 是一种小巧的脚本语言,拥有很多高级语言的特性,比如参数类型、作用域、函数等。它的语法非常简单,熟悉 Java 后基本上可以零门槛上手。如果对 Lua 语法感兴趣,推荐一个可以在线调试的网站:https://wiki.luatos.com/。
注意
Redis 7.x 支持的 Lua 版本是 5.1,而上面这个网站支持的是 5.3,在线调试时注意版本差异。
Lua 语言最大的特点是它的线程模型也是单线程的,这使得 Lua 天生就非常适合接入 Redis、Nginx 这类单线程模型的中间件。在 Redis 中执行一段 Lua 脚本,天然就是原子的。
2.4.2 Redis 中如何执行 Lua?
Redis 从 2.6.0 版本开始支持 Lua,可以通过 help eval 查看指令说明:
127.0.0.1:6379> help eval
EVAL script numkeys [key [key ...]] [arg [arg ...]]
summary: Executes a server-side Lua script.
since: 2.6.0
group: scriptingscript:一段 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:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"在 Lua 脚本中,可以通过 redis.call 函数调用 Redis 命令。例如下面这个库存调整的脚本:库存小于 10 就设置为 10。
127.0.0.1:6379> set stock_1 1
OK
127.0.0.1:6379> eval "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" 1 stock_1 10
(integer) 0
127.0.0.1:6379> get stock_1
"10"在 Java 中可以这样调用:
Jedis jedis = new Jedis(RedisTools.ip, RedisTools.port);
jedis.auth(RedisTools.auth);
String script = "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";
Object result = jedis.eval(script,
Collections.singletonList("stock_1"),
Collections.singletonList("10"));
System.out.println(result); // 0
jedis.close();为什么要显式传 KEYS?
在 Redis Cluster 中,KEYS 用于计算 key 所在的槽,保证脚本中所有 key 落在同一节点上。因此脚本中用到的 key 都应该通过 KEYS 传递,不要硬编码在脚本里。
2.4.3 使用注意点
- 不要在 Lua 脚本中出现死循环和耗时的运算,否则 Redis 会阻塞,不再接受其他命令。Redis 提供了
lua-time-limit(新版本叫busy-reply-threshold)配置,默认 5 秒;脚本执行超过该时长后,Redis 会对其他操作返回 BUSY 错误,而不是一直阻塞。此时可以通过SCRIPT KILL停止没有执行过写命令的脚本,通过FUNCTION KILL停止函数,万不得已时只能SHUTDOWN NOSAVE。 - 尽量使用只读脚本。只读脚本是 Redis 7 新增的一种脚本执行方式,表示那些不修改 Redis 数据集的脚本,需要加上只读标志并通过
EVAL_RO触发。只读脚本中不允许执行任何修改数据集的操作,并且可以随时使用SCRIPT KILL停止。好处一方面是可以限制某些用户的操作,另一方面只读脚本通常可以转移到备份节点执行,从而减轻 Redis 的压力。 - 热点脚本可以缓存到服务端。先用
SCRIPT LOAD把脚本加载到服务端并得到 SHA1 值,之后用EVALSHA复用,避免每次传输脚本文本。
2.5 Redis Function
2.5.1 什么是 Function
如果觉得开发 Lua 脚本有困难,Redis 7 之后提供了另一种方案:Redis Function。它允许将一些功能声明成统一的函数,提前加载到 Redis 服务端(可以由熟悉 Redis 的管理员加载),客户端直接调用这些函数,而不需要再开发函数的具体实现。
Function 更大的好处在于,Function 中可以嵌套调用其他 Function,从而更有利于代码复用;相比之下,Lua 脚本之间无法相互调用、复用。
2.5.2 Function 案例
例如在服务器上新建一个 mylib.lua 文件,在文件中定义函数:
#!lua name=mylib
local function my_hset(keys, args)
local hash = keys[1]
local time = redis.call('TIME')[1]
return redis.call('HSET', hash, 'last_modified', time, unpack(args))
end
redis.register_function('my_hset', my_hset)注意
脚本第一行的 #!lua name=mylib 用于指定函数的命名空间,不是注释,不能省略。
然后使用 redis-cli 将函数加载到 Redis 中:
cat mylib.lua | redis-cli -x FUNCTION LOAD REPLACE
# "mylib"加载后,其他客户端就可以直接调用这个函数,调用与传参方式和 Lua 脚本一致:
127.0.0.1:6379> FUNCTION LIST
1) 1) "library_name"
2) "mylib"
3) "engine"
4) "LUA"
127.0.0.1:6379> FCALL my_hset 1 myhash myfield "some value" another_field "another value"
(integer) 3
127.0.0.1:6379> HGETALL myhash
1) "last_modified"
2) "1717748001"
3) "myfield"
4) "some value"
5) "another_field"
6) "another value"在 Java 中可以这样调用(需要 Jedis 4.2+,对应 Redis 7+):
Jedis jedis = new Jedis(RedisTools.ip, RedisTools.port);
jedis.auth(RedisTools.auth);
Object result = jedis.fcall("my_hset",
Collections.singletonList("myhash"),
Arrays.asList("myfield", "some value", "another_field", "another value"));
System.out.println(result); // 3
jedis.close();Redis Function 和 Lua 脚本的对比:
| 对比项 | Lua 脚本 | Redis Function |
|---|---|---|
| 执行方式 | 客户端每次发送脚本(或 EVALSHA) | 服务端预加载,客户端直接调用 |
| 复用性 | 脚本之间无法相互调用 | 可以嵌套调用其他 Function |
| 只读执行 | EVAL_RO | FCALL_RO |
| 集群支持 | 按 key 路由到对应节点 | 需在每个节点手动加载,不会同步 |
| 适用场景 | 临时、一次性的原子逻辑 | 长期使用、需要复用的公共逻辑 |
2.5.3 使用注意点
- Function 同样可以进行只读调用,使用
FCALL_RO触发。 - 如果在集群中使用 Function,目前版本需要在各个节点都手动加载一次,Redis 不会在集群中同步 Function。
- Function 需要在服务端缓存,所以不建议使用太多、太大的 Function。
- Function 和 Script 一样,都有一系列管理指令,可以通过
help @scripting自行了解,比如FUNCTION LIST、FUNCTION DELETE、FUNCTION FLUSH等。
2.6 指令原子性总结
以上介绍的各种机制,其实都是 Redis 改变指令执行顺序的方式,可以根据实际业务场景灵活选择:
- 简单的批量操作:优先使用
MSET、HMSET等复合指令。 - 只是为了减少网络往返:使用 Pipeline,但要清楚它不具备原子性。
- 希望一组指令执行时不被其他客户端加塞:使用 Redis 事务,并配合
WATCH实现乐观锁。 - 需要「判断 + 修改」等复杂逻辑,且要求整体原子:使用 Lua 脚本。
- 逻辑需要长期复用、集中管理:使用 Redis Function。
在这几种工具中,Lua 脚本通常是项目中使用最多的方式,很多追求极致性能的高并发场景都有 Lua 脚本的身影。当然,其他方式也需要了解,这样面对真实业务场景时,才能有更多的方案可以选择。