32-34节 主从同步、脑裂、答疑
32-Redis主从同步与故障切换,有哪些坑?
在实际使用主从机制的时候,我们很容易踩到一些坑。这节课,我就向你介绍3个坑,分别是主从数据不一致、读到过期数据,以及配置项设置得不合理从而导致服务挂掉。
一旦踩到这些坑,业务应用不仅会读到错误数据,而且很可能会导致Redis无法正常使用,我们必须要全面地掌握这些坑的成因,提前准备一套规避方案。不过,即使不小心掉进了陷阱里,也不要担心,我还会给你介绍相应的解决方案。
主从数据不一致
主从数据不一致,就是指客户端从从库中读取到的值和主库中的最新值并不一致。
这是因为主从库间的命令复制是异步进行的。
在什么情况下,从库会滞后执行同步命令呢?其实,这里主要有两个原因。
- 一方面,主从库间的网络可能会有传输延迟,所以从库不能及时地收到主库发送的命令,从库上执行同步命令的时间就会被延后。
- 另一方面,即使从库及时收到了主库的命令,但是,也可能会因为正在处理其它复杂度高的命令(例如集合操作命令)而阻塞。
我们该怎么应对呢?我给你提供两种方法。
- 首先,在硬件环境配置方面,我们要尽量保证主从库间的网络连接状况良好。例如,我们要避免把主从库部署在不同的机房,或者是避免把网络通信密集的应用(例如数据分析应用)和Redis主从库部署在一起。
- 另外,我们还可以开发一个外部程序来监控主从库间的复制进度。
- 因为Redis的INFO replication命令可以查看主库接收写命令的进度信息(master_repl_offset)和从库复制写命令的进度信息(slave_repl_offset),所以,我们就可以开发一个监控程序,先用INFO replication命令查到主、从库的进度,然后,我们用master_repl_offset减去slave_repl_offset,这样就能得到从库和主库间的复制进度差值了。
- 如果某个从库的进度差值大于我们预设的阈值,我们可以让客户端不再和这个从库连接进行数据读取,这样就可以减少读到不一致数据的情况。不过,为了避免出现客户端和所有从库都不能连接的情况,我们需要把复制进度差值的阈值设置得大一些。
- 当然,监控程序可以一直监控着从库的复制进度,当从库的复制进度又赶上主库时,我们就允许客户端再次跟这些从库连接。
读取过期数据
Redis为什么还能在从库中读到过期的数据呢?其实,这是由Redis的过期数据删除策略引起的。我来给你具体解释下。
Redis同时使用了两种策略来删除过期的数据,分别是惰性删除策略和定期删除策略。
- 惰性删除策略。当一个数据的过期时间到了以后,并不会立即删除数据,而是等到再有请求来读写这个数据时,对数据进行检查,如果发现数据已经过期了,再删除这个数据。
- 这个策略的好处是尽量减少删除操作对CPU资源的使用,对于用不到的数据,就不再浪费时间进行检查和删除了。但是,这个策略会导致大量已经过期的数据留存在内存中,占用较多的内存资源。
- 定期删除策略是指,Redis每隔一段时间(默认100ms),就会随机选出一定数量的数据,检查它们是否过期,并把其中过期的数据删除,这样就可以及时释放一些内存。
首先,虽然定期删除策略可以释放一些内存,但是,Redis为了避免过多删除操作对性能产生影响,每次随机检查数据的数量并不多。如果过期数据很多,并且一直没有再被访问的话,这些数据就会留存在Redis实例中。业务应用之所以会读到过期数据,这些【留存数据】就是一个重要因素。
其次,惰性删除策略实现后,数据只有被再次访问时,才会被实际删除。如果客户端从主库上读取留存的过期数据,主库会触发删除操作,此时,客户端并不会读到过期数据。但是,从库本身不会执行删除操作,如果客户端在从库中访问留存的过期数据,【从库并不会触发数据删除】。那么,从库会给客户端返回过期数据吗?
这就和你使用的Redis版本有关了。如果你使用的是Redis 3.2之前的版本,那么,从库在服务读请求时,并不会判断数据是否过期,而是会返回过期数据。在3.2版本后,Redis做了改进,如果读取的数据已经过期了,从库虽然不会删除,但是会返回空值,这就避免了客户端读到过期数据。所以,在应用主从集群时,尽量使用Redis 3.2及以上版本。
这就和你使用的Redis版本有关了。如果你使用的是Redis 3.2之前的版本,那么,从库在服务读请求时,并不会判断数据是否过期,而是会返回过期数据。在3.2版本后,Redis做了改进,如果读取的数据已经过期了,从库虽然不会删除,但是会返回空值,这就避免了客户端读到过期数据。所以,在应用主从集群时,尽量使用Redis 3.2及以上版本。
设置数据过期时间的命令一共有4个,我们可以把它们分成两类:
- EXPIRE和PEXPIRE:它们给数据设置的是从命令执行时开始计算的存活时间;
- EXPIREAT和PEXPIREAT:它们会直接把数据的过期时间设置为具体的一个时间点。
这4个命令的参数和含义如下表所示:
当主从库全量同步时,如果主库接收到了一条EXPIRE命令,那么,主库会直接执行这条命令。这条命令会在全量同步完成后,发给从库执行。而从库在执行时,就会在当前时间的基础上加上数据的存活时间,这样一来,从库上数据的过期时间就会比主库上延后了。
为了避免这种情况,我给你的建议是,在业务应用中使用EXPIREAT/PEXPIREAT命令,把数据的过期时间设置为具体的时间点,避免读到过期数据。
好了,我们先简单地总结下刚刚学过的这两个典型的坑。
- 主从数据不一致。Redis采用的是异步复制,所以无法实现强一致性保证(主从数据时时刻刻保持一致),数据不一致是难以避免的。我给你提供了应对方法:保证良好网络环境,以及使用程序监控从库复制进度,一旦从库复制进度超过阈值,不让客户端连接从库。
- 对于读到过期数据,这是可以提前规避的,一个方法是,使用Redis 3.2及以上版本;另外,你也可以使用EXPIREAT/PEXPIREAT命令设置过期时间,避免从库上的数据过期时间滞后。不过,这里有个地方需要注意下,因为EXPIREAT/PEXPIREAT设置的是时间点,所以,主从节点上的时钟要保持一致,具体的做法是,让主从节点和相同的NTP服务器(时间服务器)进行时钟同步。
不合理配置项导致的服务挂掉
这里涉及到的配置项有两个,分别是protected-mode和cluster-node-timeout。
1.protected-mode 配置项
这个配置项的作用是限定哨兵实例能否被其他服务器访问。当这个配置项设置为yes时,哨兵实例只能在部署的服务器本地进行访问。当设置为no时,其他服务器也可以访问这个哨兵实例。
正因为这样,如果protected-mode被设置为yes,而其余哨兵实例部署在其它服务器,那么,这些哨兵实例间就无法通信。当主库故障时,哨兵无法判断主库下线,也无法进行主从切换,最终Redis服务不可用。
所以,我们在应用主从集群时,要注意将protected-mode 配置项设置为no,并且将bind配置项设置为其它哨兵实例的IP地址。这样一来,只有在bind中设置了IP地址的哨兵,才可以访问当前实例,既保证了实例间能够通信进行主从切换,也保证了哨兵的安全性。
2.cluster-node-timeout配置项
这个配置项设置了Redis Cluster中实例响应心跳消息的超时时间。
如果执行主从切换的实例超过半数,而主从切换时间又过长的话,就可能有半数以上的实例心跳超时,从而可能导致整个集群挂掉。所以,我建议你将cluster-node-timeout调大些(例如10到20秒)。
小结
Redis主从库同步时可能出现的3个坑,其成因和解决方案如图
关于主从库数据不一致的问题,我还想再给你提一个小建议:Redis中的slave-serve-stale-data配置项设置了从库能否处理数据读写命令,你可以把它设置为no。这样一来,从库只能服务INFO、SLAVEOF命令,这就可以避免在从库中读到不一致的数据了。
不过,你要注意下这个配置项和slave-read-only的区别,slave-read-only是设置从库能否处理写命令,slave-read-only设置为yes时,从库只能处理读请求,无法处理写请求,你可不要搞混了。
课后问题
33-脑裂:一次奇怪的数据丢失
所谓的脑裂,就是指在主从集群中,同时有两个主节点,它们都能接收写请求。而脑裂最直接的影响,就是客户端不知道应该往哪个主节点写入数据,结果就是不同的客户端会往不同的主节点上写入数据。而且,严重的话,脑裂会进一步导致数据丢失。
为什么会发生脑裂
第一步:确认是不是数据同步出现了问题
在主从集群中发生数据丢失,最常见的原因就是主库的数据还没有同步到从库,结果主库发生了故障,等从库升级为主库后,未同步的数据就丢失了。
如果是这种情况的数据丢失,我们可以通过比对主从库上的复制进度差值来进行判断,也就是计算master_repl_offset和slave_repl_offset的差值。如果从库上的slave_repl_offset小于原主库的master_repl_offset,那么,我们就可以认定数据丢失是由数据同步未完成导致的。
如果相等的话,我们的第一个设想就被推翻了。这时,我们想到,所有的数据操作都是从客户端发送给Redis实例的,那么,是不是可以从客户端操作日志中发现问题呢?紧接着,我们就把目光转到了客户端。
第二步:排查客户端的操作日志,发现脑裂现象
在排查客户端的操作日志时,我们发现,在主从切换后的一段时间内,有一个客户端仍然在和原主库通信,并没有和升级的新主库进行交互。这就相当于主从集群中同时有了两个主库。根据这个迹象,我们就想到了在分布式主从集群发生故障时会出现的一个问题:脑裂。
但是,不同客户端给两个主库发送数据写操作,按道理来说,只会导致新数据会分布在不同的主库上,并不会造成数据丢失。那么,为什么我们的数据仍然丢失了呢?
到这里,我们的排查思路又一次中断了。不过,在分析问题时,我们一直认为“从原理出发是追本溯源的好方法”。脑裂是发生在主从切换的过程中,我们猜测,肯定是漏掉了主从集群切换过程中的某个环节,所以,我们把研究的焦点投向了主从切换的执行过程。
第三步:发现是原主库假故障导致的脑裂
我们是采用哨兵机制进行主从切换的,当主从切换发生时,一定是有超过预设数量(quorum配置项)的哨兵实例和主库的心跳都超时了,才会把主库判断为客观下线,然后,哨兵开始执行切换操作。哨兵切换完成后,客户端会和新主库进行通信,发送请求操作。
但是,在切换过程中,既然客户端仍然和原主库通信,这就表明,原主库并没有真的发生故障(例如主库进程挂掉)。我们猜测,主库是由于某些原因无法处理请求,也没有响应哨兵的心跳,才被哨兵错误地判断为客观下线的。结果,在被判断下线之后,原主库又重新开始处理请求了,而此时,哨兵还没有完成主从切换,客户端仍然可以和原主库通信,客户端发送的写操作就会在原主库上写入数据了。
为了验证原主库只是“假故障”,我们也查看了原主库所在服务器的资源使用监控记录。
的确,我们看到原主库所在的机器有一段时间的CPU利用率突然特别高,这是我们在机器上部署的一个数据采集程序导致的。因为这个程序基本把机器的CPU都用满了,导致Redis主库无法响应心跳了,在这个期间内,哨兵就把主库判断为客观下线,开始主从切换了。不过,这个数据采集程序很快恢复正常,CPU的使用率也降下来了。此时,原主库又开始正常服务请求了。
正因为原主库并没有真的发生故障,我们在客户端操作日志中就看到了和原主库的通信记录。等到从库被升级为新主库后,主从集群里就有两个主库了,到这里,我们就把脑裂发生的原因摸清楚了。
为什么脑裂会导致数据丢失?
主从切换后,从库一旦升级为新主库,哨兵就会让原主库执行slave of命令,和新主库重新进行全量同步。而在全量同步执行的最后阶段,原主库需要清空本地的数据,加载新主库发送的RDB文件,这样一来,原主库在主从切换期间保存的新写数据就丢失了。
在主从切换的过程中,如果原主库只是“假故障”,它会触发哨兵启动主从切换,一旦等它从假故障中恢复后,又开始处理请求,这样一来,就会和新主库同时存在,形成脑裂。等到哨兵让原主库和新主库做全量同步后,原主库在切换期间保存的数据就丢失了。
如何应对脑裂问题?
刚刚说了,主从集群中的数据丢失事件,归根结底是因为发生了脑裂。所以,我们必须要找到应对脑裂问题的策略。
既然问题是出在原主库发生假故障后仍然能接收请求上,我们就开始在主从集群机制的配置项中查找是否有限制主库接收请求的设置。
通过查找,我们发现,Redis已经提供了两个配置项来限制主库的请求处理,分别是min-slaves-to-write和min-slaves-max-lag。
- min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量;
- min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送ACK消息的最大延迟(以秒为单位)。
有了这两个配置项后,我们就可以轻松地应对脑裂问题了。具体咋做呢?
我们可以把min-slaves-to-write和min-slaves-max-lag这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为N和T。这两个配置项组合后的要求是,主库连接的从库中至少有N个从库,和主库进行数据复制时的ACK消息延迟不能超过T秒,否则,主库就不会再接收客户端的请求了。
即使原主库是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然也就无法和从库进行ACK确认了。这样一来,min-slaves-to-write和min-slaves-max-lag的组合要求就无法得到满足,原主库就会被限制接收客户端请求,客户端也就不能在原主库中写入新数据了。
等到新主库上线时,就只有新主库能接收和处理客户端请求,此时,新写的数据会被直接写到新主库中。而原主库会被哨兵降为从库,即使它的数据被清空了,也不会有新数据丢失。
小结
脑裂是指在主从集群中,同时有两个主库都能接收写请求。在Redis的主从切换过程中,如果发生了脑裂,客户端数据就会写入到原主库,如果原主库被降为从库,这些新写入的数据就丢失了。
脑裂发生的原因主要是原主库发生了假故障,我们来总结下假故障的两个原因。
- 和主库部署在同一台服务器上的其他程序临时占用了大量资源(例如CPU资源),导致主库资源使用受限,短时间内无法响应心跳。其它程序不再使用资源时,主库又恢复正常。
- 主库自身遇到了阻塞的情况,例如,处理bigkey或是发生内存swap(你可以复习下第19讲中总结的导致实例阻塞的原因),短时间内无法响应心跳,等主库阻塞解除后,又恢复正常的请求处理了。
为了应对脑裂,你可以在主从集群部署时,通过合理地配置参数min-slaves-to-write和min-slaves-max-lag,来预防脑裂的发生。
在实际应用中,可能会因为网络暂时拥塞导致从库暂时和主库的ACK消息超时。在这种情况下,并不是主库假故障,我们也不用禁止主库接收请求。
所以,我给你的建议是,假设从库有K个,可以将min-slaves-to-write设置为K/2+1(如果K等于1,就设为1),将min-slaves-max-lag设置为十几秒(例如10~20s),在这个配置下,如果有一半以上的从库和主库进行的ACK消息延迟超过十几秒,我们就禁止主库接收客户端写请求。
这样一来,我们可以避免脑裂带来数据丢失的情况,而且,也不会因为只有少数几个从库因为网络阻塞连不上主库,就禁止主库接收请求,增加了系统的鲁棒性。
课后问题
34-第23~33讲课后思考题答案及常见问题答疑
第23讲
问题:Redis的只读缓存和使用直写策略的读写缓存,都会把数据同步写到后端数据库中,你觉得它们有什么区别吗?
答案:主要的区别在于,当有缓存数据被修改时,在只读缓存中,业务应用会直接修改数据库,并把缓存中的数据标记为无效;而在读写缓存中,业务应用需要同时修改缓存和数据库。
第24讲
问题:Redis缓存在处理脏数据时,不仅会修改数据,还会把它写回数据库。我们在前面学过Redis的只读缓存模式和两种读写缓存模式(带同步直写的读写模式,带异步写回的读写模式)),请你思考下,Redis缓存对应哪一种或哪几种模式?
答案:如果我们在使用Redis缓存时,需要把脏数据写回数据库,这就意味着,Redis中缓存的数据可以直接被修改,这就对应了读写缓存模式。更进一步分析的话,脏数据是在被替换出缓存时写回后端数据库的,这就对应了带有异步写回策略的读写缓存模式。
第25讲
问题:在只读缓存中对数据进行删改时,需要在缓存中删除相应的缓存值。如果在这个过程中,我们不是删除缓存值,而是直接更新缓存的值,你觉得,和删除缓存值相比,直接更新缓存值有什么好处和不足吗?
答案:如果我们直接在缓存中更新缓存值,等到下次数据再被访问时,业务应用可以直接从缓存中读取数据,这是它的一大好处。
不足之处在于,当有数据更新操作时,我们要保证缓存和数据库中的数据是一致的,这就可以采用我在第25讲中介绍的重试或延时双删方法。不过,这样就需要在业务应用中增加额外代码,有一定的开销。
第26讲
问题:在讲到缓存雪崩时,我提到,可以采用服务熔断、服务降级、请求限流三种方法来应对。请你思考下,这三个方法可以用来应对缓存穿透问题吗?
答案:
缓存穿透这个问题的本质是查询了Redis和数据库中没有的数据,而服务熔断、服务降级和请求限流的方法,本质上是为了解决Redis实例没有起到缓存层作用的问题,缓存雪崩和缓存击穿都属于这类问题。
在缓存穿透的场景下,业务应用是要从Redis和数据库中读取不存在的数据,此时,如果没有人工介入,Redis是无法发挥缓存作用的。
一个可行的办法就是事前拦截,不让这种查询Redis和数据库中都没有的数据的请求发送到数据库层。
使用布隆过滤器也是一个方法,布隆过滤器在判别数据不存在时,是不会误判的,而且判断速度非常快,一旦判断数据不存在,就立即给客户端返回结果。使用布隆过滤器的好处是既降低了对Redis的查询压力,也避免了对数据库的无效访问。
另外,这里,有个地方需要注意下,对于缓存雪崩和击穿问题来说,服务熔断、服务降级和请求限流这三种方法属于【有损方法】,会降低业务吞吐量、拖慢系统响应、降低用户体验。不过,采用这些方法后,随着数据慢慢地重新填充回Redis,Redis还是可以逐步恢复缓存层作用的。
第27讲
问题:使用了LFU策略后,缓存还会被污染吗?
答案:在Redis中,我们使用了LFU策略后,还是有可能发生缓存污染的。
在一些极端情况下,LFU策略使用的计数器可能会在短时间内达到一个很大值,而计数器的衰减配置项设置得较大,导致计数器值衰减很慢,在这种情况下,数据就可能在缓存中长期驻留。例如,一个数据在短时间内被高频访问,即使我们使用了LFU策略,这个数据也有可能滞留在缓存中,造成污染。
第28讲
问题:这节课,我向你介绍的是使用SSD作为内存容量的扩展,增加Redis实例的数据保存量,我想请你来聊一聊,我们可以使用机械硬盘来作为实例容量扩展吗?有什么好处或不足吗?
答案:我再来总结下使用机械硬盘的优劣势。
从容量维度来看,机械硬盘的性价比更高,机械硬盘每GB的成本大约在0.1元左右,而SSD每GB的成本大约是0.4~0.6元左右。
从性能角度来看,机械硬盘(例如SAS盘)的延迟大约在35ms,而企业级SSD的读延迟大约是6080us,写延迟在20us。缓存的负载特征一般是小粒度数据、高并发请求,要求访问延迟低。所以,如果使用机械硬盘作为Pika底层存储设备的话,缓存的访问性能就会降低。
所以,我的建议是,如果业务应用需要缓存大容量数据,但是对缓存的性能要求不高,就可以使用机械硬盘,否则最好是用SSD。
第29讲
问题:Redis在执行Lua脚本时,是可以保证原子性的,那么,在课程里举的Lua脚本例子(lua.script)中,你觉得是否需要把读取客户端ip的访问次数,也就是GET(ip),以及判断访问次数是否超过20的判断逻辑,也加到Lua脚本中吗?代码如下所示:
1 | local current |
答案:在这个例子中,要保证原子性的操作有三个,分别是INCR、判断访问次数是否为1和设置过期时间。而对于获取IP以及判断访问次数是否超过20这两个操作来说,它们只是读操作,即使客户端有多个线程并发执行这两个操作,也不会改变任何值,所以并不需要保证原子性,我们也就不用把它们放到Lua脚本中了。
第30讲
问题:在课程里,我提到,我们可以使用SET命令带上NX和EX/PX选项进行加锁操作,那么,我们是否可以用下面的方式来实现加锁操作呢?
1 | // 加锁 |
答案:如果使用这个方法实现加锁的话,SETNX和EXPIRE两个命令虽然分别完成了对锁变量进行原子判断和值设置,以及设置锁变量的过期时间的操作,但是这两个操作一起执行时,并没有保证原子性。
如果在执行了SETNX命令后,客户端发生了故障,但锁变量还没有设置过期时间,就无法在实例上释放了,这就会导致别的客户端无法执行加锁操作。所以,我们不能使用这个方法进行加锁。
第31讲
问题:在执行事务时,如果Redis实例发生故障,而Redis使用的是RDB机制,那么,事务的原子性还能得到保证吗?
答案:当Redis采用RDB机制保证数据可靠性时,Redis会按照一定的周期执行内存快照。
一个事务在执行过程中,事务操作对数据所做的修改并不会实时地记录到RDB中,而且,Redis也不会创建RDB快照。我们可以根据故障发生的时机以及RDB是否生成,分成三种情况来讨论事务的原子性保证。
假设事务在执行到一半时,实例发生了故障,在这种情况下,上一次RDB快照中不会包含事务所做的修改,而下一次RDB快照还没有执行。所以,实例恢复后,事务修改的数据会丢失,事务的原子性能得到保证。
假设事务执行完成后,RDB快照已经生成了,如果实例发生了故障,事务修改的数据可以从RDB中恢复,事务的原子性也就得到了保证。
假设事务执行已经完成,但是RDB快照还没有生成,如果实例发生了故障,那么,事务修改的数据就会全部丢失,也就谈不上原子性了。
第32讲
问题:在主从集群中,我们把slave-read-only设置为no,让从库也能直接删除数据,以此来避免读到过期数据。你觉得,这是一个好方法吗?
答案:这道题目的重点是,假设从库也能直接删除过期数据的话(也就是执行写操作),是不是一个好方法?其实,我是想借助这道题目提醒你,【主从复制中的增删改操作都需要在主库执行,即使从库能做删除,也不要在从库删除,否则会导致数据不一致】。
例如,主从库上都有a:stock的键,客户端A给主库发送一个SET命令,修改a:stock的值,客户端B给从库发送了一个SET命令,也修改a:stock的值,此时,相同键的值就不一样了。所以,如果从库具备执行写操作的功能,就会导致主从数据不一致。
@Kaito同学在留言区对这道题做了分析,回答得很好,我稍微整理下,给你分享下他的留言。
即使从库可以删除过期数据,也还会有不一致的风险,有两种情况。
第一种情况是,对于已经设置了过期时间的key,主库在key快要过期时,使用expire命令重置了过期时间,例如,一个key原本设置为10s后过期,在还剩1s就要过期时,主库又用expire命令将key的过期时间设置为60s后。但是,expire命令从主库传输到从库时,由于网络延迟导致从库没有及时收到expire命令(比如延后了3s从库才收到expire命令),所以,从库按照原定的过期时间删除了过期key,这就导致主从数据不一致了。
第二种情况是,主从库的时钟不同步,导致主从库删除时间不一致。
另外,当slave-read-only设置为no时,如果在从库上写入的数据设置了过期时间,Redis 4.0前的版本不会删除过期数据,而Redis 4.0及以上版本会在数据过期后删除。但是,对于主库同步过来的带有过期时间的数据,从库仍然不会主动进行删除。
第33讲
问题:假设我们将min-slaves-to-write设置为1,min-slaves-max-lag设置为15s,哨兵的down-after-milliseconds设置为10s,哨兵主从切换需要5s,而主库因为某些原因卡住了12s。此时,还会发生脑裂吗?主从切换完成后,数据会丢失吗?
答案:主库卡住了12s,超过了哨兵的down-after-milliseconds 10s阈值,所以,哨兵会把主库判断为客观下线,开始进行主从切换。因为主从切换需要5s,在主从切换过程中,原主库恢复正常。min-slaves-max-lag设置的是15s,而原主库在卡住12s后就恢复正常了,所以没有被禁止接收请求,客户端在原主库恢复后,又可以发送请求给原主库。一旦在主从切换之后有新主库上线,就会出现脑裂。如果原主库在恢复正常后到降级为从库前的这段时间内,接收了写操作请求,那么,这些数据就会丢失了。
如何理解把Redis称为旁路缓存?
有同学提到,平时看到的旁路缓存是指,写请求的处理方式是直接更新数据库,并删除缓存数据;而读请求的处理方式是查询缓存,如果缓存缺失,就读取数据库,并把数据写入缓存。那么,课程中说的“Redis属于旁路缓存”是这个意思吗?
其实,这位同学说的是典型的只读缓存的特点。而我把Redis称为旁路缓存,更多的是从“业务应用程序如何使用Redis缓存”这个角度来说的。业务应用在使用Redis缓存时,需要在业务代码中显式地增加缓存的操作逻辑。
例如,一个基本的缓存操作就是,一旦发生缓存缺失,业务应用需要自行去读取数据库,而不是缓存自身去从数据库中读取数据再返回。
为了便于你理解,我们再来看下和旁路缓存相对应的、计算机系统中的CPU缓存和page cache。这两种缓存默认就在应用程序访问内存和磁盘的路径上,我们写的应用程序都能直接使用这两种缓存。
我之所以强调Redis是一个旁路缓存,也是希望你能够记住,在使用Redis缓存时,我们需要修改业务代码。
使用Redis缓存时,应该用哪种模式?
我提到,通用的缓存模式有三种:只读缓存模式、采用同步直写策略的读写缓存模式、采用异步写回策略的读写缓存模式。
一般情况下,我们会把Redis缓存用作只读缓存。只读缓存涉及的操作,包括查询缓存、缓存缺失时读数据库和回填,数据更新时删除缓存数据,这些操作都可以加到业务应用中。而且,当数据更新时,缓存直接删除数据,缓存和数据库的数据一致性较为容易保证。
当然,有时我们也会把Redis用作读写缓存,同时采用同步直写策略。在这种情况下,缓存涉及的操作也都可以加到业务应用中。而且,和只读缓存相比有一个好处,就是数据修改后的最新值可以直接从缓存中读取。
对于采用异步写回策略的读写缓存模式来说,缓存系统需要能在脏数据被淘汰时,自行