You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

171 lines
15 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 34 | 第23~33讲课后思考题答案及常见问题答疑
你好,我是蒋德钧。
今天又到了我们的答疑时间我们一起来学习下第2333讲的课后思考题。同时我还会给你讲解两道典型问题。
## 课后思考题答案
### [第23讲](https://time.geekbang.org/column/article/293929)
问题Redis的只读缓存和使用直写策略的读写缓存都会把数据同步写到后端数据库中你觉得它们有什么区别吗
答案:主要的区别在于,当有缓存数据被修改时,在只读缓存中,业务应用会直接修改数据库,并把缓存中的数据标记为无效;而在读写缓存中,业务应用需要同时修改缓存和数据库。
我把这两类缓存的优劣势汇总在一张表中,如下所示:
![](https://static001.geekbang.org/resource/image/84/51/84ed48ebccd3443f29cba150b5c1a951.jpg?wh=2822*1556)
### [第24讲](https://time.geekbang.org/column/article/294640)
问题Redis缓存在处理脏数据时不仅会修改数据还会把它写回数据库。我们在前面学过Redis的只读缓存模式和两种读写缓存模式带同步直写的读写模式带异步写回的读写模式请你思考下Redis缓存对应哪一种或哪几种模式
答案如果我们在使用Redis缓存时需要把脏数据写回数据库这就意味着Redis中缓存的数据可以直接被修改这就对应了读写缓存模式。更进一步分析的话脏数据是在被替换出缓存时写回后端数据库的这就对应了带有异步写回策略的读写缓存模式。
### [第25讲](https://time.geekbang.org/column/article/295812)
问题:在只读缓存中对数据进行删改时,需要在缓存中删除相应的缓存值。如果在这个过程中,我们不是删除缓存值,而是直接更新缓存的值,你觉得,和删除缓存值相比,直接更新缓存值有什么好处和不足吗?
答案:如果我们直接在缓存中更新缓存值,等到下次数据再被访问时,业务应用可以直接从缓存中读取数据,这是它的一大好处。
不足之处在于当有数据更新操作时我们要保证缓存和数据库中的数据是一致的这就可以采用我在第25讲中介绍的重试或延时双删方法。不过这样就需要在业务应用中增加额外代码有一定的开销。
### [第26讲](https://time.geekbang.org/column/article/296586)
问题:在讲到缓存雪崩时,我提到,可以采用服务熔断、服务降级、请求限流三种方法来应对。请你思考下,这三个方法可以用来应对缓存穿透问题吗?
答案:关于这个问题,@徐培同学回答得特别好,他看到了缓存穿透的本质,也理解了穿透和缓存雪崩、击穿场景的区别,我再来回答一下这个问题。
缓存穿透这个问题的本质是查询了Redis和数据库中没有的数据而服务熔断、服务降级和请求限流的方法本质上是为了解决Redis实例没有起到缓存层作用的问题缓存雪崩和缓存击穿都属于这类问题。
在缓存穿透的场景下业务应用是要从Redis和数据库中读取不存在的数据此时如果没有人工介入Redis是无法发挥缓存作用的。
一个可行的办法就是**事前拦截**不让这种查询Redis和数据库中都没有的数据的请求发送到数据库层。
使用布隆过滤器也是一个方法布隆过滤器在判别数据不存在时是不会误判的而且判断速度非常快一旦判断数据不存在就立即给客户端返回结果。使用布隆过滤器的好处是既降低了对Redis的查询压力也避免了对数据库的无效访问。
另外这里有个地方需要注意下对于缓存雪崩和击穿问题来说服务熔断、服务降级和请求限流这三种方法属于有损方法会降低业务吞吐量、拖慢系统响应、降低用户体验。不过采用这些方法后随着数据慢慢地重新填充回RedisRedis还是可以逐步恢复缓存层作用的。
### [第27讲](https://time.geekbang.org/column/article/297270)
问题使用了LFU策略后缓存还会被污染吗
答案在Redis中我们使用了LFU策略后还是有可能发生缓存污染的。@yeek回答得不错我给你分享下他的答案。
在一些极端情况下LFU策略使用的计数器可能会在短时间内达到一个很大值而计数器的衰减配置项设置得较大导致计数器值衰减很慢在这种情况下数据就可能在缓存中长期驻留。例如一个数据在短时间内被高频访问即使我们使用了LFU策略这个数据也有可能滞留在缓存中造成污染。
### [第28讲](https://time.geekbang.org/column/article/298205)
问题这节课我向你介绍的是使用SSD作为内存容量的扩展增加Redis实例的数据保存量我想请你来聊一聊我们可以使用机械硬盘来作为实例容量扩展吗有什么好处或不足吗
答案:这道题有不少同学(例如@Lemon、@Kaito都分析得不错我再来总结下使用机械硬盘的优劣势。
从容量维度来看机械硬盘的性价比更高机械硬盘每GB的成本大约在0.1元左右而SSD每GB的成本大约是0.4~0.6元左右。
从性能角度来看机械硬盘例如SAS盘的延迟大约在3~5ms而企业级SSD的读延迟大约是60~80us写延迟在20us。缓存的负载特征一般是小粒度数据、高并发请求要求访问延迟低。所以如果使用机械硬盘作为Pika底层存储设备的话缓存的访问性能就会降低。
所以我的建议是如果业务应用需要缓存大容量数据但是对缓存的性能要求不高就可以使用机械硬盘否则最好是用SSD。
### [第29讲](https://time.geekbang.org/column/article/299806)
问题Redis在执行Lua脚本时是可以保证原子性的那么在课程里举的Lua脚本例子lua.script你觉得是否需要把读取客户端ip的访问次数也就是GET(ip)以及判断访问次数是否超过20的判断逻辑也加到Lua脚本中吗代码如下所示
```
local current
current = redis.call("incr",KEYS[1])
if tonumber(current) == 1 then
redis.call("expire",KEYS[1],60)
end
```
答案在这个例子中要保证原子性的操作有三个分别是INCR、判断访问次数是否为1和设置过期时间。而对于获取IP以及判断访问次数是否超过20这两个操作来说它们只是读操作即使客户端有多个线程并发执行这两个操作也不会改变任何值所以并不需要保证原子性我们也就不用把它们放到Lua脚本中了。
### [第30讲](https://time.geekbang.org/column/article/301092)
问题在课程里我提到我们可以使用SET命令带上NX和EX/PX选项进行加锁操作那么我们是否可以用下面的方式来实现加锁操作呢
```
// 加锁
SETNX lock_key unique_value
EXPIRE lock_key 10S
// 业务逻辑
DO THINGS
```
答案如果使用这个方法实现加锁的话SETNX和EXPIRE两个命令虽然分别完成了对锁变量进行原子判断和值设置以及设置锁变量的过期时间的操作但是这两个操作一起执行时并没有保证原子性。
如果在执行了SETNX命令后客户端发生了故障但锁变量还没有设置过期时间就无法在实例上释放了这就会导致别的客户端无法执行加锁操作。所以我们不能使用这个方法进行加锁。
### [第31讲](https://time.geekbang.org/column/article/301491)
问题在执行事务时如果Redis实例发生故障而Redis使用的是RDB机制那么事务的原子性还能得到保证吗
答案当Redis采用RDB机制保证数据可靠性时Redis会按照一定的周期执行内存快照。
一个事务在执行过程中事务操作对数据所做的修改并不会实时地记录到RDB中而且Redis也不会创建RDB快照。我们可以根据故障发生的时机以及RDB是否生成分成三种情况来讨论事务的原子性保证。
1. 假设事务在执行到一半时实例发生了故障在这种情况下上一次RDB快照中不会包含事务所做的修改而下一次RDB快照还没有执行。所以实例恢复后事务修改的数据会丢失事务的原子性能得到保证。
2. 假设事务执行完成后RDB快照已经生成了如果实例发生了故障事务修改的数据可以从RDB中恢复事务的原子性也就得到了保证。
3. 假设事务执行已经完成但是RDB快照还没有生成如果实例发生了故障那么事务修改的数据就会全部丢失也就谈不上原子性了。
### [第32讲](https://time.geekbang.org/column/article/303247)
问题在主从集群中我们把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讲](https://time.geekbang.org/column/article/303568)
问题假设我们将min-slaves-to-write设置为1min-slaves-max-lag设置为15s哨兵的down-after-milliseconds设置为10s哨兵主从切换需要5s而主库因为某些原因卡住了12s。此时还会发生脑裂吗主从切换完成后数据会丢失吗
答案主库卡住了12s超过了哨兵的down-after-milliseconds 10s阈值所以哨兵会把主库判断为客观下线开始进行主从切换。因为主从切换需要5s在主从切换过程中原主库恢复正常。min-slaves-max-lag设置的是15s而原主库在卡住12s后就恢复正常了所以没有被禁止接收请求客户端在原主库恢复后又可以发送请求给原主库。一旦在主从切换之后有新主库上线就会出现脑裂。如果原主库在恢复正常后到降级为从库前的这段时间内接收了写操作请求那么这些数据就会丢失了。
## 典型问题答疑
在第23讲中我们学习了Redis缓存的工作原理我提到了Redis是旁路缓存而且可以分成只读模式和读写模式。我看到留言区有一些共性问题如何理解Redis属于旁路缓存Redis通常会使用哪种模式现在我来解释下这两个问题。
### 如何理解把Redis称为旁路缓存
有同学提到平时看到的旁路缓存是指写请求的处理方式是直接更新数据库并删除缓存数据而读请求的处理方式是查询缓存如果缓存缺失就读取数据库并把数据写入缓存。那么课程中说的“Redis属于旁路缓存”是这个意思吗
其实这位同学说的是典型的只读缓存的特点。而我把Redis称为旁路缓存更多的是从“业务应用程序如何使用Redis缓存”这个角度来说的。**业务应用在使用Redis缓存时需要在业务代码中显式地增加缓存的操作逻辑**。
例如,一个基本的缓存操作就是,一旦发生缓存缺失,业务应用需要自行去读取数据库,而不是缓存自身去从数据库中读取数据再返回。
为了便于你理解我们再来看下和旁路缓存相对应的、计算机系统中的CPU缓存和page cache。这两种缓存默认就在应用程序访问内存和磁盘的路径上我们写的应用程序都能直接使用这两种缓存。
我之所以强调Redis是一个旁路缓存也是希望你能够记住在使用Redis缓存时我们需要修改业务代码。
### 使用Redis缓存时应该用哪种模式
我提到,通用的缓存模式有三种:**只读缓存模式、采用同步直写策略的读写缓存模式、采用异步写回策略的读写缓存模式**。
一般情况下我们会把Redis缓存用作只读缓存。只读缓存涉及的操作包括查询缓存、缓存缺失时读数据库和回填数据更新时删除缓存数据这些操作都可以加到业务应用中。而且当数据更新时缓存直接删除数据缓存和数据库的数据一致性较为容易保证。
当然有时我们也会把Redis用作读写缓存同时采用同步直写策略。在这种情况下缓存涉及的操作也都可以加到业务应用中。而且和只读缓存相比有一个好处就是数据修改后的最新值可以直接从缓存中读取。
对于采用异步写回策略的读写缓存模式来说缓存系统需要能在脏数据被淘汰时自行把数据写回数据库但是Redis是无法实现这一点的所以我们使用Redis缓存时并不采用这个模式。
## 小结
好了,这次的答疑就到这里。如果你在学习的过程中遇到了什么问题,欢迎随时给我留言。
最后我想说“学而不思则罔思而不学则殆”。你平时在使用Redis的时候不要局限于你眼下的问题你要多思考问题背后的原理积累相应的解决方案。当然在学习课程里的相关操作和配置时也要有意识地亲自动手去实践。只有学思结合才能真正提升你的Redis实战能力。