分类 Redis 下的文章

redis6.0之前是单线程模型,6.0之后是多线程模型

为什么6.0之前采用单线程模型:

  1. redis本身是一个内存数据库,除了持久化,其他操作基于内存,所以处理速度非常快
  2. 大多数场景下,都不是IO密集型的,而是IO密集型的,对于redis来说,瓶颈主要在于网络IO

同时,单线程也带来以下优点:

  1. 避免了多线程调度的上下文切换的开销
  2. 避免了底层的数据同步的锁的开销

6.0之后的多线程:

为什么要引入多线程?

单线程模型的redis已经可以满足10w左右QPS,但是随着互联网的发展,需要满足更大的QPS

而网络IO的read/write系统调用占用了redis执行期间大部分的CPU时间,所以redis使用多线程充分利用多核CPU来减少socket读写阻塞

和单线程的区别:不再是单线程的事件循环,而是多个IO线程各自维护一个事件循环

整体模型上是由Main线程负责接收连接,在分发给IO线程去解析请求命令,具体命令还是Main线程执行,执行完后再使用IO线程回写响应给客户端。

也就是具体命令执行还是单线程,只是socket读写是多线程,一定程度上提高了性能

另外,Redis4.0引入了异步多线程任务来解决比较耗时命令的执行,比如大key删除,持久化

Redis的三种集群模式

  1. 主从模式
  2. 哨兵模式
  3. Cluster模式

主从模式

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

全量复制:

在slave启动的初始化阶段,slave需要将master的全量数据都复制一份,全量复制步骤:

  1. slave连接master,发送SYNC命令
  2. master接收到SYNC命令后,开始执行bgsave生成rdb文件,并使用缓冲区接受所有写命令
  3. master执行完bgsave之后,向slave发送快照,并继续将写命令记录在缓冲区
  4. slave接收到快照之后,载入收到的数据
  5. master发送完快照之后,向slave发送缓冲区的写命令
  6. slave载入快照之后,重放写命令

增量复制:

在slave初始化之后,将master上的写命令同步到slave,保证slave和master数据一致

采用主从复制架构时,必须在master开启持久化,否则可能在master当机重启时,因为master的数据为空导致复制之后slave的数据也丢失

主从复制的特点:

  1. 采用异步复制
  2. 一主可以多从
  3. 从服务器可以接受其他从服务器的连接
  4. 主从复制对于master、slave都是非阻塞的

主从复制的优点:

实现了读写分离,提高了可用性,解决了单点故障

缺点是当master节点宕机时,需要手动进行切换主机,进行故障转移,不容易扩容

哨兵模式

哨兵(Sentinel)是Redis中的一个组件,主要有以下功能:

  • 集群监听:监听master、slave是否正常工作
  • 消息通知:如果某个slave发生了故障,哨兵负责通知给管理员
  • 故障转移:一个master挂掉了,会自动转移到slave上
  • 配置中心:当故障发生了之后,通知client新的master地址

哨兵用于实现redis的高可用,本身也是分布式的,作为一个哨兵集群与运行

哨兵的核心知识:

  • 至少需要3个哨兵,来保证哨兵集群的健壮性
  • 哨兵+主从的架构,主要用于保证redis集群的高可用,不保证不会丢失数据

哨兵主要用来监控Redis集群,来保证Redis的高可用,但是还有一个缺点,就是不易扩容

Cluster模式

Redis Cluster是一种服务端芬片技术,采用了槽(slot)的概念,实现了分布式存储

分片原理:

redis将集群划分为16384个槽,存取数据的时候,对key进行hash来决定放在那个槽上,而redis集群的每一个槽都存放了所有槽的路由信息,当定位到槽后,可以直接从配置中获取到所处的节点,再去对应节点获取数据,扩容时只要将槽迁移到新的节点上,再更新槽的路由信息即可

Cluster模式下,每个节点需要开放两个端口,一个用于客户端请求命令,一个用于进行节点之间的通信,用于故障检测、配置更新、故障转移

优点是:

  • 分布式架构便于动态扩容
  • 节点不需要存放所有数据,节省资源
  • 具备哨兵模式的故障监控和转移能力
  • 无proxy,客户端直接访问redis服务

但缺点是运维更复杂,迁移数据需要人工操作,只能使用0号库,不能使用SELECT

Redis有两种持久化方式:RDB和AOF

  • RDB 是将Redis的内存数据生成一份快照保存在dump.rdb二进制文件中
  • AOF是将每一次执行的修改命令都以追加写入的方式写入appendonly.aof文件中

RDB是redis的默认持久化方式,可以配置持久化策略:save N M,表示N秒内至少M个改动则会触发一次RDB操作,RDB持久化的时候不会阻塞Redis的读写请求,因为Redis从主线程中fock出一个子线程bgsave,可以共享主线程中的数据,当读取redis内存数据时,主线程的读请求不受影响,当写请求时,会借助写时复制技术,将该写请求会操作的数据拷贝一份出来写入dump.rdb,这样主线程可以继续处理写请求

RDB的优缺点:

优点:

dump.rdb是二进制文件,可以很快用于数据恢复

缺点:

  1. 如果redis宕机的话,会丢失上一次RDB同步到宕机之间的数据
  2. RDB 需要将内存的数据都写入到dump.rdb,如果频繁执行的话,开销比较大,影响性能

AOF是同步地将修改写入到appendonly.aof,能最大限度地减少数据丢失的风险,为什么说是最大限度而不是完全避免呢?先看ROF的刷盘策略:

  1. appendfsync always 每次执行命令都刷盘
  2. appendfsync everysec 每秒刷盘一次
  3. appendfsync no 由系统决定什么时候刷盘

当修改命令写入到文件缓存后,需要将缓存刷到磁盘才能真正完成持久化,所以如果刷盘策略不是第一种,则可能丢失短时间内的数据。

AOF重写,因为ROF持久化的是命令而不是具体数据,所以可能有一些命令是重复的,redis有一个机制称为bgrewriteof,用于生成新的aof文件。

比起RDB,ROF还是有一个缺点,就是重放命令来恢复数据效率太低了,Redis 4.0版本为了解决这个问题,提供了混合持久化方案:

在AOF重写时,执行一次RDB,加上增量AOF一起写入到appendonly.aof,这样在恢复数据时,就可以先将RDB恢复,再重放AOF,重启效率提高。

在业务中,我们使用缓存来换缓解数据库压力,只有当缓存不存在或者缓存失效才让请求访问到数据库,这就不可避地需要保证缓存和数据库的数据的一致性

常见的,有3种更新策略:

  1. 先更新数据库,再更新缓存
  2. 先删除缓存,再更新数据库
  3. 先更新数据库,再删除缓存

先更新数据库,再更新缓存

这种方案不推荐,因为考虑有两个写请求A、B,当A更新数据库后,B请求又更新了数据库,但是因为A更新缓存的时候有网络波动导致,B的更新缓存早于A,则会导致缓存中的数据是脏数据的情况

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

这种方案也不推荐,考虑一个写请求A,一个读请求B,当A删除缓存后,B请求到达,查询不到缓存,则立即查询数据库,并更新缓存,A请求此时更新数据库,则导致缓存和数据库数据不一致

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

推荐,先更新数据库再删除缓存,则可以避免并发的写请求进行缓存更新导致缓存不是最新的数据的情况,当然极端情况下,一个读请求,在更新数据前查询到了数据,在删除缓存后写入了缓存,也会出现数据不一致,但是一般来说读请求效率比写请求更高,不会出现这种情况,

实在需要考虑来确保一致性,也可以采用 延时双删 的方案:在删除缓存后,先sleep,再执行一次删除,具体sleep的时长可以根据业务中一个请求的耗时来决定,确保删除可能的由读请求产生的脏数据。

当然,同步的删除操作,会降低吞吐量,可以考虑将删除操作放在MQ异步执行,或者订阅binlog来异步删除缓存

但是如果缓存删除失败,则也会出现数据不一致的情况,这时候可以考虑重试删除

还可以结合主从同步来考虑,比如延时双删的sleep时间大于主从同步时间,避免从库查到旧数据写入缓存后形成脏数据

最后,可以设置过期时间来确保可能的脏数据被刷新,来达到最终一致性

缓存穿透

当大量请求访问的数据在并不存在时,也不能建立缓存,每个请求都会打到数据库,当请求量超过数据库承载能力时,数据库可能被打挂

比如,访问id为0的用户,数据库和缓存都不存在该用户数据,不断访问这个id则会导致数据库崩溃

解决方案:

1、使用布隆过滤器,将数据写入过滤器,如果是不存在的数据就无法再过滤器中查询到数据,丢弃请求或者返回默认值

2、增加参数校验,不合法的参数进行拦截

3、即使不存在的数据也加上缓存,设置合理的超时时间

缓存击穿

热点key缓存过期,导致大量并发访问热点数据的请求查询不到缓存时,需要查询数据库,由于同一时间压力陡增,数据库有崩溃的风险

解决方案:

1、热点数据不过期或者自动续期

2、加互斥锁,查询不到缓存时,只有第一个请求访问数据库

缓存雪崩

大量热点key,同一时间过期,导致大量请求打到数据库,数据库压力像雪崩一样越来越大

解决方案:

1、热点数据不过期或者自动续期

2、设置的缓存时间加上随机值,避免同时过期

3、加互斥锁,查询不到缓存时,只有第一个请求访问数据库