Redis 探索Ⅱ

本文主要讨论了Redis的持久化、Redis可用性、Redis锁以及AKF微服务划分原则的相关话题。

一、Redis 持久化

1.1 持久化的必要性

  1. 因为Redis是放在内存做缓存使用,如果宕机,那么就会损失数据。
    如果缓存的数据是可有可无的,那么损失的数据没什么影响。
    如果缓存的数据是相对比较重要的,那么在数据丢失后,服务无法在Redis请求到数据时,就会去数据库请求,这样就可能造成后面数据库的压力倍增。则在这种情况下,是有持久化的必要的。
  2. 持久化的开启,会引发一系列I/O的操作,而造成一定性能的下降。故是否持久化,还是依据具体环境配置。

1.2 RDB 与 AOF

数据写到磁盘的流程:数据从内存写入到磁盘时,会经过kernel内核再写入磁盘。而kernel里有一个buffer缓冲区,默认情况下只有当buffer里面的数据满的时候,OS才会主动将buffer里面的数据刷写到磁盘。如果主动执行flush操作,则buffer中的数据会立刻被刷写到磁盘。

  1. 持久化的两种方案:快照(RDB)与日志(AOF)
  2. RDB工作模式:将Redis中的二进制数据直接存入磁盘中(拍快照),但是拍快照的时间间隔较长,例如一小时拍一次快照。当Redis宕机后重启,Redis就会直接将磁盘的快照数据读入内存,不需要做数据的转换。
    优点:恢复数据的速度快。
    缺点:拍快照的时间间隔较长,丢失的数据大。
  3. AOF工作模式:将Redis所执行的写操作指令都写入磁盘,在宕机开启后,需要读取日志中的指令,然后逐个执行一遍对应的写操作,以此来达到恢复数据的目的。具体AOF有三种工作级别,分别是appendfsync always、appendfsync everysec与appendsync no,三种分别具有不同的优缺点。
  4. AOF的appendfsync always:采用将每次操作都立刻执行flush写入磁盘。
    优点:数据的完整性强。极端情况下,最多丢一条记录。
    缺点:操作过于频繁,导致性能下降。
  5. AOF的appendfsync everysec(默认级别):采用每秒flush一次,将buffer中的数据刷写到磁盘。
    优点:最多丢失一秒钟的数据。极端情况下,在一秒之内,刚好buffer写满时宕机,至多丢失一个buffer的数据。
    缺点:可能会丢失一秒钟的数据。
  6. AOF的appendsync no:完全交由OS来执行flush来将buffer中的数据刷写到磁盘。
    优点:至多丢一个buffer的数据
    缺点:没有一秒的刷写操作,比第二种方案丢数据的可能性更大。例如,一分钟的时间内,前50s秒
    来了10条写操作指令,此时都存在buffer中,但是buffer没有满,在第51秒宕机,则会丢失这51之内的数据。如果第二种方案,每秒执行一次flush,则前50秒的数据已刷写到磁盘。

1.3 Redis对 RDB 与 AOF 的使用

  1. 早期Redis
    默认开启的是RDB,虽然可手动开启AOF,但是开启AOF后,重启只会加载AOF,前面写入磁盘的RDB将会丢失。
  2. 目前Redis
    采用的是RDB与AOF的混合使用,简称R&A(也叫AOF)。例如每隔一小时拍一次快照,拍完快照后将这一小时之内的日志删除,而在之后的一小时之内继续进行日志记录。这样重启之后就先加载RDB,然后加载一个小时之内的日志写操作并执行即可。这样既加快了恢复速度,又减少了数据的丢失。

二、Redis 可用性

两个问题:单点故障与压力(即存储上线)。
单点故障问题:即单个实例可能宕机,而在宕机后如何保证服务继续运行。
压力问题:即单个实例内存空间有限,在数据超过实例容量的情况下如何保证服务继续运行。

2.1 单点故障问题解决方案

多准备几个Redis,在实例A挂了之后,后面的实例B可以接上。而实例B与实例A的数据是否完全相同,就会产生数据同步的问题。

  1. 主备-强一致性:客户端往A写入一条指令,A收到后先往B写,B写成功后A再写,然后A再告诉客户端我们俩都写成功了。
    问题:若A与B的网络出现问题,或B出现宕机。这样都会导致A持续等待B的返回,而客户端又在等A的数据返回,这种情况下在外界看来就是Redis已经挂了。即强一致性破坏了可用性。
  2. 主从-弱一致性(Redis默认的工作模式):客户端往A写入一条指令,A写成功后立刻返回,然后A再往B中写。
    问题:在A宕机后,可能A有部分数据还没有进行对B写,则B接管后的数据会少于A中数据的量。
  3. 最终一致性技术(Redis未采用)

2.2 压力问题解决方案

  1. 多维度划分
    若一个数据很大,若能将该数据拆分到不同的维度,则将不同维度的数据存放在不同的Redis中,这些Redis无数据同步的问题。在保证可用性的问题上,只需要再对不同维度Redis做一次主从复制即可。
  2. 分片(若无法多维度划分)
    按照区间或hash取模等映射算法,将数据的不同部分存入多个不同的实例中(分治)。

三、AKF微服务划分原则

以类似三维的划分进行服务处理。
x轴:采用主从主备的模式,提升Redis的可靠性(解决单点故障问题)
y轴:对服务分治处理,使得服务的功能单一性,降低耦合性。
z轴:对数据存储进行分片划分(解决压力问题)

3.1 分片集群问题

若将分片的映射算法放在服务中实现,那么在更新映射算法的时候会很麻烦。

3.2 代理集群问题

  1. 若将数据全都交给代理,再通过代理将数据扔给服务,以此可实现解耦。但是,若客户端数据并发,代理层在实现负载均衡上就很复杂。
  2. Redis是否能自身解决代理层的负载均衡问题?
    可在每个Redis实例前都绑定一个Proxy以及存储自身映射范围与其他实例映射范围及其信息的记录。当一个服务请求过来,代理收到数据后,先判断其映射的范围是否是该Redis,若是,则将数据交付给Redis;若不是,则在记录中找到映射该数据的Redis信息,并告知服务应该去这个Redis执行操作。这样就没有了中间代理层的负载均衡问题,使得每个Redis联合起来实现了负载均衡的问题。

四、Redis 是否适合做分布式锁

了解一:何时需要锁?
多个并发客户端,都会对公共资源进行相关写操作时。
了解二:锁如何解决问题?
谁得到锁,谁就获得访问资源的资格。

4.1 单Redis实例实现锁的方法

哪个服务在Redis中成功创建了key(获得锁),则谁就获得访问公共资源的问题。操作完成后,移除key(释放锁),则其他服务就可在获得锁。
注:假设此时有A、B两个服务并发竞争锁。

  1. 问题1:A抢到锁后,A挂了,锁不会消失,导致后面的服务无法占有锁,导致死锁问题。
    解决:对key做过期设置,例如30s之后自动消失。
  2. 问题2:A抢到锁后,Redis挂了,在Redis重启后,锁还在,但是A已经不会再进行对锁的释放操作,同样是死锁问题。
    解决:同问题一,设置key的过期时间
  3. 问题3:A抢到锁后,Redis挂了,在Redis重启后,锁消失(并非A主动释放),此时B获得锁,会造成A与B并发访问资源的问题。
    解决:开启AOF的每操作,但是这样Redis性能就会退化到MySql级别。若开启主从复制,但是是弱一致,并无法解决。
  4. 总结:一个Redis实现锁并不可靠

4.2 多Redis实例实现锁的方法

哪个服务抢占到过半的Redis实例,谁就能访问资源
注:假设此时有三个Redis实例R1、R2、R3。

  1. 问题1:当三个服务抢三个资源,每个都占有一把锁,此时谁都不能达到过半。
    解决:当对R1、R2、R3轮询获取锁之后,若还是不能获得过半实例的服务,就释放获得的锁,然后再开启新的一轮竞争。
  2. 问题2:极端情况下,每个服务一直都不能抢占过半,导致服务无法进行下去。
  3. 总结:Redis不适合实现锁。

4.3 zk 的锁机制

  1. zk的每个节点自身就可以互相通信,并分为leader与follower。增删改查的事情只能交由leader完成。
  2. 在并发抢锁时,zk是串行化的处理,会逐个对资源操作。
  3. 在服务获取到锁后若断线,zk会马上知道,并通知竞争锁的服务,使这些服务可以重新开始竞争锁。
  4. zk具有最终一致性。

思考

分布式串行好还是并行好?
有时候串行慢,但是未必一定