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.

135 lines
12 KiB
Markdown

2 years ago
# 41 | 第3540讲课后思考题答案及常见问题答疑
你好,我是蒋德钧。
今天是我们最后一节答疑课我会带你一起分析一下第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 > 20 THEN
ERROR "exceed 20 accesses per second"
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的对比情况总结在了一张表里你可以看下。
![](https://static001.geekbang.org/resource/image/9e/29/9eb06cfea8a3ec6fced6e736e4e9ec29.jpg)
#### Redis和RocksDB的比较
和Redis不同RocksDB可以把数据直接保存到硬盘上。这样一来单个RocksDB可以保存的数据量要比Redis多很多而且数据都能持久化保存下来。
除此之外RocksDB还能支持表结构即列族结构而Redis的基本数据模型就是键值对。所以如果你需要一个大容量的持久化键值数据库并且能按照一定表结构保存数据RocksDB是一个不错的替代方案。
不过RocksDB毕竟是要把数据写入底层硬盘进行保存的而且在进行数据查询时如果RocksDB要读取的数据没有在内存中缓存那么RocksDB就需要到硬盘上的文件中进行查找这会拖慢RocksDB的读写延迟降低带宽。
在性能方面RocksDB是比不上Redis的。而且RocksDB只是一个动态链接库并没有像Redis那样提供了客户端-服务器端访问模式以及主从集群和切片集群的功能。所以我们在使用RocksDB替代Redis时需要结合业务需求重点考虑替换的可行性。
我把Redis和RocksDB的对比情况总结了下如下表所示
![](https://static001.geekbang.org/resource/image/7c/82/7c0a225636f4983cb56a5b7265cf5982.jpg)
## 总结
集群是实际业务应用中很重要的一个需求,在课程的最后,我还想再给你提一个小建议。
集群部署和运维涉及的工作量非常大,所以,我们一定要重视集群方案的选择。
**集群的可扩展性是我们评估集群方案的一个重要维度**你一定要关注集群中元数据是用Slot映射表还是一致性哈希维护的。如果是Slot映射表那么是用中心化的第三方存储系统来保存还是由各个实例来扩散保存这也是需要考虑清楚的。Redis Cluster、Codis和Memcached采用的方式各不相同。
* Redis Cluster使用Slot映射表并由实例扩散保存。
* Codis使用Slot映射表并由第三方存储系统保存。
* Memcached使用一致性哈希。
从可扩展性来看Memcached优于CodisCodis优于Redis Cluster。所以如果实际业务需要大规模集群建议你优先选择Codis或者是基于一致性哈希的Redis切片集群方案。