通过面试题学Redis--进阶篇

欢迎来我的个人网站,里面有最新的版本

上篇篇介绍了下单体Redis的知识,这篇介绍分布式集群下的Redis。

还未全部完成,遗留集群和异步更新策略还未解决

面试题

上篇博客解决了以下问题:

Redis是什么,用在哪?Redis 的缺点?

Redis常见数据类型

  • 用在什么场景
  • 底层数据结构是啥
  • Zset底层为什么要用两个数据结构

Redis的持久化

说一下 Redis 的数据淘汰策略

Redis和MySql的区别?

  • redis为什么不能代替mysql?
  • redis能存大量的数据呢为什么不能?说到了事务

Redis和memcached有什么区别?


这篇博客主要解决以下问题

Redis的并发竞争问题如何解决?
Redis的缓存穿透,缓存雪崩,缓存击穿?怎么解决?
怎么保证缓存和数据库数据的一致性?
Redis集群!集群是如何判断是否有某个节点挂掉?集群进入fail状态的必要条件?
Redis哨兵
Redis主从复制和一致性保证!
Redis热key问题

一、redis缓存与数据库一致性

在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库。

1.1 关于缓存

加入缓存后,客户端发来的请求一般经过以下步骤

通过面试题学Redis--进阶篇_第1张图片

从缓存中读数据会出现以下情况

读数据的时候首先去缓存里读

没有读到再去MySQL里读

读回来之后更新到缓存

下一次从缓存中获取数据

1.2 更新操作带来的一致性问题

写数据的时候会产生数据不一致的问题,无论是先写到Redis里再写MySQL还是先写MySQL再写Redis,这两步写操作不能保证原子性,所以会出现Redis和MySQL里的数据不一致。

数据库中数据更新了,显然缓存也是要更新的,那么并发场景下该先写数据库还是先更新缓存?

1.2.1 先更新数据库,然后再删除缓存

到我们最常用的方案了,但是也会导致一致性问题,不过产生脏数据比较少。

我们假设有两个请求,请求A是读请求,请求B是写请求,那么可能会出现下述情形:

  • 请求B更新数据库

  • 请求A从缓存中读出旧数据

  • 请求B删除缓存

期间只有请求B更新数据库,还没来得及删除缓存这段时间内会有脏数据,导致数据不一致。但是后面更新操作完成后,立马将缓存删除了,在后面的读请求获取到的就是新的数据了。

1.2.2 先删除缓存,然后再更新数据库

这个方案的问题很明显,假设现在并发两个请求,一个是写请求A,一个是读请求B,那么可能出现如下的执行序列:

  • 请求A删除缓存

  • 请求B读取缓存,发现不存在,从数据库中读取到旧值

  • 请求A将新值写入数据库

  • 请求B将旧值写入缓存

这样就会导致缓存中存的还是旧值,在缓存过期之前都无法读到新值。这个问题在数据库读写分离的情况下会更明显,因为主从同步需要时间,请求B获取到的数据很可能还是旧值,那么写入缓存中的也会是旧值。这样会产生大量脏数据

1.3 解决方案

1.3.1 采用延时双删策略

在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。

伪代码如下

public void write(String key,Object data){
 redis.delKey(key);		// 请求A删除缓存
    					// 请求B读取缓存,发现不存在,从数据库中读取到旧值
 db.updateData(data);	// 请求A将新值写入数据库
 Thread.sleep(500);		// 请求B将旧值写入缓存
 redis.delKey(key);		// 删除B写入的旧值
 }
1.具体步骤
  1. 先删除缓存
  2. 再写数据库
  3. 休眠500毫秒
  4. 再次删除缓存
2.确定休眠时间

需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据

当然这种策略还要考虑redis和数据库主从同步的耗时。最后的的写数据的休眠时间:则在读数据业务逻辑的耗时基础上,加几百ms即可。比如:休眠1秒。

3.设置缓存过期时间

从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。

4.该方案的弊端

结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时

1.3.2 异步更新缓存(基于订阅binlog的同步机制)

1.技术整体思路:

MySQL binlog增量订阅消费+消息队列+增量数据更新到redis

1)读Redis:热数据基本都在Redis

2)写MySQL:增删改都是操作MySQL

3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis

2.Redis更新

1)数据操作主要分为两大块:

  • 一个是全量(将全部数据一次写入到redis)
  • 一个是增量(实时更新)

这里说的是增量,指的是mysql的update、insert、delate变更数据。

2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。

这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新

其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。

这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。

当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。

看到这里我想大家还有点疑惑,感觉上比延时双删更浪费时间,以及需要等待的更久,并发场景下可行嘛?我的理解是多个线程的读或写操作都放进了队列,这样可以保证写库并且删了缓存之后再执行下一个线程的读操作,通过这种方式保证了操作的原子性,另外只要异步消息处理的足够快,那么是不会有问题的

二、缓存穿透、击穿与雪崩

使用缓存的主要目是提升查询速度和减轻数据库压力。而缓存最常见的问题是缓存穿透、击穿和雪崩,在高并发下这三种情况都会有大量请求落到数据库,导致数据库资源占满,引起数据库故障。

2.1 缓存穿透

在高并发下,查询一个不存在的值时,缓存不会被命中,导致大量请求直接落到数据库上,这会给持久层数据库造成很大的压力。

2.1.1 解决方案

  1. 布隆过滤器

    布隆过滤器是一种数据结构,用于数据量大的情况下代替hashmap判断某个元素是否存在,拦截对不存在数据的请求

    它是一个bit 向量或者说 bit 数组

    对于要查找的值,将其用多个不同的hash函数映射到这个bit数组中。如hash1(baidu)=1,hash2(baidu)=4,hash3(baidu)=8,将这些位置的元素置为1

    这时,如果查询“ten”是否存在,哈希函数返回了 1、5、8三个值,发现5这个位置没有倍置为1,那么这个元素肯定不存在。

    将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

  2. 缓存空对象

    存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源

    但是这种方法会存在两个问题:

    • 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;

    • 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

2.2 缓存击穿(热点key)

在高并发下,对一个特定的值进行查询,但是这个时候缓存正好过期了,缓存没有命中,导致大量请求直接落到数据库上。

2.2.1 解决方案

  • 使用锁,单机用synchronized,lock等,分布式用分布式锁。
  • 缓存过期时间不设置,而是设置在key对应的value里。如果检测到存的时间超过过期时间则异步更新缓存。
  • 在value设置一个比过期时间t0小的过期时间值t1,当t1过期的时候,延长t1并做更新缓存操作。
  • 设置标签缓存,标签缓存设置过期时间,标签缓存过期后,需异步地更新实际缓存

2.3 缓存雪崩

在高并发下,大量的缓存key在同一时间失效,导致大量的请求落到数据库上。

2.2.1 解决方案

  1. redis高可用

这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群

  1. 限流降级

这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

  1. 数据预热

数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀

三、复制

3.1 主从复制

一个Redis服务可以有多个该服务的复制品,这个Redis服务称为Master,其它复制称为Slaves

主库只负责写数据,每次有数据更新都将更新的数据同步到它所有的从库,而从库只负责读数据。这样一来,就有了两个好处:

  1. 读写分离,不仅可以提高服务器的负载能力,并且可以根据读请求的规模自由增加或者减少从库的数量。

  2. 容灾恢复,数据被复制成了了好几份,就算有一台机器出现故障,也可以使用其他机器的数据快速恢复

Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。下图为级联结构。

3.1.1 全量复制

Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:

  1. 从服务器连接主服务器,发送SYNC命令

  2. 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令

  3. 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;

  4. 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照

  5. 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令

  6. 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令

完成上面几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请求。

3.1.2 增量复制

Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

3.2 Redis主从同步策略

主从刚刚连接的时候,进行全量同步;全量同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

注意

从Redis 2.8开始,如果遭遇连接断开,重新连接之后可以从中断处继续进行复制,而不必重新同步。

部分同步的实现依赖于在master服务器内存中给每个slave服务器维护了一份同步日志和同步标识,每个slave服务器在跟master服务器进行同步时都会携带自己的同步标识和上次同步的最后位置。当主从连接断掉之后,slave服务器隔断时间内(默认1s)主动尝试和master服务器进行连接,如果从服务器携带的偏移量标识还在master服务器上的同步备份日志中,那么就从slave发送的偏移量开始继续上次的同步操作,如果slave发送的偏移量已经不再master的同步备份日志中(可能由于主从之间断掉的时间比较长或者在断掉的短暂时间内master服务器接收到大量的写操作),则必须进行一次全量更新。在部分同步过程中,master会将本地记录的同步备份日志中记录的指令依次发送给slave服务器从而达到数据一致。

3.3 Redis主从复制特点

  1. 采用异步复制;
  2. 一个主redis可以含有多个从redis;每个从redis可以接收来自其他从redis服务器的连接;
  3. 主从复制对于主redis服务器来说是非阻塞的,这意味着当从服务器在进行主从复制同步过程中,主redis仍然可以处理外界的访问请求;
  4. 主从复制对于从redis服务器来说也是非阻塞的,这意味着,即使从redis在进行主从复制过程中也可以接受外界的查询请求,只不过这时候从redis返回的是以前老的数据,如果你不想这样,那么在启动redis时,可以在配置文件中进行设置,那么从redis在复制同步过程中来自外界的查询请求都会返回错误给客户端;
  5. 主从复制提高了redis服务的扩展性,避免单个redis服务器的读写访问压力过大的问题,同时也可以给为数据备份及冗余提供一种解决方案;

四、Sentinel

4.1 哨兵模式–Redis提供的高可用方案

主从模式的弊端就是不具备高可用性,当master挂掉以后,Redis将不能再对外提供写入操作,因此sentinel应运而生。sentinel中文含义为哨兵,它的作用就是监控redis集群的运行状况。有多个sentinel实例组成的sentinel系统可以监视多个主服务器以及下属的从服务器,并在主服务器挂掉之后,升级从服务器为主服务器,由新的主服务器代替已下线的主服务器继续处理命令请求。

通过面试题学Redis--进阶篇_第2张图片

工作机制

  • 每个sentinel以每秒钟一次的频率向它所知的master,slave以及其他sentinel实例发送一个 PING 命令
  • 如果一个实例距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被sentinel标记为主观下线
  • 如果一个master被标记为主观下线,则正在监视这个master的所有sentinel要以每秒一次的频率确认master的确进入了主观下线状态
  • 当有足够数量的sentinel(大于等于配置文件指定的值)在指定的时间范围内确认master的确进入了主观下线状态, 则master会被标记为客观下线
  • 在一般情况下, 每个sentinel会以每 10 秒一次的频率向它已知的所有master,slave发送 INFO 命令
  • 当master被sentinel标记为客观下线时,sentinel向下线的master的所有slave发送 INFO 命令的频率会从 10 秒一次改为 1 秒一次
  • 没有足够数量的sentinel同意master已经下线,master的客观下线状态就会被移除
  • 若master重新向sentinel的 PING 命令返回有效回复,master的主观下线状态就会被移除

当使用sentinel模式的时候,客户端就不要直接连接Redis,而是连接sentinel的ip和port,由sentinel来提供具体的可提供服务的Redis实现,这样当master节点挂掉以后,sentinel就会感知并将新的master节点提供给使用者。

特点

  • sentinel模式是建立在主从模式的基础上,如果只有一个Redis节点,sentinel就没有任何意义
  • master挂了以后,sentinel会在slave中选择一个做为master,并修改它们的配置文件,其他slave的配置文件也会被修改,比如slaveof属性会指向新的master
  • master重新启动后,它将不再是master而是做为slave接收新的master的同步数据
  • sentinel因为也是一个进程有挂掉的可能,所以sentinel也会启动多个形成一个sentinel集群
  • 多sentinel配置的时候,sentinel之间也会自动监控
  • 当主从模式配置密码时,sentinel也会同步将配置信息修改到配置文件中,不需要担心
  • 一个sentinel或sentinel集群可以管理多个主从Redis,多个sentinel也可以监控同一个redis
  • sentinel最好不要和Redis部署在同一台机器,不然Redis的服务器挂了以后,sentinel也挂了

缺点

sentinel模式基本可以满足一般生产的需求,具备高可用性。但无法满足数据量大的场景。

五、集群

5.1 分布式数据库方案

所谓的集群,就是通过添加服务器的数量,提供相同的服务,从而让服务器达到一个稳定、高效的状态。集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。

可参考:https://www.jianshu.com/p/161e66611fe9

4.3 集群模式

4.3.1 主从模式

在主从复制中,数据库分为两类:主数据库(master)和从数据库(slave)

工作机制

当slave启动后,主动向master发送SYNC命令。

master接收到SYNC命令后在后台保存快照(RDB持久化)和缓存保存快照这段时间的命令,然后将保存的快照文件和缓存的命令发送给slave。

slave接收到快照文件和命令后加载快照文件和缓存的执行命令。

复制初始化后,master每次接收到的写命令都会同步发送给slave,保证主从数据一致性

主从复制特点

  • 主数据库可以进行读写操作,当读写操作导致数据变化时会自动将数据同步给从数据库

  • 从数据库一般都是只读的,并且接收主数据库同步过来的数据

  • 一个master可以拥有多个slave,但是一个slave只能对应一个master

  • slave挂了不影响其他slave的读和master的读和写,重新启动后会将数据从master同步过来

  • master挂了以后,不影响slave的读,但redis不再提供写服务,master重启后redis将重新对外提供写服务

  • master挂了以后,不会在slave节点中重新选一个master

缺点

从上面可以看出,master节点在主从模式中唯一,若master挂掉,则redis无法对外提供写服务

你可能感兴趣的:(数据库)