深入理解Redis线程模型

52 阅读11分钟

Redis是什么

Redis 全称 REmote DIctionary Server,远程字典服务,是⼀个完全开源的,⾼性能的Key-Value数据库。官网地址: redis.io/

核⼼总结:

1.数据结构复杂。

Redis相⽐于传统的K-V型数据库,能够支撑更更复杂的数据类型。这意味着Redis已经远远超出了缓存的范围,可以实现很多复杂的业务场景。并且还在不断发展更多的业务场景。

2.数据保存在内存,但是持久化到硬盘。

数据全部保存在内存,意味着Redis进⾏数据读和写的性能⾮常⾼。是集中式缓存的不二之选。 数据持久化到硬盘,意味着Redis上保存的数据是非常安全的。⽬前Redis完全可以当做⼀个数据库来⽤。 所以,官⽅对Redis的作⽤,也已经定位成了三个⽅⾯:Cache(缓存),Database(数据库),Vector Search(向量搜索)

在2023年之前,Redis是⼀个纯粹的开源数据库。但是,在最近的这两年,Redis正在进行华丽的蜕变。从⼀个缓存产品变成⼀整套⽣态服务。

图片.png

其中,Redis Cloud是⼀套云服务,基于AWS,Azure等公有云,提供了⼀整套完整的企业服务。 并提供了Redis Enterprise,企业级的收费产品服务。

Redis Insight是⼀套Redis服务的安装及管理套件。可以简单理解为是Redis官⽅推出的⼀个图形化客户端。以往使⽤Redis都需要寻找各种第三⽅的客户端,现在不⽤了。并且Redis Insight也可以在Redis Cloud上直接使⽤。

⽽在功能层⾯。目前已经形成了Redis OSS和Redis Stack两套服务体系。 其中Redis OSS就是以前常⽤的开源的服务体系。⽽Redis Stack可以认为是基于Redis OSS打造的⼀套更完整的技术栈。基于Redis Cloud提供服务,在Redis OSS功能的基础上,提供了很多⾼级的扩展功能。

Redis到底是单线程还是多线程?

⾸先:整体来说,Redis的整体线程模型可以简单解释为 客户端多线程,服务端单线程

Redis为了能够与更多的客户端进⾏连接,还是使⽤的多线程来维护与客户端的Socket连接。在redis.conf中就有⼀个参数maxclients维护了最⼤的客户端连接数

但是,在服务端,Redis响应⽹络IO和键值对读写的请求,则是由⼀个单独的主线程完成的。Redis基于epoll实现了IO多路复⽤,这就可以⽤⼀个主线程同时响应多个客户端Socket连接的请求。 图片.png

在这种线程模型下,Redis将客户端多个并发的请求转成了串⾏的执⾏⽅式。因此,在Redis中,完全不⽤考虑诸如MySQL的脏读、幻读、不可重复读之类的并发问题。并且,这种串⾏化的线程模型,加上Redis基于内存⼯作的极⾼性能,也让Redis成为很多并发问题的解决工具。

然后,严格来说,Redis后端的线程模型跟Redis的版本是有关系的。

Redis4.X以前的版本,都是采⽤的纯单线程。但是在2018年10月份,Redis5.x版本进⾏了⼀次⼤的核⼼代码重构。 到Redis6.x和7.x版本中,开始⽤⼀种全新的多线程机制来提升后台工作。尤其在现在的Redis7.x版本中,Redis后端的很多比较费时的操作,⽐如持久化RDB,AOF⽂件、unlink异步删除、集群数据同步等,都是由额外的线程执⾏的。例如,对于 FLUSHALL操作,就已经提供了异步的⽅式。

Redis如何保证指令原⼦性

对于核⼼的读写键值的操作,Redis是单线程处理的。如果多个客户端同时进⾏读写请求,Redis只会排队串⾏。也就是说,针对单个客户端,Redis并没有类似MySQL的事务那样保证同⼀个客户端的操作原⼦性。像下⾯这种情况,返回的k1的值,就很难确定。

如何控制Redis指令的原⼦性呢?这是⼀系列的问题,在不同的业务场景下,Redis也提供了不同的思路。我们需要在项⽬中能够灵活选择。

1、复合指令

Redis内部提供了很多复合指令,他们是⼀个指令,可是明显⼲着多个指令的活。⽐如 MSET(HMSET)、GETSET、SETNX、SETEX。这些复合指令都能很好的保持原⼦性。

2、Redis事务

像MySQL⼀样,Redis也提供了事务机制。 图片.png 使⽤⽅式也很典型,开启事务后,接⼊⼀系列操作,然后根据执⾏情况选择是执⾏事务还是回滚事务。例如 图片.png 但是,这和数据库中的事务,是不是同一回事呢?看下⾯这个例⼦。 图片.png lpop指令是针对list的操作,现在针对string类型的k2操作,那么肯定会报错。 但是,结果是这⾏错误的指令并没有让整个事务回滚,甚⾄后⾯的指令都没有受到影响。

从这⾥可以看到。Redis的事务并不是像数据库的事务那样,保证事务中的指令⼀起成功或者⼀起失败。Redis的事务作⽤,仅仅只是保证事务中的原⼦操作是⼀起执⾏,⽽不会在执⾏过程中被其他指令加塞。

核⼼重点做⼏个总结: 1、Redis事务可以通过Watch机制进一步保证在某个事务执⾏前,某⼀个key不被修改。

图片.png

2、Redis事务失败如何回滚

Redis中的事务回滚,不是回滚数据,⽽是回滚操作。

1》如果事务是在EXEC执⾏前失败(⽐如事务中的指令敲错了,或者指令的参数不对),那么整个事务的操作都不会执⾏。

2》如果事务是在EXEC执⾏之后失败(⽐如指令操作的key类型不对),那么事务中的其他操作都会正常执行,不受影响。

3、事务执⾏过程中出现失败了怎么办

1》只要客户端执⾏了EXEC指令,那么就算之后客户端的连接断开了,事务就会⼀直进⾏下去。

2》事务有可能造成数据不一致。当EXEC指令执⾏后,Redis会先将事务中的所有操作都先记录到AOF⽂件中,然后再执⾏具体的操作。这时有⼀种可能,Redis保存了AOF记录后,事务的操作在执⾏过程中,服务就出现了⾮正常宕机(服务崩溃了,或者执⾏进程被kill -9了)。这就会造成AOF中记录的操作,与数据不符合。如果Redis发现这种情况,那么在下次服务启动时,就会出现错误,⽆法正常启动。这时,就要使⽤redis-check-aof⼯具修复AOF⽂件,将这些不完整的事务操作记录移除掉。这样下次服务就可以正常启动了。

4、事务机制优缺点,什么时候⽤事务

!!!没有标准答案,⾃⾏总结。

lua脚本

Redis的事务和Pipelinie机制,对于Redis的指令原⼦性问题,都有⼀定的帮助,但是,从之前的分析可以看到,这两个机制对于指令原⼦性问题都有⽔⼟不服的地⽅。并且,他们都只是对Redis现有指令进⾏拼凑,⽆法添加更多⾃定义的复杂逻辑。因此,企业中⽤到更多的是lua脚本。同时,也是Redis7版本着重调整的⼀个功能。

什么是lua?为什么Redis⽀持lua?

lua是⼀种⼩巧的脚本语⾔,他拥有很多⾼级语⾔的特性。⽐如参数类型、作⽤域、函数等。lua的语法非常简单,熟悉Java后基本上可以零⻔槛上⼿lua。

lua语⾔最⼤的特点是他的线程模型是单线程的模式。这使得lua天⽣就⾮常适合⼀些单线程模型的中间件。⽐如Redis,Nginx等都非常适合接⼊lua语⾔进⾏功能定制。所以,在Redis中执⾏⼀段lua脚本,天然就是原⼦性的。

Redis中如何执⾏lua?

Redis中对lua语⾔的API介绍参考官⽹:

redis.io/docs/latest…

Redis中对lua⽀持从2.6.0版本就已经开始了。具体参考指令可以使⽤ help eval指令查看

图片.png

  • script参数是⼀段Lua脚本程序,它会被运⾏在Redis服务器上下⽂中,这段脚本不必(也不应该)定义为⼀ 个Lua函数。
  • numkeys参数⽤于指定键名参数的个数。键名参数 key [key ...] 从EVAL的第三个参数开始算 起,表示在脚本中所⽤到的那些Redis键(key),这些键名参数可以在 Lua中通过全局变量KEYS数组,⽤1 为基址的形式访问( KEYS[1] ,KEYS[2] ,以此类推)。
  • 在命令的最后,那些不是键名参数的附加参数 arg [arg ...] ,可以在Lua中通过全局变量ARGV数组访问, 访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2],诸如此类)。

在lua脚本中,可以使⽤redis.call函数来调⽤Redis的命令。

图片.png

图片.png

使⽤lua注意点

1》不要在Lua脚本中出现死循环和耗时的运算,否则redis会阻塞,将不接受其他的命令。相比之下,管道pipeline不会阻塞redis。

Redis中有⼀个配置参数来控制Lua脚本的最⻓控制时间。默认5秒钟。当lua脚本执⾏时间超过了这个时⻓,Redis会对其他操作返回⼀个BUSY错误,⽽不会⼀直阻塞。

2》尽量使⽤只读脚本

只读脚本是Redis7中新增的⼀种脚本执⾏⽅法,表示那些不修改Redis数据集的只读脚本。需要在脚本上加上⼀个只读的标志,并通过指令EVAL_RO触发。在只读脚本中不允许执⾏任何修改数据集的操作,并且可以随时使⽤SCRIPT_KILL指令停⽌。

使⽤只读脚本的好处⼀⽅⾯在于可以限制某些⽤户的操作。另⼀⽅⾯,这些只读脚本通常都可以转移到备份节点执⾏,从⽽减轻Redis的压力。

3》热点脚本可以缓存到服务端

Redis Function

如果你觉得开发lua脚本有困难,那么在Redis7之后,提供了另外⼀种让程序员解脱的方法-Redis Function。

Redis Function允许将⼀些功能声明成⼀个统⼀的函数,提前加载到Redis服务端(可以由熟悉Redis的管理员加载)。客户端可以直接调⽤这些函数,⽽不需要再去开发函数的具体实现。

Redis Function更⼤的好处在于在Function中可以嵌套调⽤其他Function,从⽽更有利于代码复⽤。相⽐之下,lua脚本就⽆法进⾏复⽤。

Function案例

例如,可以在服务器上新增⼀个mylib.lua⽂件。在⽂件中定义函数。

图片.png 然后,就可以使用Redis客户端,将这个函数加载到Redis中。

图片.png

3、Function注意点

1》Function同样也可以进⾏只读调⽤。

2》如果在集群中使⽤Function,⽬前版本需要在各个节点都⼿动加载⼀次。Redis不会在集群中进⾏Function同步

3》Function是要在服务端缓存的,所以不建议使⽤太多太⼤的Function。

4》Function和Script⼀样,也有⼀系列的管理指令。使⽤指令 help @scripting ⾃⾏了解。

Redis指令原⼦性总结

以上介绍的各种机制,其实都是Redis改变指令执⾏顺序的⽅式。在这⼏种⼯具中,Lua脚本通常会是项⽬中⽤得最多的⽅式。在很多追求极致性能的⾼并发场景,Lua脚本都会担任很重要的⻆⾊。但是其他的各种⽅式你也需要有了解,这样⾯临真实业务场景,你才有更多的方案可以选择。

Redis中的Bigkey问题

Bigkey指那些占⽤空间⾮常⼤的key。⽐如⼀个list中包含200W个元素,或者⼀个string⾥放⼀篇⽂章。基于Redis的单线程为主的核⼼⼯作机制,这些Bigkey⾮常容易造成Redis的服务阻塞。因此在实际项⽬中,⼀定需要特殊关照。

在Redis客户端指令中,提供了两个扩展参数,可以帮助快速发现这些BigKey

图片.png

Redis线程模型总结

Redis的线程模型整体还是多线程的,只是后台执⾏指令的核⼼线程是单线程的。整个线程模型可以理解为还是以单线程为主。基于这种单线程为主的线程模型,不同客户端的各种指令都需要依次排队执行。

Redis这种以单线程为主的线程模型,相⽐其他中间件,还是非常简单的。这使得Redis处理线程并发问题,要简单高效很多。甚至在很多复杂业务场景下,Redis都是⽤来进⾏线程并发控制的很好的工具。但是,这并不意味着Redis就没有线程并发的问题。这时候选择合理的指令执⾏⽅式,就⾮常重要了。

另外,Redis这种比较简单的线程模型其实本身是不利于发挥多线程的并发优势的。⽽且Redis的应⽤场景⼜通常与⾼性能深度绑定在⼀起,所以,在使⽤Redis的时候,还是要时刻思考Redis的这些指令执⾏⽅式,这样才能最⼤限度发挥Redis⾼性能的优势。