1. Redis

1.1.1. Redis的过期失效机制?

点击显示

scan扫描+给每个key存储过期时间戳

叶落山城秋: Redis过期键删除使用惰性删除(消极删除)和定时删除(积极删除)两种策略来删除过期键,

惰性删除就是一旦访问到过期键(所有的读写数据库的命令,set,lrange,sadd,hget,keys 等等),expireIfNeeded函数先判断键是否过期,如果过期就删除,没有过期就正常返回!

定时删除就是activeExpireCycle 函数被调用,规定时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键

2019-12-18补充几个(因为我确实被问到了,如果内存满了..redis怎么清理的..而之前我就记得上面的定时删除和扫描时间戳删除...)

  • 惰性清除。在访问key时,如果发现key已经过期,那么会将key删除。

  • 定时清理。Redis配置项hz定义了serverCron任务的执行周期,默认每次清理时间为25ms,每次清理会依次遍历所有DB,从db随机取出20个key,如果过期就删除,如果其中有5个key过期,那么就继续对这个db进行清理,否则开始清理下一个db。

  • 当执行写入命令时,如果发现内存不够,那么就会按照配置的淘汰策略清理内存,淘汰策略主要由以下几种

    1. noeviction,不删除,达到内存限制时,执行写入命令时直接返回错误信息。

    2. allkeys-lru,在所有key中,优先删除最少使用的key。(适合请求符合幂定律分布,也就是一些键访问频繁,一部分键访问较少)

    3. allkeys-random,在所有key中,随机删除一部分key。(适合请求分布比较平均)

    4. volatile-lru,在设置了expire的key中,优先删除最少使用的key。

    5. volatile-random,在设置了expire的key中,随机删除一部分key。

    6. volatile-ttl,在设置了expire的key中,优先删除剩余时间段的key。

    4.0版本后增加以下两种:

    1. volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰。

    2. allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key。

LRU算法

LRU算法的设计原则是如果一个数据近期没有被访问到,那么之后一段时间都不会被访问到。所以当元素个数达到限制的值时,优先移除距离上次使用时间最久的元素。 可以使用HashMap+双向链表Node来实现,每次访问元素后,将元素移动到链表尾部,当元素满了时,将链表头部的元素移除。 使用单向链表能不能实现呢,也可以,单向链表的节点虽然获取不到pre节点的信息,但是可以将下一个节点的key和value设置在当前节点上,然后把当前节点的next指针指向下下个节点。

LFU算法

LFU算法的设计原则时,如果一个数据在最近一段时间被访问的时次数越多,那么之后被访问的概率会越大,实现是每个数据 都有一个引用计数,每次数据被访问后,引用计数加1,需要淘汰数据时,淘汰引用计数最小的数据。在Redis的实现中, 每次key被访问后,引用计数是加一个介于0到1之间的数p,并且访问越频繁p值越大,而且在一定的时间间隔内, 如果key没有被访问,引用计数会减少

1.1.2. Redis持久化方案aof的默认fsync时间是多长?

点击显示

1s

叶落山城秋: aof 意思是 append only file,只允许追加不允许改写的文件, 这种存储机制是 某些写操作会把命令以协议内容追加到aof_buf缓冲区的末尾,在服务器每次结束一个事件循环之前,会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到aof文件里面,这个函数由appendfsync选项来决定,用户可以配置这个选项

always: 将 aof_buf 缓冲区中的所有内容写入并同步到AOF文件

everysec(默认): 将aof_buf 缓冲区中的所有文件写入到AOF文件,如果上次同步AOF文件的时间距离现在超过1秒钟,那么再次对AOF文件进行同步,并且这个同步操作是由一个线程专门负责执行的

no: 将aof_buf 缓冲区中的所有内容写入到AOF文件,但并不对AOF文件进行同步,何时间同步由操作系统决定

以上三种里 写入和同步 开始不理解,搜索了一下, 操作系统的写入(write) 应该也是 从缓冲区 写入到 文件中.. 所以上面写入并不一定已经写入到了磁盘的AOF文件里..

WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。

SAVE:根据条件,调用 fsyncfdatasync 函数,将 AOF 文件保存到磁盘中。

1.1.3. Redis持久化方案rdb和aof的区别?

点击显示
  • aof文件比rdb更新的频率高,优先使用aof还原数据
  • aof比rdb更安全也更大
  • rdb性能比aof好
  • 如果两个都配了优先加载aof

  • AOF因为是保存了所有执行的修改命令,粒度更细,进行数据恢复时,恢复的数据更加完整,但是由于需要对所有命令执行一遍,效率比较低,同样因为是保存了所有的修改命令,同样的数据集,保存的文件会比RDB大,而且随着执行时间的增加,AOF文件可能会越来越大,所有会通过执行BGREWRITEAOF命令来重新生成AOF文件,减小文件大小。

  • Redis服务器故障重启后,默认恢复数据的方式首选是通过AOF文件恢复,其次是通过RDB文件恢复。

  • RDB是保存某一个时间点的所有键值对信息,所以恢复时可能会丢失一部分数据,但是恢复效率会比较高

1.1.4. Redis的aof后台重写的触发条件?

点击显示
  • 可以由用户通过调用 BGREWRITEAOF手动触发
  • 服务器在AOF功能开启的情况下,会维持以下三个变量
    • 记录当前AOF文件大小的变量aof_current_size
    • 记录最后一次AOF重写之后,AOF文件大小的变量 aof_rewrite_base_size
    • 增长百分比变量 aof_rewrite_perc
  • 每次当 serverCron函数执行时,它都会检查以下条件是否全部满足,如果是的话,就会触发AOF重写
    • 没有BGSAVE命令在进行
    • 没有BGREWRITEAOF在进行
    • 当前AOF文件大小大于server.aof_rewrite_min_size (默认值1MB)
    • 当前AOF文件大小和最后一次AOF重写后的大小之间的比率大于等于指定的增长百分比(默认百分比为100%)

1.1.5. 简单介绍下什么是缓存击穿, 缓存穿透, 缓存雪崩? 能否介绍些应对办法?

点击显示
  • 缓存穿透

    • 解释:

      缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞

    • 解决:

      有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

  • 缓存雪崩

    • 解释:

      缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

    • 解决:

      缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件

  • 缓存击穿

    • 解释:

      对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。

      缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮

    • 解决:

解决方案 优点 缺点
简单分布式互斥锁(mutex key) 1. 思路简单
2. 保证一致性
1. 代码复杂度增大
2. 存在死锁的风险
3. 存在线程池阻塞的风险
“提前”使用互斥锁 1. 保证一致性 同上
不过期 1. 异步构建缓存,不会阻塞线程池 1. 不保证一致性
2. 代码复杂度增大(每个value都要维护一个timekey)
3. 占用一定的内存空间(每个value都要维护一个timekey)
资源隔离组件hystrix 1. hystrix技术成熟,有效保证后端
2. hystrix监控强大
1. 部分访问存在降级策略。

参考: 缓存穿透,缓存击穿,缓存雪崩解决方案分析

1.1.6. 解释下RESP?

点击显示

Redis Serialization Protocol --- redis序列化协议

RESP是redis客户端和服务端之间使用的一种通讯协议, 特点: 实现简单,快速解析,可读性好

RESP可以用于序列化不同的数据类型,如:整型,字符串,数组..并且为错误提供专门的类型,客户端发送请求时,以字符串数组的作为待执行命令的参数,redis服务器根据不同的命令返回不同的类型

RESP是二进制安全协议,并且处理批量数据无序逐个请求处理,因此批量数据传输时,在请求参数中添加了数据长度作为前缀,传输层基于TCP协议,默认端口为6379

RESP协议仅用作redis客户端和服务端之间通信,redis集群节点之间使用另一种二进制协议进行数据交换

RESP支持五种数据类型:

  • 简单字符串类型(Simple Strings)

    简单字符串以+开头

  • 错误类型 (Errors)

    错误数据以-开头

  • 整型(Integers)

    整数以:开头

  • 批量字符串类型(Bulk Strings)

    批量字符串以$开头

  • 数组类型(Arrays)

    数组以*开头

参考: https://juejin.im/entry/5b5583c2e51d4534c34a33b6

1.1.7. Redis有哪些架构模式,它们有什么特别点?

点击显示
  • 单机版
    • 内存容量有限
    • 处理能力有限
    • 无法高可用
  • 主从复制

    Redis的复制(replication)功能允许用户根据一个 Redis 服务器来创建任意多个该服务器的复制品,

    其中被复制的服务器为主服务器(master),而通过复制创建出来的服务器复制品则为从服务器(slave)。

    只要主从服务器之间的网络连接正常,主从服务器两者会具有相同的数据,主服务器就会一直将发生在自己身上的数据更新同步 给从服务器,从而一直保证主从服务器的数据相同。

    特点:

    • master/slave 角色
    • master/slave 数据相同
    • 降低 master 读压力在转交从库

    问题:

    • 无法保证高可用
    • 没有解决master写的压力
  • 哨兵

    Redis sentinel 是一个分布式系统中监控redis主从服务器,并在主服务器下线时自动进行故障转移,其中三个特性:

    监控(Monitoring): Sentinel 会不断的检查你的主服务器和从服务器是否运作正常

    提醒(Notification): 当被监控的某个redis服务器出现问题时,Sentinel可以通过API向管理员或者其他应用程序发送通知

    自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时,Sentinel会开始一次自动故障迁移操作

    特点:

    • 保证高可用
    • 监控各个节点
    • 自动故障迁移

    缺点:

    • 主从模式,切换需要时间丢数据
    • 没有解决master写的压力
  • 集群(proxy型)

    Twemproxy 是一个Twitter开源的一个redis和memcache 快速/轻量级代理服务器,Twemproxy是一个快速的单线程代理程序,支持 Memcached ASCII 协议和redis协议

    特点:

    • 多种hash算法: MD5、CRC16、CRC32、CRC32a、hsieh、murmur、Jenkins
    • 支持失败节点自动删除
    • 后端Sharding 分片逻辑对业务透明,业务方的读写方式和操作单个Redis一致

    缺点:

    • 增加了新的proxy,需要维护其高可用
    • failover 逻辑需要自己实现,其本身不能支持故障的自动转移可扩展性差,进行扩缩容都需要手动干预
  • 集群(redis cluster)

    从redis 3.0之后版本支持redis-cluster集群,Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。

    特点:

    • 无中心结构(不存在哪个节点影响性能瓶颈),少了proxy层
    • 数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布
    • 可扩展性,可线性扩展到1000个节点,节点可动态添加和删除
    • 高可用性,部分节点不可用时,集群仍可用,通过添加Slave做备份数据副本
    • 实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升

    缺点:

    • 资源隔离性较差,容易出现相互影响的情况
    • 数据通过异步复制,不保证数据的强一致性

以上摘自: https://www.jianshu.com/p/9ae00c4dc4cd

1.1.8. 什么是哈希槽?

点击显示

从Redis3.0之后的版本支持redis-cluster集群,Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接

结构特点:

  • 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽
  • 节点的fail是通过集群中超过半数的节点检测失效时才生效
  • 客户端与redis节点直连,不需要中间proxy层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
  • redis-cluster把所有的物理节点映射到[0~16383]slot上(不一定平均分配),cluster负责维护node<->slot<->value
  • Redis集群预分好16384个桶,当需要在Redis集群中放置一个 key-value 时,根据 CRC16(key) mod 16384的值,决定将一个key放到哪个桶中

基本思想:

  • 一共有16384个槽,每台服务器分管其中的一部分
  • 插入一个数据的时候,先根据CRC16算法计算key对应的值,然后用该值对16384取余数,确定将数据放到哪个槽里面
  • 在增加的时候,之前的节点个字分出一些槽给心节点,对应的数据也一起迁出
  • 客户端可以向任何一个Redis节点发送请求,然后由节点将请求重定向到正确的节点上

    为什么要选择的槽是16384个呢? crc16会输出16bit的结果,可以看作是一个分布在0-2^16-1之间的数,redis的作者测试发现这个数对2^{14}求模的会将key在0-2^{14-1}之间分布得很均匀,因此选了这个值。

参考: https://www.jianshu.com/p/fa623e59fdcf

参考: https://blog.csdn.net/z15732621582/article/details/79121213

1.1.9. 什么是CAP?

点击显示
  • 一致性(Consistency)

    在分布式系统中的所有数据备份,在同一时刻是否同样的值.(等同于所有节点访问同一份最新的数据副本)

  • 可用性(Availability)

    在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求.(对数据更新具备高可用性)

  • 分区容错性(Partition tolerance)

    以实际效果而言,分区相当于对通信的时限要求,系统如果不能在时限内达成数据的一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择

CAP理论就是说在分布式存储系统中,最多只能实现上面的两点!

1.1.10. 乐观锁,悲观锁?

点击显示

悲观锁(Pessimistic Lock) 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

1.1.11. Redis五大对象类型和其底层实现?

不骗你们,这题必考!!!

点击显示
对象 对象type属性的值 Type命令的输出 编码常量 编码对应的数据结构 编码对象
字符串对象 REDIS_STRING "string" REDIS_ENCODING_INT long类型的整数 使用整数值实现的字符串对象
REDIS_ENCODING_EMBSTR embstr编码的简单动态字符串 使用embstr编码的简单动态字符串实现的字符串对象
REDIS_ENCODING_RAW 简单动态字符串 使用简单动态字符串实现的字符串对象
列表对象 REDIS_LIST "list" REDIS_ENCODING_ZIPLIST 压缩列表 使用压缩列表实现的列表对象
REDIS_ENCODING_LINKEDLIST 双端链表 使用双端链表实现的列表对象
哈希对象 REDIS_HASH "hash" REDIS_ENCODING_ZIPLIST 压缩列表 使用压缩列表实现的哈希对象
REDIS_ENCODING_HT 字典 使用字典实现的哈希对象
集合对象 REDIS_SET "set" REDIS_ENCODING_INTSET 整数集合 使用整数集合实现的集合对象
REDIS_ENCODING_HT 字典 使用字典实现的集合对象
有序集合对象 REDIS_ZSET "zset" REDIS_ENCODING_ZIPLIST 压缩列表 使用压缩列表实现的有序集合对象
REDIS_ENCODING_SKIPLIST 跳跃表和字典 使用跳跃表和字典实现的有序集合对象
  • 查看类型

命令: OBJECT ENCODING + key

  • 字符串对象

    • int
      • 如果一个字符串对象保存的是整数值,并且这个值可以用long类型表示,那么字符串对象会将整数存在字符串对象结构的ptr属性里面,并将字符串对象的编码设置为int
    • raw
      • 如果字符串对象保存的是一个字符串值,且字符串值的长度大于32字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置成raw
    • embstr
      • 如果字符串对象保存的是一个字符串值,并且这字符串长度小于等于32字节,那么字符串对象将embstr编码的方式来保存这个值
      • embstr编码是专门用于保存短字符串的一种优化方式,编码跟raw一样,都使用redisObject和sdshdr结构来表示字符串对象,但raw编码会调用分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一个连续空间,空间中依次包含redisObject和sdshsr两结构
      • 好处:
        • embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次
        • 释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数
        • 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里,所以这种编码的字符串对象比起raw编码的字符串对象能够更好的利用缓存带来优势
    • 字符串总结:
      • embstr和raw编码
        • 两种存储方式下,都RedisObject和SDS结构(简单动态字符串)来存储字符串,区别在于,embstr对象用于存储较短的字符串,embstr编码中RedisObject结构与ptr指向的SDS结构在内存中是连续的,内存分配次数和内存释放次数均是一次,而raw编码会分别调用两次内存分配函数来分别创建RedisObject结构和SDS结构。
  • 列表对象

    • ziplist(压缩列表)
      • 介绍: 压缩列表
      • 每个压缩列表节点(entry)保存了一个列表元素
    • linkedlist(双端链表)
      • 每个双端链表节点(node)都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素
    • 列表对象总结:
      • 底层数据结构是一个链表,插入和删除很快,随机访问较慢,时间复杂度是O(N)
      • Redis中的List可以作为一个队列来使用,也可以作为一个栈来使用。在实际使用中,常用来做异步队列使用,将可以延时处理的任务序列化成字符串塞进Redis的列表,另外一个线程从列表中轮询数据进行处理
      • 老版本中的Redis,元素较少时,使用ziplist来作为底层编码,元素较多时使用双向链表linkedList作为底层编码。因为链表每个节点需要prev,next指针,需要占用16字节,而且每个节点内存都是单独分配,加剧内存碎片化,所以新版本使用quiklist作为底层编码,quiklist的是一个双向链表,但是它的每一个节点是一个ziplist。(默认每个ziplist最大长度为8k字节)
  • 哈希对象

    • ziplist(压缩列表)
      • 每当有新的键值对要假如到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再保存了值的压缩列表节点推入到压缩列表表尾
        • 保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后
        • 先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向
    • hashtable(哈希表)
      • 哈希对象中的每个键值对都使用一个字典键值对来保存
        • 字典的每个都是一个字符串对象,对象中保存了键值对的
        • 字典的每个都是一个字符串对象,对象中保存了键值对的
    • 哈希对象总结
      • value可以是一个hash表,底层编码可以是ziplist,也可以是hashtable(默认情况下,当元素小于512个时,底层使用ziplist存储数据)
      • 元素保存的字符串长度较短且元素个数较少时(小于64字节,个数小于512),出于节约内存的考虑,hash表会使用ziplist作为的底层实现,ziplist是一块连续的内存,里面每一个节点保存了对应的key和value,然后每个节点很紧凑地存储在一起,优点是没有冗余空间,缺点插入新元素需要调用realloc扩展内存。(可能会进行内存重分配,将内容拷贝过去,也可能在原有地址上扩展)。
      • 元素比较多时就会使用hashtable编码来作为底层实现,这个时候RedisObject的ptr指针会指向一个dict结构,dict结构中的ht数组保存了ht[0]和ht[1]两个元素,通常使用ht[0]保存键值对,ht[1]只在渐进式rehash时使用。hashtable是通过链地址法来解决冲突的,table数组存储的是链表的头结点(添加新元素,首先根据键计算出hash值,然后与数组长度取模之后得到数组下标,将元素添加到数组下标对应的链表中去)
  • 集合对象

    • intset(整数集合)
      • 集合对象包含的所有元素都被保存在整数集合里面
    • hashtable(哈希表)
      • 字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL
    • 集合对象
      • 当元素都为整数,且元素个数较少时会使用inset作为底层编码,inset结构中的有一个contents属性,content是是一个整数数组,从小到大保存了所有元素。
      • 当元素个数较多时,Set使用hashtable来保存元素,元素的值作为key,value都是NULL。
  • 有序集合对象
    • ziplist(压缩列表)
      • 每个集合元素使用两个紧挨在一起的压缩列表来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)
      • 压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向
    • skiplist(跳跃表)
      • 介绍: 跳跃表
      • 一个zset结构同时包含一个字典和一个跳跃表
    • 有序集合总结:
      • Zset与Set的区别在于每一个元素都有一个Score属性,并且存储时会将元素按照Score从低到高排列。
      • 当元素较少时,ZSet的底层编码使用ziplist实现,所有元素按照Score从低到高排序。
      • 当元素较多时,使用skiplist+dict来实现,
      • skiplist存储元素的值和Score,并且将所有元素按照分值有序排列。便于以O(logN)的时间复杂度插入,删除,更新,及根据Score进行范围性查找。
      • dict存储元素的值和Score的映射关系,便于以O(1)的时间复杂度查找元素对应的分值。

以上参考 < redis设计与实现 > 一书

1.1.12. 怎么保持Redis缓存里的数据与数据库里的一致?

如果上面那个被问到了,那么这个就比问...

点击显示

读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。

不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。

举一个例子:

  1. 如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
  2. 如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。

    因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。如来解决?这里给出两个解决方案,先易后难,结合业务和技术代价选择使用。

解决方案
第一种方案:采用延时双删策略

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

伪代码如下

 public void write( String key, Object data )
 {
     redis.delKey( key );
     db.updateData( data );
     Thread.sleep( 500 );
     redis.delKey( key );
 }
  • 具体的步骤就是:

    • 先删除缓存
    • 再写数据库
    • 休眠500毫秒
    • 再次删除缓存

    那么,这个500毫秒怎么确定的,具体该休眠多久呢?

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

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

  • 设置缓存过期时间

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

  • 该方案的弊端

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

叶落山城秋: 这种方案,还是有误差延时的,对于秒杀这种操作肯定是不行的! 另外,这种操作的流程是 先删除缓存,如果这时候有请求进来了

数据库还没更新操作,这时候如果量比较大可能会发生缓存穿透, 不过可能是单点穿透,这时候又对key写入了以前的值!

此时数据库更新了! 500毫秒后 再次删掉之前的key,重新穿透再缓存一次!

第二种方案:异步更新缓存(基于订阅binlog的同步机制)
  • 技术整体思路:(MySQL binlog增量订阅消费+消息队列+增量数据更新到redis)

    • 读Redis:热数据基本都在Redis
    • 写MySQL: 增删改都是操作MySQL
    • 更新Redis数据:MySQ的数据操作binlog,来更新到Redis
  • Redis更新

    • 数据操作主要分为两大块:
      • 一个是全量(将全部数据一次写入到redis)
      • 一个是增量(实时更新)(这里说的是增量,指的是mysql的update、insert、delate变更数据。)
    • 读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。

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

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

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

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

以上摘自: Redis和mysql数据怎么保持数据一致的?

1.1.13. Redis的持久化是怎么实现的?

点击显示

Redis主要通过AOF和RDB实现持久化。

AOF持久化主要是Redis在修改相关的命令后,将命令添加到aof_buf缓存区的末尾,然后在每次事件循环结束时,根据appendfsync的配置(always是总是写入,everysec是每秒写入,no是根据操作系统来决定何时写入),判断是否需要将aof_buf写入AOF文件。

RDB持久化指的是在满足一定的触发条件时(在一个的时间间隔内执行修改命令达到一定的数量,或者手动执行SAVE和BGSAVE命令),对这个时间点的数据库所有键值对信息生成一个压缩文件dump.rdb,然后将旧的删除,进行替换。

实现原理是fork一个子进程,然后对键值对进行遍历,生成rdb文件,在生成过程中,父进程会继续处理客户端发送的请求,当父进程要对数据进行修改时,会对相关的内存页进行拷贝,修改的是拷贝后的数据。(也就是COPY ON WRITE,写时复制技术,就是当多个调用者同时请求同一个资源,如内存或磁盘上的数据存储,他们会共用同一个指向资源的指针,指向相同的资源,只有当一个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给这个调用者,其他调用者还是使用最初的资源,在ArrayList的实现中,也有用到,添加一个新元素时过程是,加锁,对原数组进行复制,然后添加新元素,然后替代旧数组,解锁)

1.1.14. 怎么防止AOF文件越来越大?

点击显示

为了防止AOF文件越来越大,可以通过执行BGREWRITEAOF命令,会fork子进程出来,读取当前数据库的键值对信息,生成所需的写命令,写入新的AOF文件。

在生成期间,父进程继续正常处理请求,执行修改命令后,不仅会将命令写入aof_buf缓冲区,还会写入重写aof_buf缓冲区。

当新的AOF文件生成完毕后,子进程父进程发送信号,父进程将重写aof_buf缓冲区的修改命令写入新的AOF文件,写入完毕后,对新的AOF文件进行改名,原子地(atomic)地替换旧的AOF文件。

1.1.15. Redis持久化策略该如何进行选择?

点击显示

RDB持久化的特点是:文件小,恢复快,不影响性能,实时性低,兼容性差(老版本的Redis不兼容新版本的RDB文件)

AOF持久化的特点是:文件大,恢复慢,性能影响大,实时性高。是目前持久化的主流(主要是当前项目开发不太能接受大量数据丢失的情况)。

需要了解的是持久化选项的开启必然会造成一定的性能消耗

RDB持久化主要在于bgsave在进行fork操作时,会阻塞Redis的主线程。以及向硬盘写数据会有一定的I/O压力。

AOF持久化主要在于将aof_buf缓冲区的数据同步到磁盘时会有I/O压力,而且向硬盘写数据的频率会高很多。

其次是,AOF文件重写跟RDB持久化类似,也会有fork时的阻塞和向硬盘写数据的压力。

以下是几种持久化方案选择的场景:

  1. 不需要考虑数据丢失的情况,那么不需要考虑持久化。

  2. 单机实例情况下,可以接受丢失十几分钟及更长时间的数据,可以选择RDB持久化,对性能影响小,如果只能接受秒级的数据丢失,只能选择AOF持久化。

  3. 在主从环境下,因为主服务器在执行修改命令后,会将命令发送给从服务器,从服务进行执行后,与主服务器保持数据同步,实现数据热备份,在master宕掉后继续提供服务。同时也可以进行读写分离,分担Redis的读请求。

    那么在从服务器进行数据热备份的情况下,是否还需要持久化呢?

    • 需要持久化,因为不进行持久化,主服务器,从服务器同时出现故障时,会导致数据丢失。(例如:机房全部机器断电)。如果系统中有自动拉起机制(即检测到服务停止后重启该服务)将master自动重启,由于没有持久化文件,那么master重启后数据是空的,slave同步数据也变成了空的。应尽量避免“自动拉起机制”和“不做持久化”同时出现。

    所以一般可以采用以下方案:

    • 主服务器不开启持久化,使得主服务器性能更好。

    • 从服务器开启AOF持久化,关闭RDB持久化,并且定时对AOF文件进行备份,以及在凌晨执行bgaofrewrite命令来进行AOF文件重写,减小AOF文件大小。(当然如果对数据丢失容忍度高也可以开启RDB持久化,关闭AOF持久化)

  4. 异地灾备,一般性的故障(停电,关机)不会影响到磁盘,但是一些灾难性的故障(地震,洪水)会影响到磁盘,所以需要定时把单机上或从服务器上的AOF文件,RDB文件备份到其他地区的机房。

1.1.16. AOF文件追加阻塞是什么?

点击显示

修改命令添加到aof_buf之后,如果配置是everysec那么会每秒执行fsync操作,调用write写入磁盘一次,但是如果硬盘负载过高,fsync操作可能会超过1s,Redis主线程持续高速向aof_buf写入命令,硬盘的负载可能会越来越大,IO资源消耗更快,所以Redis的处理逻辑是会对比上次fsync成功的时间,如果超过2s,则主线程阻塞直到fsync同步完成,所以最多可能丢失2s的数据,而不是1s。

1.1.17. Redis为什么是单线程的?

经典题了..最近某大厂也被问了..不过可能还会继续问,如果多线程,会有什么影响之类的

点击显示
  • Redis官方FAQ回答: Redis是基于内存的操作,CPU不会成为瓶颈所在,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。 (这里的单线程指的是处理网络请求的模块是单线程,其他模块不一定是单线程的)

  • Redis采用单线程的优势:

    1. Redis项目的代码会更加清晰,处理逻辑会更加简单。

    2. 不用考虑多个线程修改数据的情况,修改数据时不用加锁,解锁,也不会出现死锁的问题,导致性能消耗。

    3. 不存在多进程或者多线程导致的切换而造成的一些性能消耗。

  • Redis采用单线程的劣势:

    1.无法充分发挥多核机器的优势,不过可以通过在机器上启动多个Redis实例来利用资源。

1.1.18. Redis性能为什么高?

点击显示

根据官网的介绍,Redis单机可以到到10W的QPS(每秒处理请求数),Redis这么快的原因主要有以下几点:

  1. 完全基于内存,数据全部存储在内存中,读取时没有磁盘IO,所以速度非常快。

  2. Redis采用单线程的模型,没有上下文切换的开销,也没有竞态条件,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。

  3. Redis项目中使用的数据结构都是专门设计的,例如SDS(简单动态字符串)是对C语言中的字符串频繁修改时,会频繁地进行内存分配,十分消耗性能,而SDS会使用空间预分配和惰性空间释放来避免这些问题的出现。

    空间预分配技术: 对SDS进行修改时,如果修改后SDS实际使用长度为len,

    当len<1M时,分配的空间会是2*len+1,也就是会预留len长度的未使用空间,其中1存储空字符

    当len>1M时,分配的空间会是len+1+1M,也就是会预留1M长度的未使用空间,其中1存储空字符

  4. 采用多路复用IO模型,可以同时监测多个流的IO事件能力,在空闲时,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态唤醒,轮询那些真正发出了事件的流,并只依次顺序的处理就绪的流。可以让单个线程高效的处理多个连接请求(尽量减少网络 I/O 的时间消耗)。

1.1.19. Redis主从同步是怎么实现的?

点击显示

主从节点建立连接后,从节点会进行判断

  1. 如果这是从节点之前没有同步过数据,属于初次复制,会进行全量重同步 那么从节点会向主节点发送PSYNC?-1 命令,请求主节点进行全量重同步。

  2. 如果这是从节点不说初次复制(例如出现掉线后重连),

    这个时候从节点会将之前进行同步的Replication ID(一个随机字符串,标识主节点上的特定数据集)和offset(从服务器当前的复制偏移量)通过PSYNC 命令发送给主节点,主节点会进行判断,

    • 如果Replication ID跟当前的Replication ID不一致,或者是当前buffer缓冲区中不存在对应的offset,那么会跟上面的初次复制一样,进行全量重同步。
    • 如果Replication ID跟当前的Replication ID一致并且当前buffer缓冲区中存在对应的offset,那么会进行部分重同步。(部分重同步是Redis 2.8之后的版本支持的,主要基于性能考虑,为了断线期间的小部分数据修改进行全量重同步效率比较低)
  3. 全量重同步

    主节点会执行BGSAVE命令,fork出一个子进程,在后台生成一个RDB持久化文件,完成后,发送给从服务器,从节点接受并载入RDB文件,使得从节点的数据库状态更新至主节点执行BGSAVE命令时的状态。并且在生成RDB文件期间,主节点也会使用一个缓冲区来记录这个期间执行的所有写命令,将这些命令发送给从节点,从节点执行命令将自己数据库状态更新至与主节点完全一致。

  4. 部分重同步

    因为此时从节点只是落后主节点一小段时间的数据修改,并且偏移量在复制缓冲区buffer中可以找到,所以主节点把从节点落后的这部分数据修改命令发送给从节点,完成同步。

  5. 命令传播

    在执行全量重同步或者部分重同步以后,主从节点的数据库状态达到一致后,会进入到命令传播阶段。主节点执行修改命令后,会将修改命令添加到内存中的buffer缓冲区(是一个定长的环形数组,满了时就会覆盖前面的数据),然后异步地将buffer缓冲区的命令发送给从节点。

1.1.20. Redis中哨兵是什么?

点击显示

Redis中的哨兵服务器是一个运行在哨兵模式下的Redis服务器,核心功能是监测主节点和从节点的运行情况,在主节点出现故障后, 完成自动故障转移,让某个从节点升级为主节点。

1.1.21. Redis哨兵系统是怎么实现自动故障转移的?

点击显示
  1. 认定主节点主观下线

    因为每隔2s,哨兵节点会给主节点发送PING命令,如果在一定时间间隔内,都没有收到回复,那么哨兵节点就认为主节点主观下线。

  2. 认定主节点客观下线

    哨兵节点认定主节点主观下线后,会向其他哨兵节点发送sentinel is-master-down-by-addr命令,获取其他哨兵节点对该主节点的状态,当认定主节点下线的哨兵数量达到一定数值时,就认定主节点客观下线。

  3. 进行领导者哨兵选举

    认定主节点客观下线后,各个哨兵之间相互通信,选举出一个领导者哨兵,由它来对主节点进行故障转移操作。

    选举使用的是Raft算法,基本思路是所有哨兵节点A会先其他哨兵节点,发送命令,申请成为该哨兵节点B的领导者,如果B还没有同意过其他哨兵节点,那么就同意A成为领导者,最终得票超过半数以上的哨兵节点会赢得选举,如果本次投票,没有选举出领导者哨兵,那么就开始新一轮的选举,直到选举出哨兵节点(实际开发中,最先判定主节点客观下线的哨兵节点,一般就能成为领导者。)

  4. 领导者哨兵进行故障转移

    领导者哨兵节点首先会从从节点中选出一个节点作为新的主节点。选择的规则是:

    1. 首先排除一些不健康的节点。(下线的,断线的,最近5s没有回复哨兵节点的INFO命令的,与旧的主服务器断开连接时间较长的)
    2. 然后根据优先级,复制偏移量,runid最小,来选出一个从节点作为主节点。

    向这个从节点发送slaveof no one命令,让其成为主节点,通过slaveof 命令让其他从节点成为它的从节点,将已下线的主节点更新为新的主节点的从节点。

1.1.22. 为什么不用红黑树作为zset底层实现?

点击显示

They are not very memory intensive. It's up to you basically.

  • 缺点:

    • 比红黑树占用更多的内存,每个节点的大小取决于该节点的层数
    • 空间局部性较差导致缓存命中率低,感觉上会比红黑树更慢
  • 优点:

    • 实现比红黑树简单
    • 比红黑树更容易扩展,作者之后实现zrank指令时没怎么改动代码。
    • 红黑树插入删除时为了平衡高度需要旋转附近节点,高并发时需要锁。skiplist不需要考虑。
    • 一般用zset的操作都是执行zrange之类的操作,取出一片连续的节点。这些操作的缓存命中率不会比红黑树低。

以上部分资源来自网络

参考: go 夜读

redis的 悲观锁和乐观锁的区别

透过面试题掌握Redis

欢迎加入PHP交流群(QQ群号):PHP编程技术交流群 440221268

欢迎加入Golang交流群(QQ群号):PHP/Golang技术交流群 423069874

results matching ""

    No results matching ""