Redis 到底是单线程还是多线程?
总体来说,可以简单解释为客户端使用多线程,服务端核心线程使用单线程。
Redis 为了能够与更多客户端进行连接,使用多线程来维护与客户端的 Socket 连接,redis.conf 配置文件中的 maxclients 参数维护了最大客户端连接数。
在服务端,Redis 响应网络 IO 和键值对读写的请求,是由单线程完成的。Redis 基于 epoll 实现了 IO 多路复用,这种模式可以将多个并发的请求转化为串行的执行方式。
注意: 在 Redis7.x 版本中,后端对于比较耗时的操作,可以使用多线程机制来提升后台的工作。redis.conf 配置文件中的 io-threads 参数可以设置线程数。
Redis I/O 多路复用模型(基于 epoll)
┌──────────────────────────────────────────────┐
│ 多个客户端连接 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Client A │ │ Client B │ │ Client C │ ... │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
└───────┼────────────┼────────────┼────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────┐
│ epoll 实例 │
│(注册所有 client socket 的读/写事件) │
└────────────────┬───────────────────────┘
│
┌─────────▼─────────┐
│ epoll_wait 阻塞等待 │◄─────┐
└─────────┬─────────┘ │
│ │
有 N 个 socket 准备好(A、B、C)
▼ │
┌──────────────────────┐ │
│ 事件循环(单线程) │──┘
│ │
│ for i in events: │
│ 读取 socket 数据 │───► readQueryFromClient()
│ 解析 & 执行命令 │───► 执行命令(如 GET、SET)
│ 写入响应缓冲区 │───► 注册写事件
└─────────┬────────────┘
│
socket 可写后 epoll 通知
▼
sendReplyToClient()
│
响应发给客户端
-
客户端 A/B/C:每个连接对应一个 socket
-
epoll 实例:Redis 注册每个连接的“感兴趣事件”(比如可读、可写)
-
epoll_wait:系统调用,阻塞等待直到有事件发生
-
事件循环:Redis 的主线程循环处理所有已就绪连接
-
readQueryFromClient():读取命令并解析
-
sendReplyToClient():把执行结果发送给客户端
Redis 为什么一直使用单线程?
因为经过测试发现,性能瓶颈并不在 CPU 上,而是因为内存和网络,所以核心线程改为多线程的需求并不急切。并且多线程还会带来线程上下文切换带来的性能消耗,资源竞争,反而会影响业务的执行效率。
Redis 为什么不能保证指令原子性?
因为同一个客户端在执行两条指令时,两条指令中间可能会其他客户端的指令。如下图所示,Client1 最终获得的结果是 3,因为在执行 get 指令之前,Client2 执行了一次 incr 操作。
Redis 如何保证指令原子性?
1、复合指令
例如 GETSET, MSET, HMSET, SETNX。
2、Redis 事务 (了解)
127.0.0.1:6379> help @transactions
DISCARD (null)
summary: Discards a transaction.
since: 2.0.0
EXEC (null)
summary: Executes all commands in a 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 ...]
summary: Monitors changes to keys to determine the execution of a transaction.
since: 2.2.0
127.0.0.1:6379> MULTI #开启事务
OK
127.0.0.1:6379(TX)> set ec a
QUEUED
127.0.0.1:6379(TX)> get ec
QUEUED
127.0.0.1:6379(TX)> EXEC #执行事务
1) OK
2) "a"
注意:事务中的存在语句执行报错,也不会影响其他语句的执行
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set kk2 10
QUEUED
127.0.0.1:6379(TX)> get kk2
QUEUED
127.0.0.1:6379(TX)> lpush kk2 8
QUEUED
127.0.0.1:6379(TX)> incr kk2
QUEUED
127.0.0.1:6379(TX)> get kks
QUEUED
127.0.0.1:6379(TX)> get kk2
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) "10"
3) (error) WRONGTYPE Operation against a key holding the wrong kind of value
4) (integer) 11
5) (nil)
6) "11"
3、pipline (管道) (了解)
在Linux上编辑一个文件 command.txt。文件中可以包含一系列的指令
set count 1
incr count
incr count
incr count
然后在客户端执行redis-cli指令时,就可以直接执行这个文件中的指令
[root@192-168-65-214 ~]# cat command.txt | redis-cli -a 123qweasd --pipe
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 4
[root@192-168-65-214 ~]# redis-cli -a 123qweasd
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> get count
"4"
4、lua 脚本 (重点)
Redis中对lua支持从2.6.0版本就已经开始了。具体参考指令可以使用 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: scripting
- script:lua 脚本,这个脚本可以写入多条 redis 语句,redis 执行这些语句时不会插入其他线程的语句,保证了原子性
- numkeys:key 的数量,可以是0
- [key [key …]]:键名值
- [arg [arg …]]:参数值‘
Redisson 大量使用了 lua 保证了 redis 语句的原子性
5、Redis Function
Redis7 之后提供了 Redis Function,即可以把 lua 语句写入一个函数,要使用这个语句,直接调用函数就可以。
示例:
1、在服务器上新增一个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)
2、使用Redis客户端,将这个函数加载到Redis中。
[root@192-168-65-214 myredis]# cat mylib.lua | redis-cli -a 123456 -x FUNCTION LOAD REPLACE
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
"mylib"
3、其他客户端就可以使用这个函数了
127.0.0.1:6379> FUNCTION HELP --这个指令可以查询 FUNCTION 相关命令
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"