深入理解Redis线程模型
1、Redis到底是单线程还是多线程?
Redis为了能够与更多的客户端进⾏连接,还是使⽤的多线程来维护与客户端的 Socket连接。在redis.conf中就有⼀个参数maxclients维护了最⼤的客户端连接数。但是,在服务端,Redis响应⽹络IO和键值对读写的请求,则是由⼀个单独的主线程 完成的。Redis基于epoll实现了IO多路复⽤,这就可以⽤⼀个主线程同时响应多个 客户端Socket连接的请求。

在这种线程模型下,Redis将客户端多个并发的请求转成了串⾏的执⾏⽅式。因此, 在Redis中,完全不⽤考虑诸如MySQL的脏读、幻读、不可重复读之类的并发问 题。并且,这种串⾏化的线程模型,加上Redis基于内存⼯作的极⾼性能,也让 Redis成为很多并发问题的解决⼯具。
然后,严格来说,Redis后端的线程模型跟Redis的版本是有关系的。
Redis4.X以前的版本,都是采⽤的纯单线程。到Redis6.x和7.x版本中,开始⽤⼀种全新的多线程机制来提升后台⼯作。尤其在现在的Redis7.x版本中,Redis后端的很多⽐较费时的操作,⽐如持久化RDB,AOF⽂件、unlink异步删除、集群数据同步等,都是由额外的线程执⾏的。例如,对于 FLUSHALL操作,就已经提供了异步的⽅式。
2、Redis如何保证指令原⼦性
对于核⼼的读写键值的操作,Redis是单线程处理的。如果多个客户端同时进⾏读写请求,Redis只会排队串⾏。也就是说,针对单个客户端,Redis并没有类似MySQL的事务那样保证同⼀个客户端的操作原⼦性。
在不同的业务场景下,Redis也提供了不同的思路。我们需要在项⽬中能够灵活选择。
1、复合指令
Redis内部提供了很多复合指令,他们是⼀个指令,可是明显⼲着多个指令的活。⽐如 MSET(HMSET)、GETSET、SETNX、SETEX。这些复合指令都能很好的保持原⼦性。
2、Redis事务
MULTI (null) -- 开启事务
EXEC (null) -- 执⾏事务
WATCH key [key ...] --监听某⼀个key的变化
UNWATCH (null) --去掉监听
redis事务并不会像mysql那样回滚数据,甚⾄报错后⾯的指令都没有受到影响。Redis的事务作⽤,仅仅只是保证事务中的命令原⼦操作是⼀起执⾏,⽽不会在执⾏过程中被其他指令加塞。开启事务后,所有操作的返回结果都是QUEUED,表示这些操作只是排好了队,等到EXEC后⼀起执⾏。
Redis事务可以通过Watch机制进⼀步保证在某个事务执⾏前,某⼀个key不被修改。

注意:UNWATCH取消监听,只在当前客户端有效。⽐如下图。 只有在左侧客户端步骤3之前执⾏UNWATCH才能让事务执⾏成功。在右侧客户端执⾏UNWATCH是不⽣效的。
2、Redis事务失败如何回滚?
- 如果事务是在EXEC执⾏前失败(⽐如事务中的指令敲错了,或者指令的参数不对),那么整个事务的操作都不会执⾏。
- 如果事务是在EXEC执⾏之后失败(⽐如指令操作的key类型不对),那么事务中的其他操作都会正常执⾏,不受影响。
3、Pipeline
如果你有⼤批量的数据需要快速写⼊到Redis中,这种⽅式可以⼀定程度提⾼执⾏效率。核⼼作⽤:优化RTT(round-trip time)。
当客户端执⾏⼀个指令,数据包需要通过⽹络从Client传到Server,然后再从Server返回到Client。这个中间的时间消耗,就称为RTT(Rount Trip Time)。
pipeline就是将客户端的多个指令打包,⼀起往服务端推送。但是pipeline不具备原⼦性。pipeline只是将多条命令发送到服务端,最终还是可能会被其他客户端的指令加塞的。 pipeline在执行过程中会阻塞当前客户端,因此不建议复杂耗时指令执行。
4、lua脚本
在线调试Lua脚本:https://wiki.luatos.com/
lua是⼀种单线程的模式⼩巧的脚本语⾔,所以,在Redis中执⾏⼀段lua脚本,天然就是原⼦性的。
# 执行lua脚本
EVAL script numkeys [key [key ...]] [arg [arg ...]]
- script参数是⼀段Lua脚本程序,它会被运⾏在Redis服务器上下⽂中,这段脚本不必(也不应该)定义为⼀ 个Lua函数。
- numkeys参数⽤于指定键名参数的个数。键名参数 key [key ...]
- Lua中通过全局变量KEYS数组,⽤1 为基址的形式访问( KEYS[1] ,KEYS[2] ,以此类推)。
- 参数 arg [arg ...] ,可以在Lua中通过全局变量ARGV数组访问, 访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2],诸如此类)。
在lua脚本中,可以使⽤redis.call函数来调⽤Redis的命令
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
不要在Lua脚本中出现死循环和耗时的运算,否则redis会阻塞,将不接受其他的命令。相⽐之下,管道pipeline不会阻塞redis。Redis中有⼀个配置参数来控制Lua脚本的最⻓控制时间。默认5秒钟。当lua脚本执⾏时间超过了这个时⻓,Redis会对其他操作返回⼀个BUSY错误,⽽不会⼀直阻塞。

浙公网安备 33010602011771号