一:关系型数据库和非关系型数据库区别

1.关系型数据库最典型的数据结构是表,由二维表及其之间的联系所组成的一个数据组织

优点:

(1)易于维护:都是使用表结构,结构一致

(2)使用方便:SQL语言使得我们操作数据库更加方便

(3)支持SQL:可以通过SQL语句进行复杂查询

缺点

(1)读写性能比较差,尤其是海量数据的高效率读写

(2)固定的表的结构,灵活性差

(3)硬盘I/O要求高:网站的用户并发性非常高,往往达到每秒上万次读写请求,对于传统关系型数据库来说,硬盘I/O是一个很大的瓶颈

2.非关系型数据库 不是数据库,而是数据结构化存储方法的集合,可以是文档或键值对等

优点

(1)格式灵活:存储数据格式可以是key,value格式,文档格式,图片格式等等,使用灵活,应用场景广泛,而关系型数据库只支持基础的数据类型

(2)速度快:nosql可以使用硬盘或者随机存储器作为载体,而关系型数据库只能使用硬盘;

(3)高扩展性;Nosql基于键值对,数据之间没有耦合性,所以非常容易水平扩展

缺点

(1)不支持事务的ACID特性

(2)不提供SQL支持,学习和使用成本较高

(3)只适合存储一些较为简单的数据,需要进行较复杂查询的数据,关系型数据库显的更为合适

二:什么是Redis

Redis是用C语言编写的,开源的高性能非关系型数据库(Nosql),是一个高性能的key--value数据库;

Redis可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值得类型可以为字符串,List列表,Set集合,Sorted set有序集合,Hash散列表。

与传统数据库不同的是Redis的数据库是存储与内存中的,所以读写速度非常快,所以Redis被广泛应用与缓存方向,每秒可以处理超过10w次读写操作,是已执性能最快的Key--value DB;

此外,Redis也经常用来做分布式锁。而且Redis还支持事务,持久化,LUA脚本,LRU驱动事件,多种集群方案。

三:Redis的优缺点

优点:

(1)读写性能优异

(2)支持数据持久化---AOF持久化和RDB持久化

(3)支持事务,Redis的所有操作都是原子性的

(4)数据结构丰富,除了支持String类型之外,还支持hash,set,zset,list等数据结构

(5)支持主从复制,主机会自动将数据同步到从机,可进行读写分离

缺点

(1)数据库容量容易受到物理内存的限制,不能用作高性能海量数据读写,因此Redis主要适用于较小数据量的高性能操作和运算上

(2)Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写需求失败,需要等待机器重启或者手动切换前端的IP才能恢复

(3)主机宕机,宕机前有部分数据未能同步到从机,切换IP后会引入数据不一致的问题

(4)Redis较难支持在线扩容,在集群容量达到上限时,扩容会变得很复杂;所有运维人员在系统上线时必须确保足够的空间,这对资源造成很大的浪费

四:Redis支持事务持久化--- RDB 和 AOF

RDB持久化可以在指定时间间隔内生成数据集的时间点快照

在默认情况下, Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。

以下设置会让 Redis 在满足“ 60 秒内有至少有 1000 个键被改动”这一条件时, 自动保存一次 数据集: # save 60 1000 //关闭RDB只需要将所有的save保存策略注释掉即可

AOF持久化是记录服务器执行的所有读写操作命令,并在服务器启动时,通过执行这些命令来还原数据集。AOF文件中的命令全部以Redis协议的格式来保存,新命令会被追加到文件的末尾;

你可以通过修改配置文件来打开 AOF 功能:

1 # appendonly yes

你可以配置 Redis 多久才将数据 fsync 到磁盘一次。 有三个选项:

1 appendfsync always:每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。

2 appendfsync everysec:每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据。

3 appendfsync no:从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。

Redis还可以在后台对AOF文件进行重写,使AOF文件的体积不会超出保存数据集状态所需的实际大小,。

Redis还可以同时使用RDB和AOF持久化。在这种情况下,当Redis重启时,他会优先使用AOF文件来还原数据集,因为AOF文件保存的数据集通常比RDB文件所保存的数据集更完整。

img

五:为什么要用Redis/为什么要用缓存

主要从高性能高并发这两个角度来看待问题

高性能:

假如用户第一次访问数据库中的某些数据,这个过程时比较慢的,因为使用硬盘上直接读取的。将该用户访问的数据存储在缓存中,这样下次再次访问这些数据的时候,就可以直接去缓存中拿了。操作缓存就是直接操作内存,所以速度特别快。如果数据库中的数据改变后,同步改变缓存中相对应得数据即可。

高并发:

直接操作缓存所能承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户中的一部分请求会直接到缓存中,而不是数据库。

六:为什么Redis这么快

(1)完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存 中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O(1);

(2)数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的

(3)采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者 多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁 操作,没有因为可能出现死锁而导致的性能消耗;

(4)使用多路 I/O 复用模型,非阻塞 IO

七:Redis有哪些数据类型

Redis主要有5种数据类型,包括String,List,Set,Zset,Hash,满足大部分 的使用要求

String

常用命令 :set/get/decr/incr/mget等;

应用场景 :String是最常用的一种数据类型,普通的key/value存储都可以归为此类;

实现方式:String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr、decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。

List

常用命令 :lpush/rpush/lpop/rpop/lrange等;

应用场景 :Redis list的应用场景 非常多,也是Redis最重要的数据结构之一,比如twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现;

实现方式:Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。

Set

常用命令 :sadd/spop/smembers/sunion等;

应用场景 :Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的;

实现方式:set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。

Sorted Set

常用命令 :zadd/zrange/zrem/zcard等;

应用场景 :Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。

实现方式:Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。

Hash

常用命令 :hget/hset/hgetall等

应用场景 :我们要存储一个用户信息对象数据,其中包括用户ID、用户姓名、年龄和生日,通过用户ID我们希望获取该用户的姓名或者年龄或者生日;

实现方式:Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口。如图所示,Key是用户ID, value是一个Map。这个Map的key是成员的属性名,value是属性值。这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field),也就是通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据。当前HashMap的实现有两种方式:当HashMap的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,这时对应的value的redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时redisObject的encoding字段为int。

八:Redis的过期键和删除策略

我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存的key的过 期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。

过期策略通常有以下三种

(1)定时过期:每个过期时间的Key都需要一个定时器,到过期期间就会立即清楚。该策略可以立即清楚过期的key,对内存很友好;但是会占用大量CPU去处理过期的数据,从而影响缓存的响应时间和吞吐量。

(2)惰性过期:只有当访问一个key时,才会去判断这个Key是否已经过期,过期则清除;该策略可以最大化的节省CPU资源,但对内存十分不友好,极端情况可能出现大量的 过期key没有再次被访问,从而不会被清除,占用大量内存

(3)定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数 量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定 时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达 到最优的平衡效果。

九:MySQL里有2000w数据,redis中只存20w的数据,如何 保证redis中的数据都是热点数据

Redis内存数据集大小上升到一定大小时,就会实施数据淘汰策略

十:Redis的内存淘汰策略有哪些

全局的键空间选择性移

no-eviction:当内存不足以容纳新写入数据时,新写入操作会报错。

allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用 的key。 (这个是最常用的)

allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。

设置过期时间的键空间选择性移除

volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中, 移除最近最少使用的key。

volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空 间中,随机移除某个key。

volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除将要过期的key,ttl的值越大优先被移除。

Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于 处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。

十一:Redis到底是不是单线程的

Redis是单线程的;通常说的单线程,主要是指Redis对外提供的键值存储服务的主要流程是单线程,也就是网络I/O和数据读写是由单个线程来完成的。

除此之外Redis的其他功能,例如持久化,异步删除,集群数据同步等,是由额外线程来执行的。

因此,严格来说Redis并不是全面单线程。

十二:Redis I/O多路复用

首先,Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。

I/O 多路复用其实是在单个线程中通过记录跟踪每一个sock(I/O流) 的状态来管理多个I/O流。

(1)select, poll, epoll 都是I/O多路复用的具体的实现

之所以有这三个鬼存在,其实是他们出现是有先后顺序的。

I/O多路复用这个概念被提出来以后, select是第一个实现 (1983 左右在BSD里面实现的)。

一、select 被实现以后,很快就暴露出了很多问题。

  • select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。
  • select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,于是你只能自己一个一个的找,10几个sock可能还好,要是几万的sock每次都找一遍,这个无谓的开销就颇有海天盛筵的豪气了。
  • select 只能监视1024个链接, 这个跟草榴没啥关系哦,linux 定义在头文件中的,参见FD_SETSIZE。
  • select 不是线程安全的

二、于是14年以后(1997年)一帮人又实现了poll, poll 修复了select的很多问题,比如

  • poll 去掉了1024个链接的限制,于是要多少链接呢, 主人你开心就好。
  • poll 从设计上来说,不再修改传入数组

但是poll仍然不是线程安全的, 这就意味着,不管服务器有多强悍,你也只能在一个线程里面处理一组I/O流。你当然可以那多进程来配合了,不过然后你就有了多进程的各种问题。

于是5年以后, 在2002, 大神 Davide Libenzi 实现了epoll.

三、epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:

  • epoll 现在是线程安全的。
  • epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。

可是epoll 有个致命的缺点,只有linux支持。

虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。如果用户线程只注册自己感兴趣的socket或者IO请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高CPU的利用率。

IO多路复用模型使用了Reactor设计模式实现了这一机制。

(2)IO多路复用(Reactor)

img

通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的,因此多路IO复用模型也被称为异步阻塞IO模型。

注意,*这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。*一般在使用IO多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起IO请求时,数据已经到达了,用户线程一定不会被阻塞。

十三:Redis事务相关的命令

Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的 Redis会将一个事务中的所有命令序列化,然后按顺序执行。

(1)redis不支持回滚:““Redis 在事务失败时不进行回滚,而是继续执行 余下的命令”

(2)如果一个事务的命令出现错误,那么所有的命令都不会再执行

(3)如果在一个事务中出现运行错误,那么正确的命令会被执行

  • WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS) 行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务 就不会执行,监控一直持续到EXEC命令。
  • MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以 继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列 中,当EXEC命令被调用时,所有队列中的命令才会被执行。
  • EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执 行的先后顺序排列。 当操作被打断时,返回空值 nil 。 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端 会从事务状态中退出。
  • UNWATCH命令可以取消watch对所有key的监控。

十四:事务与Redis

Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服 务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务 也具有耐久性。

Redis事务支持隔离性吗

Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可 以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有 隔离性的。

Redis事务保证原子性吗,支持回滚吗

Redis中单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务 中任意命令执行失败,其余的命令仍会被执行。

Redis事务其他实现

基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行, 其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是 会继续运行完

基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时 先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐

十五:Redis主从复制

如果你为master配置了一个slave,不管这个slave是否是第一次连接上Master,它都会发送一个PSYNC 命令给master请求复制数据。

master收到PSYNC命令后,会在后台进行数据持久化通过bgsave生成最新的rdb快照文件,持久化期 间,master会继续接收客户端的请求,它会把这些可能修改数据集的请求缓存在内存中。

当持久化进行完 毕以后,master会把这份rdb文件数据集发送给slave,slave会把接收到的数据进行持久化生成rdb,然后 再加载到内存中。

然后,master再将之前缓存在内存中的命令发送给slave。

当master与slave之间的连接由于某些原因而断开时,slave能够自动重连Master,如果master收到了多 个slave并发连接请求,它只会进行一次持久化,而不是一个连接一次,然后再把这一份持久化的数据发送 给多个并发连接的slave。
img

数据部分复制

当master和slave断开重连后,一般都会对整份数据进行复制。但从redis2.8版本开始,redis改用可以支 持部分数据复制的命令PSYNC去master同步数据,slave与master能够在网络连接断开重连后只进行部分数据复制(断点续传)。

master会在其内存中创建一个复制数据用的缓存队列,缓存最近一段时间的数据,master和它所有的 slave都维护了复制的数据下标offset和master的进程id,因此,当网络连接断开后,slave会请求master 继续进行未完成的复制,从所记录的数据下标开始。如果master进程id变化了,或者从节点数据下标 offset太旧,已经不在master的缓存队列里了,那么将会进行一次全量数据的复制。

十六:Redis哨兵模式

img

有以下功能:

(1)集群监控:负责监控 redis master 和 slave 进程是否正常工作。

(2)消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给 管理员。 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。

(3)配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。

(4)哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去 运行,互相协同工作。 故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可 用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。

哨兵的核心知识

(1)哨兵至少需要 3 个实例,来保证自己的健壮性。

(2)哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群 的高可用性。

(3)对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进 行充足的测试和演练。

十七:redis缓存穿透

img

理 解

  • 重在穿透吧,也就是访问透过redis直接经过mysql,通常是一个不存在的key,在数据库查询为null。每次请求落在数据库、并且高并发。数据库扛不住会挂掉。

解决方案

  • 可以将查到的null设成该key的缓存对象。
  • 当然,也可以根据明显错误的key在逻辑层就就行验证
  • 同时,你也可以分析用户行为,是否为故意请求或者爬虫、攻击者。针对用户访问做限制。
  • 其他等等,比如采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层 存储系统的查询压力

十八:redis缓存雪崩

是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库 上,造成数据库短时间内承受大量请求而崩掉。

解决方案

  • 通常的解决方案是将key的过期时间后面加上一个随机数,让key均匀的失效。
  • 考虑用队列或者锁让程序执行在压力范围之内,当然这种方案可能会影响并发量。
  • 热点数据可以考虑不失效

十九:缓存击穿

缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,好像蛮力击穿一样。

击穿和穿透不同,穿透的意思是想法绕过redis去使得数据库崩掉。而击穿你可以理解为正面刚击穿,这种通常为大量并发对一个key进行大规模的读写操作。这个key在缓存失效期间大量请求数据库,对数据库造成太大压力使得数据库崩掉。就比如在秒杀场景下10000块钱的mac和100块的mac这个100块的那个订单肯定会被抢到爆,不断的请求(当然具体秒杀有自己处理方式这里只是举个例子)。所以缓存击穿就是针对某个常用key大量请求导致数据库崩溃。

解决方法

  • 可以使用互斥锁避免大量请求同时落到db。
  • 可以将缓存设置永不过期(适合部分情况)

二十:Redis分片

redis的主从复制和哨兵可以解决高并发,高可用读的问题,但是还有两个没有解决

(1)高并发写

(2)海量数据存储

但是redis的分片可以解决以上问题

二十一:Redis是单线程的,如何提高多核CPU的利用率?

可以在同一个服务器部署多个Redis的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果你想使用多个CPU,你可以考虑一下分片(shard)

二十二:Redis分区有什么缺点?

(1) 涉及多个key的操作通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存储到不同的Redis实例(实际上这种情况也有办法,但是不能直接使用交集指令)。
同时操作多个key,则不能使用Redis事务.
(2)分区使用的粒度是key,不能使用一个非常长的排序key存储一个数据集
(3)当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的Redis实例和主机同时收集RDB / AOF文件。
(4)分区时动态扩容或缩容可能非常复杂。Redis集群在运行时增加或者删除Redis节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。

二十三:如何解决 Redis 的并发竞争 Key 问题

所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!

推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)

基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。

在实践中,当然是从以可靠性为主。所以首推Zookeeper。

二十四:分布式Redis是前期做还是后期规模上来了再做好?为什么?

既然Redis是如此的轻量(单实例只使用1M内存),为防止以后的扩容,最好的办法就是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让Redis以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。

一开始就多设置几个Redis实例,例如32或者64个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。

这样的话,当你的数据不断增长,需要更多的Redis服务器时,你需要做的就是仅仅将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。一旦你添加了另一台服务器,你需要将你一半的Redis实例从第一台机器迁移到第二台机器。

二十五:如何保证缓存与数据库双写时的数据一致性?

1.先写缓存,再写数据库

先写缓存,当我们写入缓存成功的时候,突然网络出现异常,导致写数据库失败,这时候数据库没有新的数据,缓存有新的数据,此时缓存中的数据就变成了脏数据

2.先写数据库,再写缓存

如果先更新缓存成功,但是数据库更新失败,则肯定会造成数据不一致

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

假设在高并发的场景中,同一个用户的同一条数据,有一个读数据请求c,还有另一个写数据请求d(一个更新操作),同时请求到业务系统。如下图所示:

在这里插入图片描述

上图流程如下:

1.请求d先过来,把缓存删除了。但由于网络原因,卡顿了一下,还没来得及写数据库。
2.这时请求c过来了,先查缓存发现没数据,再查数据库,有数据,但是旧值。
3.请求c将数据库中的旧值,更新到缓存中。
4.此时,请求d卡顿结束,把新值写入数据库。
在这个过程当中,请求d的新值并没有被请求c写入缓存,同样会导致缓存和数据库的数据不一致的情况。

解决方法:

缓存双删

针对上面这种场景有一种很简单的处理办法,思路很简单:
当d请求写入成功之后,我们在将缓存重删一次。

在这里插入图片描述

这就是我们所说的缓存双删,即:

即在写数据库之前删除一次,写完数据库后,再删除一次。

该方案有个非常关键的地方是:第二次删除缓存,并非立马就删,而是要在一定的时间间隔之后。

有了缓存删除方案之后,我们在回顾一下高并发下的场景问题:

1.请求d先过来,把缓存删除了。但由于网络原因,卡顿了一下,还没来得及写数据库。
2.这时请求c过来了,先查缓存发现没数据,再查数据库,有数据,但是旧值。
3.请求c将数据库中的旧值,更新到缓存中。
4.此时,请求d卡顿结束,把新值写入数据库。
5.一段时间之后,比如:500ms,请求d将缓存删除。

这样看确实解决了缓存不一致的问题,但是为什么我们非得等一会在删除缓存呢?
请求d卡顿结束,把新值写入数据库后,请求c将数据库中的旧值,更新到缓存中。
此时,如果请求d删除太快,在请求c将数据库中的旧值更新到缓存之前,就已经把缓存删除了,这次删除就没任何意义。我们必须要搞清楚,我们之所以要再删除一次缓存的原因是因为c请求导致缓存中更新了数据库中旧值,我们需要把这个旧值删除掉,所以必须要在请求c更新缓存之后,再删除缓存,才能把旧值及时删除了,删除删除太快,可能后面。

现在解决了一个问题之后,又遇到一个问题:如果第二次删除缓存时,删除失败了该怎么办呢?

4.先跟新数据库,后删除缓存

这一种情况也会出现问题,比如更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功,那么此时再读取缓存的时候每次都是错误的数据了。

在这里插入图片描述

此时解决方案就是利用消息队列进行删除的补偿。具体的业务逻辑用语言描述如下:

  1. 请求 A 先对数据库进行更新操作
  2. 在对 Redis 进行删除操作的时候发现报错,删除失败
  3. 此时将Redis 的 key 作为消息体发送到消息队列中
  4. 系统接收到消息队列发送的消息后再次对 Redis 进行删除操作
  5. 但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对 Mysql 数据库更新操作后再 binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。

在这里插入图片描述

二十六:redis的大key有什么影响

(1)客户端超时阻塞:因为redis命令是单线程处理的,操作大Key的时候比较耗时,就会阻塞redis,从客户端这个视角来看,就是很久很久没响应

(2)引发网络阻塞:获取 bigkey 时,传输的数据量比较大,会增加带宽的压力

(3)内存分布不均匀:集群模型在slot分片均匀情况下,会出现数据和查询倾斜的情况

(1)持久化影响

首先,我们要知道redis提供了三种AOF日志回写硬盘的策略,分别是

  • always:每写入AOF文件数据后,就会执行fysnc()函数
  • Everysec:创建一个异步策略,每秒执行一次fsync() 函数
  • No策略:永不执行fsync()函数

如果使用的是Always策略,如果写入的是一个大Key,那么主线在执行fynsc()函数的时候,阻塞的时间就会比较长,因为当写入的数据很大的时候,那么数据同步到这个硬盘还是很耗时的

当使用Everysec策略的时候,由于是异步执行fsync()函数,所以大key对持久化没用影响

当使用No策略的时候,永不执行fsync()函数,所以大Key持久化过程不影响主线

AOF的重写机制和RDB快照(besave命令)的过程,都会通过一个fork()函数创建一个子进程来处理任务,有两个阶段会导致阻塞

  1. 创建子进程途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞时间越长
  2. 创建完子进程后,如果父进程修改了共享数据中的大Key,就会发生写时复制,这个期间会拷贝物理内存,如果物理内存越大,那么阻塞的时间自然越长
Last modification:October 9, 2023
如果觉得我的文章对你有用,请收藏本站