This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,187 @@
<audio id="audio" title="39 | Redis 6.0的新特性:多线程、客户端缓存与安全" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a9/7b/a9f6891ca274e7155b4826beceebc57b.mp3"></audio>
你好,我是蒋德钧。
Redis官方在今年5月份正式推出了6.0版本这个版本中有很多的新特性。所以6.0刚刚推出,就受到了业界的广泛关注。
所以在课程的最后我特意安排了这节课想来和你聊聊Redis 6.0中的几个关键新特性分别是面向网络处理的多IO线程、客户端缓存、细粒度的权限控制以及RESP 3协议的使用。
其中面向网络处理的多IO线程可以提高网络请求处理的速度而客户端缓存可以让应用直接在客户端本地读取数据这两个特性可以提升Redis的性能。除此之外细粒度权限控制让Redis可以按照命令粒度控制不同用户的访问权限加强了Redis的安全保护。RESP 3协议则增强客户端的功能可以让应用更加方便地使用Redis的不同数据类型。
只有详细掌握了这些特性的原理你才能更好地判断是否使用6.0版本。如果你已经在使用6.0了,也可以看看怎么才能用得更好,少踩坑。
首先我们来了解下6.0版本中新出的多线程特性。
## 从单线程处理网络请求到多线程处理
**在Redis 6.0中,非常受关注的第一个新特性就是多线程**。这是因为Redis一直被大家熟知的就是它的单线程架构虽然有些命令操作可以用后台线程或子进程执行比如数据删除、快照生成、AOF重写但是从网络IO处理到实际的读写命令处理都是由单个线程完成的。
随着网络硬件的性能提升Redis的性能瓶颈有时会出现在网络IO的处理上也就是说**单个主线程处理网络请求的速度跟不上底层网络硬件的速度**。
为了应对这个问题,一般有两种方法。
第一种方法是用用户态网络协议栈例如DPDK取代内核网络协议栈让网络请求的处理不用在内核里执行直接在用户态完成处理就行。
对于高性能的Redis来说避免频繁让内核进行网络请求处理可以很好地提升请求处理效率。但是这个方法要求在Redis的整体架构中添加对用户态网络协议栈的支持需要修改Redis源码中和网络相关的部分例如修改所有的网络收发请求函数这会带来很多开发工作量。而且新增代码还可能引入新Bug导致系统不稳定。所以Redis 6.0中并没有采用这个方法。
第二种方法就是采用多个IO线程来处理网络请求提高网络请求处理的并行度。Redis 6.0就是采用的这种方法。
但是Redis的多IO线程只是用来处理网络请求的对于读写命令Redis仍然使用单线程来处理。这是因为Redis处理请求时网络处理经常是瓶颈通过多个IO线程并行处理网络操作可以提升实例的整体处理性能。而继续使用单线程执行命令操作就不用为了保证Lua脚本、事务的原子性额外开发多线程互斥机制了。这样一来Redis线程模型实现就简单了。
我们来看下在Redis 6.0中主线程和IO线程具体是怎么协作完成请求处理的。掌握了具体原理你才能真正地会用多线程。为了方便你理解我们可以把主线程和多IO线程的协作分成四个阶段。
**阶段一服务端和客户端建立Socket连接并分配处理线程**
首先主线程负责接收建立连接请求。当有客户端请求和实例建立Socket连接时主线程会创建和客户端的连接并把 Socket 放入全局等待队列中。紧接着主线程通过轮询方法把Socket连接分配给IO线程。
**阶段二IO线程读取并解析请求**
主线程一旦把Socket分配给IO线程就会进入阻塞状态等待IO线程完成客户端请求读取和解析。因为有多个IO线程在并行处理所以这个过程很快就可以完成。
**阶段三:主线程执行请求操作**
等到IO线程解析完请求主线程还是会以单线程的方式执行这些命令操作。下面这张图显示了刚才介绍的这三个阶段你可以看下加深理解。
<img src="https://static001.geekbang.org/resource/image/58/cd/5817b7e2085e7c00e63534a07c4182cd.jpg" alt="">
**阶段四IO线程回写Socket和主线程清空全局队列**
当主线程执行完请求操作后会把需要返回的结果写入缓冲区然后主线程会阻塞等待IO线程把这些结果回写到Socket中并返回给客户端。
和IO线程读取和解析请求一样IO线程回写Socket时也是有多个线程在并发执行所以回写Socket的速度也很快。等到IO线程回写Socket完毕主线程会清空全局队列等待客户端的后续请求。
我也画了一张图展示了这个阶段主线程和IO线程的操作你可以看下。
<img src="https://static001.geekbang.org/resource/image/2e/1b/2e1f3a5bafc43880e935aaa4796d131b.jpg" alt="">
了解了Redis主线程和多线程的协作方式我们该怎么启用多线程呢在Redis 6.0中多线程机制默认是关闭的如果需要使用多线程功能需要在redis.conf中完成两个设置。
**1.设置io-thread-do-reads配置项为yes表示启用多线程。**
```
io-threads-do-reads yes
```
2.设置线程个数。一般来说,**线程个数要小于Redis实例所在机器的CPU核个数**例如对于一个8核的机器来说Redis官方建议配置6个IO线程。
```
io-threads 6
```
如果你在实际应用中发现Redis实例的CPU开销不大吞吐量却没有提升可以考虑使用Redis 6.0的多线程机制,加速网络处理,进而提升实例的吞吐量。
## 实现服务端协助的客户端缓存
和之前的版本相比Redis 6.0新增了一个重要的特性就是实现了服务端协助的客户端缓存功能也称为跟踪Tracking功能。有了这个功能业务应用中的Redis客户端就可以把读取的数据缓存在业务应用本地了应用就可以直接在本地快速读取数据了。
不过,当把数据缓存在客户端本地时,我们会面临一个问题:**如果数据被修改了或是失效了,如何通知客户端对缓存的数据做失效处理?**
6.0实现的Tracking功能实现了两种模式来解决这个问题。
**第一种模式是普通模式**。在这个模式下实例会在服务端记录客户端读取过的key并监测key是否有修改。一旦key的值发生变化服务端会给客户端发送invalidate消息通知客户端缓存失效了。
在使用普通模式时有一点你需要注意一下服务端对于记录的key只会报告一次invalidate消息也就是说服务端在给客户端发送过一次invalidate消息后如果key再被修改此时服务端就不会再次给客户端发送invalidate消息。
只有当客户端再次执行读命令时服务端才会再次监测被读取的key并在key修改时发送invalidate消息。这样设计的考虑是节省有限的内存空间。毕竟如果客户端不再访问这个key了而服务端仍然记录key的修改情况就会浪费内存资源。
我们可以通过执行下面的命令打开或关闭普通模式下的Tracking功能。
```
CLIENT TRACKING ON|OFF
```
**第二种模式是广播模式**。在这个模式下服务端会给客户端广播所有key的失效情况不过这样做了之后如果key 被频繁修改,服务端会发送大量的失效广播消息,这就会消耗大量的网络带宽资源。
所以在实际应用时我们会让客户端注册希望跟踪的key的前缀当带有注册前缀的key被修改时服务端会把失效消息广播给所有注册的客户端。**和普通模式不同在广播模式下即使客户端还没有读取过key但只要它注册了要跟踪的key服务端都会把key失效消息通知给这个客户端**。
我给你举个例子带你看一下客户端如何使用广播模式接收key失效消息。当我们在客户端执行下面的命令后如果服务端更新了user:id:1003这个key那么客户端就会收到invalidate消息。
```
CLIENT TRACKING ON BCAST PREFIX user
```
这种监测带有前缀的key的广播模式和我们对key的命名规范非常匹配。我们在实际应用时会给同一业务下的key设置相同的业务名前缀所以我们就可以非常方便地使用广播模式。
不过刚才介绍的普通模式和广播模式需要客户端使用RESP 3协议RESP 3协议是6.0新启用的通信协议,一会儿我会给你具体介绍。
对于使用RESP 2协议的客户端来说就需要使用另一种模式也就是重定向模式redirect。在重定向模式下想要获得失效消息通知的客户端就需要执行订阅命令SUBSCRIBE专门订阅用于发送失效消息的频道_redis_:invalidate。同时再使用另外一个客户端执行CLIENT TRACKING命令设置服务端将失效消息转发给使用RESP 2协议的客户端。
我再给你举个例子带你了解下如何让使用RESP 2协议的客户端也能接受失效消息。假设客户端B想要获取失效消息但是客户端B只支持RESP 2协议客户端A支持RESP 3协议。我们可以分别在客户端B和A上执行SUBSCRIBE和CLIENT TRACKING如下所示
```
//客户端B执行客户端B的ID号是303
SUBSCRIBE _redis_:invalidate
//客户端A执行
CLIENT TRACKING ON BCAST REDIRECT 303
```
这样设置以后如果有键值对被修改了客户端B就可以通过_redis_:invalidate频道获得失效消息了。
好了了解了6.0 版本中的客户端缓存特性后我们再来了解下第三个关键特性也就是实例的访问权限控制列表功能Access Control ListACL这个特性可以有效地提升Redis的使用安全性。
## 从简单的基于密码访问到细粒度的权限控制
在Redis 6.0 版本之前,要想实现实例的安全访问,只能通过设置密码来控制,例如,客户端连接实例前需要输入密码。
此外对于一些高风险的命令例如KEYS、FLUSHDB、FLUSHALL等在Redis 6.0 之前我们也只能通过rename-command来重新命名这些命令避免客户端直接调用。
Redis 6.0 提供了更加细粒度的访问权限控制,这主要有两方面的体现。
**首先6.0版本支持创建不同用户来使用Redis**。在6.0版本前所有客户端可以使用同一个密码进行登录使用但是没有用户的概念而在6.0中我们可以使用ACL SETUSER命令创建用户。例如我们可以执行下面的命令创建并启用一个用户normaluser把它的密码设置为“abc”
```
ACL SETUSER normaluser on &gt; abc
```
**另外6.0版本还支持以用户为粒度设置命令操作的访问权限**。我把具体操作列在了下表中,你可以看下,其中,加号(+)和减号(-)就分别表示给用户赋予或撤销命令的调用权限。
<img src="https://static001.geekbang.org/resource/image/d1/c8/d1bd6891934cfa879ee080de1c5455c8.jpg" alt="">
为了便于你理解我给你举个例子。假设我们要设置用户normaluser只能调用Hash类型的命令操作而不能调用String类型的命令操作我们可以执行如下命令
```
ACL SETUSER normaluser +@hash -@string
```
除了设置某个命令或某类命令的访问控制权限6.0版本还支持以key为粒度设置访问权限。
具体的做法是使用波浪号“~”和key的前缀来表示控制访问的key。例如我们执行下面命令就可以设置用户normaluser只能对以“user:”为前缀的key进行命令操作
```
ACL SETUSER normaluser ~user:* +@all
```
好了到这里你了解了Redis 6.0可以设置不同用户来访问实例而且可以基于用户和key的粒度设置某个用户对某些key允许或禁止执行的命令操作。
这样一来我们在有多用户的Redis应用场景下就可以非常方便和灵活地为不同用户设置不同级别的命令操作权限了这对于提供安全的Redis访问非常有帮助。
## 启用RESP 3协议
Redis 6.0实现了RESP 3通信协议而之前都是使用的RESP 2。在RESP 2中客户端和服务器端的通信内容都是以字节数组形式进行编码的客户端需要根据操作的命令或是数据类型自行对传输的数据进行解码增加了客户端开发复杂度。
而RESP 3直接支持多种数据类型的区分编码包括空值、浮点数、布尔值、有序的字典集合、无序的集合等。
所谓区分编码就是指直接通过不同的开头字符区分不同的数据类型这样一来客户端就可以直接通过判断传递消息的开头字符来实现数据转换操作了提升了客户端的效率。除此之外RESP 3协议还可以支持客户端以普通模式和广播模式实现客户端缓存。
## 小结
这节课我向你介绍了Redis 6.0的新特性,我把这些新特性总结在了一张表里,你可以再回顾巩固下。
<img src="https://static001.geekbang.org/resource/image/21/f0/2155c01bf3129d5d58fcb98aefd402f0.jpg" alt="">
最后我也再给你一个小建议因为Redis 6.0是刚刚推出的新的功能特性还需要在实际应用中进行部署和验证所以如果你想试用Redis 6.0可以尝试先在非核心业务上使用Redis 6.0,一方面可以验证新特性带来的性能或功能优势,另一方面,也可以避免因为新特性不稳定而导致核心业务受到影响。
## 每课一问
你觉得Redis 6.0的哪个或哪些新特性会对你有帮助呢?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事。我们下节课见。

View File

@@ -0,0 +1,124 @@
<audio id="audio" title="40 | Redis的下一步基于NVM内存的实践" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e1/29/e1cdc414c24a5f14c47006a774702129.mp3"></audio>
你好,我是蒋德钧。
今天这节课是咱们课程的最后一节课了我们来聊聊Redis的下一步发展。
这几年呢新型非易失存储Non-Volatile MemoryNVM器件发展得非常快。NVM器件具有容量大、性能快、能持久化保存数据的特性这些刚好就是Redis追求的目标。同时NVM器件像DRAM一样可以让软件以字节粒度进行寻址访问所以在实际应用中NVM可以作为内存来使用我们称为NVM内存。
你肯定会想到Redis作为内存键值数据库如果能和NVM内存结合起来使用就可以充分享受到这些特性。我认为Redis发展的下一步就可以基于NVM内存来实现大容量实例或者是实现快速持久化数据和恢复。这节课我就带你了解下这个新趋势。
接下来我们先来学习下NVM内存的特性以及软件使用NVM内存的两种模式。在不同的使用模式下软件能用到的NVM特性是不一样的所以掌握这部分知识可以帮助我们更好地根据业务需求选择适合的模式。
## NVM内存的特性与使用模式
Redis是基于DRAM内存的键值数据库而跟传统的DRAM内存相比NVM有三个显著的特点。
首先,**NVM内存最大的优势是可以直接持久化保存数据**。也就是说数据保存在NVM内存上后即使发生了宕机或是掉电数据仍然存在NVM内存上。但如果数据是保存在DRAM上那么掉电后数据就会丢失。
其次,**NVM内存的访问速度接近DRAM的速度**。我实际测试过NVM内存的访问速度结果显示它的读延迟大约是200~300ns而写延迟大约是100ns。在读写带宽方面单根NVM内存条的写带宽大约是1~2GB/s而读带宽约是5~6GB/s。当软件系统把数据保存在NVM内存上时系统仍然可以快速地存取数据。
最后,**NVM内存的容量很大**。这是因为NVM器件的密度大单个NVM的存储单元可以保存更多数据。例如单根NVM内存条就能达到128GB的容量最大可以达到512GB而单根DRAM内存条通常是16GB或32GB。所以我们可以很轻松地用NVM内存构建TB级别的内存。
总结来说NVM内存的特点可以用三句话概括
- 能持久化保存数据;
- 读写速度和DRAM接近
- 容量大。
现在业界已经有了实际的NVM内存产品就是Intel在2019年4月份时推出的Optane AEP内存条简称AEP内存。我们在应用AEP内存时需要注意的是AEP内存给软件提供了两种使用模式分别对应着使用了NVM的容量大和持久化保存数据两个特性我们来学习下这两种模式。
第一种是Memory模式。
这种模式是把NVM内存作为大容量内存来使用的也就是说只使用NVM容量大和性能高的特性没有启用数据持久化的功能。
例如我们可以在一台服务器上安装6根NVM内存条每根512GB这样我们就可以在单台服务器上获得3TB的内存容量了。
在Memory模式下服务器上仍然需要配置DRAM内存但是DRAM内存是被CPU用作AEP内存的缓存DRAM的空间对应用软件不可见。换句话说**软件系统能使用到的内存空间就是AEP内存条的空间容量**。
第二种是App Direct模式。
这种模式启用了NVM持久化数据的功能。在这种模式下应用软件把数据写到AEP内存上时数据就直接持久化保存下来了。所以使用了App Direct模式的AEP内存也叫做持久化内存Persistent MemoryPM
现在呢我们知道了AEP内存的两种使用模式那Redis是怎么用的呢我来给你具体解释一下。
## 基于NVM内存的Redis实践
当AEP内存使用Memory模式时应用软件就可以利用它的大容量特性来保存大量数据Redis也就可以给上层业务应用提供大容量的实例了。而且在Memory模式下Redis可以像在DRAM内存上运行一样直接在AEP内存上运行不用修改代码。
不过有个地方需要注意下在Memory模式下AEP内存的访问延迟会比DRAM高一点。我刚刚提到过NVM的读延迟大约是200~300ns而写延迟大约是100ns。所以在Memory模式下运行Redis实例实例读性能会有所降低我们就需要在保存大量数据和读性能较慢两者之间做个取舍。
那么当我们使用App Direct模式把AEP内存用作PM时Redis又该如何利用PM快速持久化数据的特性呢这就和Redis的数据可靠性保证需求和现有机制有关了我们来具体分析下。
为了保证数据可靠性Redis设计了RDB和AOF两种机制把数据持久化保存到硬盘上。
但是无论是RDB还是AOF都需要把数据或命令操作以文件的形式写到硬盘上。对于RDB来说虽然Redis实例可以通过子进程生成RDB文件但是实例主线程fork子进程时仍然会阻塞主线程。而且RDB文件的生成需要经过文件系统文件本身会有一定的操作开销。
对于AOF日志来说虽然Redis提供了always、everysec和no三个选项其中always选项以fsync的方式落盘保存数据虽然保证了数据的可靠性但是面临性能损失的风险。everysec选项避免了每个操作都要实时落盘改为后台每秒定期落盘。在这种情况下Redis的写性能得到了改善但是应用会面临秒级数据丢失的风险。
此外当我们使用RDB文件或AOF文件对Redis进行恢复时需要把RDB文件加载到内存中或者是回放AOF中的日志操作。这个恢复过程的效率受到RDB文件大小和AOF文件中的日志操作多少的影响。
所以在前面的课程里我也经常提醒你不要让单个Redis实例过大否则会导致RDB文件过大。在主从集群应用中过大的RDB文件就会导致低效的主从同步。
我们先简单小结下现在Redis在涉及持久化操作时的问题
- RDB文件创建时的fork操作会阻塞主线程
- AOF文件记录日志时需要在数据可靠性和写性能之间取得平衡
- 使用RDB或AOF恢复数据时恢复效率受RDB和AOF大小的限制。
但是如果我们使用持久化内存就可以充分利用PM快速持久化的特点来避免RDB和AOF的操作。因为PM支持内存访问而Redis的操作都是内存操作那么我们就可以把Redis直接运行在PM上。同时数据本身就可以在PM上持久化保存了我们就不再需要额外的RDB或AOF日志机制来保证数据可靠性了。
那么当使用PM来支持Redis的持久化操作时我们具体该如何实现呢
我先介绍下PM的使用方法。
当服务器中部署了PM后我们可以在操作系统的/dev目录下看到一个PM设备如下所示
```
/dev/pmem0
```
然后我们需要使用ext4-dax文件系统来格式化这个设备
```
mkfs.ext4 /dev/pmem0
```
接着,我们把这个格式化好的设备,挂载到服务器上的一个目录下:
```
mount -o dax /dev/pmem0 /mnt/pmem0
```
此时我们就可以在这个目录下创建文件了。创建好了以后再把这些文件通过内存映射mmap的方式映射到Redis的进程空间。这样一来我们就可以把Redis接收到的数据直接保存到映射的内存空间上了而这块内存空间是由PM提供的。所以数据写入这块空间时就可以直接被持久化保存了。
而且如果要修改或删除数据PM本身也支持以字节粒度进行数据访问所以Redis可以直接在PM上修改或删除数据。
如果发生了实例故障Redis宕机了因为数据本身已经持久化保存在PM上了所以我们可以直接使用PM上的数据进行实例恢复而不用再像现在的Redis那样通过加载RDB文件或是重放AOF日志操作来恢复了可以实现快速的故障恢复。
当然因为PM的读写速度比DRAM慢所以**如果使用PM来运行Redis需要评估下PM提供的访问延迟和访问带宽是否能满足业务层的需求**。
我给你举个例子带你看下如何评估PM带宽对Redis业务的支撑。
假设业务层需要支持1百万QPS平均每个请求的大小是2KB那么就需要机器能支持2GB/s的带宽1百万请求操作每秒 * 2KB每请求 = 2GB/s。如果这些请求正好是写操作的话那么单根PM的写带宽可能不太够用了。
这个时候我们就可以在一台服务器上使用多根PM内存条来支撑高带宽的需求。当然我们也可以使用切片集群把数据分散保存到多个实例分担访问压力。
好了到这里我们就掌握了用PM将Redis数据直接持久化保存在内存上的方法。现在我们既可以在单个实例上使用大容量的PM保存更多的业务数据了同时也可以在实例故障后直接使用PM上保存的数据进行故障恢复。
## 小结
这节课我向你介绍了NVM的三大特点性能高、容量大、数据可以持久化保存。软件系统可以像访问传统DRAM内存一样访问NVM内存。目前Intel已经推出了NVM内存产品Optane AEP。
这款NVM内存产品给软件提供了两种使用模式分别是Memory模式和App Direct模式。在Memory模式时Redis可以利用NVM容量大的特点实现大容量实例保存更多数据。在使用App Direct模式时Redis可以直接在持久化内存上进行数据读写在这种情况下Redis不用再使用RDB或AOF文件了数据在机器掉电后也不会丢失。而且实例可以直接使用持久化内存上的数据进行恢复恢复速度特别快。
NVM内存是近年来存储设备领域中一个非常大的变化它既能持久化保存数据还能像内存一样快速访问这必然会给当前基于DRAM和硬盘的系统软件优化带来新的机遇。现在很多互联网大厂已经开始使用NVM内存了希望你能够关注这个重要趋势为未来的发展做好准备。
## 每课一问
按照惯例我给你提个小问题你觉得有了持久化内存后还需要Redis主从集群吗?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事。

View File

@@ -0,0 +1,134 @@
<audio id="audio" title="41 | 第3540讲课后思考题答案及常见问题答疑" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/59/22/5953267ce8bf69ac50e74488b55f5222.mp3"></audio>
你好,我是蒋德钧。
今天是我们最后一节答疑课我会带你一起分析一下第3540讲的课后思考题。同时我还会讲解两个典型问题分别是原子操作使用问题以及Redis和其他键值数据库的对比情况。
## [第35讲](https://time.geekbang.org/column/article/306548)
问题假设Codis集群中保存的80%的键值对都是Hash类型每个Hash集合的元素数量在10万~20万个每个集合元素的大小是2KB。你觉得迁移这样的Hash集合数据会对Codis的性能造成影响吗
答案其实影响不大。虽然一个Hash集合数据的总数据量有200MB ~ 400MB2KB * 0.1M ≈ 200MB到 2KB * 0.2M ≈ 400MB但是Codis支持异步、分批迁移数据所以Codis可以把集合中的元素分多个批次进行迁移每批次迁移的数据量不大所以不会给源实例造成太大影响。
## [第36讲](https://time.geekbang.org/column/article/307421)
问题假设一个商品的库存量是800我们使用一个包含了4个实例的切片集群来服务秒杀请求我们让每个实例各自维护库存量200把客户端的秒杀请求分发到不同的实例上进行处理你觉得这是一个好方法吗
答案:这个方法是不是能达到一个好的效果,主要取决于,**客户端请求能不能均匀地分发到每个实例上**。如果可以的话,那么,每个实例都可以帮着分担一部分压力,避免压垮单个实例。
在保存商品库存时key一般就是商品的ID所以客户端在秒杀场景中查询同一个商品的库存时会向集群请求相同的key集群就需要把客户端对同一个key的请求均匀地分发到多个实例上。
为了解决这个问题客户端和实例间就需要有代理层来完成请求的转发。例如在Codis中codis proxy负责转发请求那么如果我们让codis proxy收到请求后按轮询的方式把请求分发到不同实例上可以对Codis进行修改增加转发规则就可以利用多实例来分担请求压力了。
如果没有代理层的话客户端会根据key和Slot的映射关系以及Slot和实例的分配关系直接把请求发给保存key的唯一实例了。在这种情况下请求压力就无法由多个实例进行分担了。题目中描述的这个方法也就不能达到好的效果了。
## [第37讲](https://time.geekbang.org/column/article/308393)
问题当有数据访问倾斜时如果热点数据突然过期了假设Redis中的数据是缓存数据的最终值是保存在后端数据库中的这样会发生什么问题吗?
答案:在这种情况下,会发生缓存击穿的问题,也就是热点数据突然失效,导致大量访问请求被发送到数据库,给数据库带来巨大压力。
我们可以采用[第26讲](https://time.geekbang.org/column/article/296586)中介绍的方法,不给热点数据设置过期时间,这样可以避免过期带来的击穿问题。
除此之外,我们最好在数据库的接入层增加流控机制,一旦监测到有大流量请求访问数据库,立刻开启限流,这样做也是为了避免数据库被大流量压力压垮。因为数据库一旦宕机,就会对整个业务应用带来严重影响。所以,我们宁可在请求接入数据库时,就直接拒接请求访问。
## [第38讲](https://time.geekbang.org/column/article/310347)
问题如果我们采用跟Codis保存Slot分配信息相类似的方法把集群实例状态信息和Slot分配信息保存在第三方的存储系统上例如Zookeeper这种方法会对集群规模产生什么影响吗
答案假设我们将Zookeeper作为第三方存储系统保存集群实例状态信息和Slot分配信息那么实例只需要和Zookeeper通信交互信息实例之间就不需要发送大量的心跳消息来同步集群状态了。这种做法可以减少实例之间用于心跳的网络通信量有助于实现大规模集群。而且网络带宽可以集中用在服务客户端请求上。
不过在这种情况下实例获取或更新集群状态信息时都需要和Zookeeper交互Zookeeper的网络通信带宽需求会增加。所以采用这种方法的时候需要给Zookeeper保证一定的网络带宽避免Zookeeper受限于带宽而无法和实例快速通信。
## [第39讲](https://time.geekbang.org/column/article/310838)
问题你觉得Redis 6.0的哪个或哪些新特性会对你有帮助呢?
答案这个要根据你们的具体需求来定。从提升性能的角度上来说Redis 6.0中的多IO线程特性可以缓解Redis的网络请求处理压力。通过多线程增加处理网络请求的能力可以进一步提升实例的整体性能。业界已经有人评测过跟6.0之前的单线程Redis相比6.0的多线程性能的确有提升。所以,这个特性对业务应用会有比较大的帮助。
另外基于用户的命令粒度ACL控制机制也非常有用。当Redis以云化的方式对外提供服务时就会面临多租户比如多用户或多个微服务的应用场景。有了ACL新特性我们就可以安全地支持多租户共享访问Redis服务了。
## [第40讲](https://time.geekbang.org/column/article/312568)
问题你觉得有了持久化内存后还需要Redis主从集群吗
答案持久化内存虽然可以快速恢复数据但是除了提供主从故障切换以外主从集群还可以实现读写分离。所以我们可以通过增加从实例让多个从实例共同分担大量的读请求这样可以提升Redis的读性能。而提升读性能并不是持久化内存能提供的所以如果业务层对读性能有高要求时我们还是需要主从集群的。
## 常见问题答疑
好了关于思考题的讨论到这里就告一段落了。接下来我结合最近收到的一些问题来和你聊一聊在进行原子操作开发时局部变量和全局共享变量导致的差异问题以及Redis和另外两种常见的键值数据库Memcached、RocksDB的优劣势对比。
### 关于原子操作的使用疑问
在[第29讲](https://time.geekbang.org/column/article/299806)中,我在介绍原子操作时,提到了一个多线程限流的例子,借助它来解释如何使用原子操作。我们再来回顾下这个例子的代码:
```
//获取ip对应的访问次数
current = GET(ip)
//如果超过访问次数超过20次则报错
IF current != NULL AND current &gt; 20 THEN
ERROR &quot;exceed 20 accesses per second&quot;
ELSE
//如果访问次数不足20次增加一次访问计数
value = INCR(ip)
//如果是第一次访问将键值对的过期时间设置为60s后
IF value == 1 THEN
EXPIRE(ip,60)
END
//执行其他操作
DO THINGS
END
```
在分析这个例子的时候我提到“第一个线程执行了INCR(ip)操作后第二个线程紧接着也执行了INCR(ip)此时ip对应的访问次数就被增加到了2我们就不能再对这个ip设置过期时间了。”
有同学认为value是线程中的局部变量所以两个线程在执行时每个线程会各自判断value是否等于1。判断完value值后就可以设置ip的过期时间了。因为Redis本身执行INCR可以保证原子性所以客户端线程使用局部变量获取ip次数并进行判断时是可以实现原子性保证的。
我再进一步解释下这个例子中使用Lua脚本保证原子性的原因。
在这个例子中value其实是一个在多线程之间共享的全局变量所以多线程在访问这个变量时就可能会出现一种情况一个线程执行了INCR(ip)后第二个线程也执行了INCR(ip)等到第一个线程再继续执行时就会发生ip对应的访问次数变成2的情况。而设置过期时间的条件是ip访问次数等于1这就无法设置过期时间了。在这种情况下我们就需要用Lua脚本保证计数增加和计数判断操作的原子性。
### Redis和Memcached、RocksDB的对比
Memcached和RocksDB分别是典型的内存键值数据库和硬盘键值数据库应用得也非常广泛。和Redis相比它们有什么优势和不足呢是否可以替代Redis呢我们来聊一聊这个问题。
#### Redis和Memcached的比较
和Redis相似Memcached也经常被当做缓存来使用。不过Memcached有一个明显的优势**就是它的集群规模可以很大**。Memcached集群并不是像Redis Cluster或Codis那样使用Slot映射来分配数据和实例的对应保存关系而是使用一致性哈希算法把数据分散保存到多个实例上而一致性哈希的优势就是可以支持大规模的集群。所以如果我们需要部署大规模缓存集群Memcached会是一个不错的选择。
不过在使用Memcached时有个地方需要注意Memcached支持的数据类型比Redis少很多。Memcached只支持String类型的键值对而Redis可以支持包括String在内的多种数据类型当业务应用有丰富的数据类型要保存的话使用Memcached作为替换方案的优势就没有了。
如果你既需要保存多种数据类型又希望有一定的集群规模保存大量数据那么Redis仍然是一个不错的方案。
我把Redis和Memcached的对比情况总结在了一张表里你可以看下。
<img src="https://static001.geekbang.org/resource/image/9e/29/9eb06cfea8a3ec6fced6e736e4e9ec29.jpg" alt="">
#### Redis和RocksDB的比较
和Redis不同RocksDB可以把数据直接保存到硬盘上。这样一来单个RocksDB可以保存的数据量要比Redis多很多而且数据都能持久化保存下来。
除此之外RocksDB还能支持表结构即列族结构而Redis的基本数据模型就是键值对。所以如果你需要一个大容量的持久化键值数据库并且能按照一定表结构保存数据RocksDB是一个不错的替代方案。
不过RocksDB毕竟是要把数据写入底层硬盘进行保存的而且在进行数据查询时如果RocksDB要读取的数据没有在内存中缓存那么RocksDB就需要到硬盘上的文件中进行查找这会拖慢RocksDB的读写延迟降低带宽。
在性能方面RocksDB是比不上Redis的。而且RocksDB只是一个动态链接库并没有像Redis那样提供了客户端-服务器端访问模式以及主从集群和切片集群的功能。所以我们在使用RocksDB替代Redis时需要结合业务需求重点考虑替换的可行性。
我把Redis和RocksDB的对比情况总结了下如下表所示
<img src="https://static001.geekbang.org/resource/image/7c/82/7c0a225636f4983cb56a5b7265cf5982.jpg" alt="">
## 总结
集群是实际业务应用中很重要的一个需求,在课程的最后,我还想再给你提一个小建议。
集群部署和运维涉及的工作量非常大,所以,我们一定要重视集群方案的选择。
**集群的可扩展性是我们评估集群方案的一个重要维度**你一定要关注集群中元数据是用Slot映射表还是一致性哈希维护的。如果是Slot映射表那么是用中心化的第三方存储系统来保存还是由各个实例来扩散保存这也是需要考虑清楚的。Redis Cluster、Codis和Memcached采用的方式各不相同。
- Redis Cluster使用Slot映射表并由实例扩散保存。
- Codis使用Slot映射表并由第三方存储系统保存。
- Memcached使用一致性哈希。
从可扩展性来看Memcached优于CodisCodis优于Redis Cluster。所以如果实际业务需要大规模集群建议你优先选择Codis或者是基于一致性哈希的Redis切片集群方案。

View File

@@ -0,0 +1,6 @@
你好,我是蒋德钧。
在课程的最后我给你出了一份结课测试题满分100分10道单选题10道多选题快来测试下你的掌握程度吧。
[<img src="https://static001.geekbang.org/resource/image/28/a4/28d1be62669b4f3cc01c36466bf811a4.png" alt="">](http://time.geekbang.org/quiz/intro?act_id=236&amp;exam_id=815)