Redis常见面试题

356 阅读29分钟

Redis有什么作用?为什么要用Redis/缓存?

  • 访问速度快:传统数据库将数据存储到磁盘上,而使用redis就可以将高频访问的数据放到redis中,直接从内存中读取。
  • 高并发:mysql数据的并发能力有限,使用缓存接收的数据库请求数量远大于直接访问数据库的。
  • 功能全面:除了缓存,还可以提供分布式锁、消息队列、限流、延时队列等场景。
  • 使用Reids,不使用本地缓存的原因:
特性本地缓存Redis缓存
数据一致性多服务器部署时存在数据不一致数据一致
内存限制受限于单台服务器内存限制独立部署,内存空间大
数据丢失问题服务宕机,数据丢失可持久化,不易丢失
管理维护分散不易维护集中管理,有丰富的管理工具

Redis除了做缓存之外,还能做什么?

  • 分布式锁:用redis来做分布式锁是比较常见的方式, 通过Redisson来是实现分布式锁
  • 限流: 可以通过Redis+Lua脚本来实现限流,或者可以通过Redisson的RRateLimiter来实现分布式限流,底层就是基于Lua代码+令牌桶来实现。
  • 消息队列:Redis自带数据结构List可以做队列,5.0之后增加了stream 数据结构更适合做消息队列。类似于Kafka, 有主题、消费者组的概念,支持消息持久化以及ACK机制。、
  • 延时队列: Redisson支持延迟队列(基于 Sorted Set 机制实现 )
  • 分布式Session: 利用String或Hash存储 session数据,所有服务器都可访问。

如何基于Redis实现延时队列

  • Redis实现延迟任务有两种方法:
    • Redis过期事件监听
    • Redisson 内置延时队列

1.Redis过期事件监听

  • Redis 过期事件监听实现延迟任务的原理:

    • Redis有发布订阅功能(sub/pub),其中引入了channel(频道),类似于消息队列topic的概念。 pub生产者将消息发送给指定的channel, sub消费者则订阅对应的channel获取消息
      • 发布者通过PUBLISH投递消息给指定channel
      • 消费者通过SUBSCRIBE订阅指定的channel, 并且订阅者可以订阅一个或多个channel
    • Redis中有些默认的channel, 这些channel是Redis本身向它们发送消息,不是我们编写的代码;其中_keyevent@0__:expired,就是一个默认channel,负责监听key的过期事件。 也就是说,当一个key过期,redis会发送一个消息到__keyevent@0__:expired这个channel中。
    • 而我们只需要订阅这个channel,就可以实现延迟队列了
  • 缺陷:

    • 时效性差: 过期事件消息是在Redis删除key是发送的,而不是过期之后就会发送。

      • 其中Redis的过期删除策略就两个:定期删除和惰性删除,定期删除是每隔一段时间会抽出一部分有过期时间的数据进行检查,如果过期就删除;而惰性删除,只有在取出key的时候才会进行过期检查。
      • 因为两者删除策略各有千秋,所以使用定期删除+惰性删除的方案,所以就会存在,我设置了过期时间的key已经过期,但还没有删除的情况。
    • 丢消息:redis的sub/pub模式中消息没有持久化,如果没有订阅者订阅指定channel,消息就会直接被丢弃,不会存储在redis中。

    • 重复消息:redis的sub/pub模式只有广播模式,意味着消费者发送消息给指定channel, 订阅当前channel的所有订阅者都会收到消息。

2.Redisson内置延时队列

  • Redisson 内部是如何实现延时任务的?
    • 通过Redisson内部的延时队列RDelayedQueue来实现延时任务
      • RDelayedQueue延时队列基于Redis的zset有序集合来实现的,其中zset有序集合中每个元素都可以设置一个分数,来表示该元素的权重值。Redisson利用这一特性,将需要延迟执行的任务放入zset中,并给它们设置相应的过期时间作为分数。
      • Redisson在客户端其中一个定时任务,到时间后使用zrangebyscore扫描zset中的过期元素(分数小于当前时间的),然后将过期的元素删除,并加入到就绪列表中。
  • 优势:
    • 减少了丢消息的可能:RDelayedQueue的消息会被持久化,即使Redis宕机了,也可能只丢失一小部分数据,也可以用扫描数据库的方式作为补偿机制。
    • 消息不存在重复消费的可能:都是从同一个队列中获取消息的,不存在重复消费的问题。

分布式缓存常见的选型方案:

  • Memcached 和 Redis

  • 共同点:

    • 都是内存数据库,一般当缓存使用
    • 都有过期策略
    • 性能比较高
  • 区别:

    • 数据类型方面:
      • Redis支持多种数据类型,除了简单key/value,还有list,set,zset,hash等,支持复杂应用场景
      • Memcached:只支持key/value键值对的类型
    • 数据持久化:
      • Redis会将数据持久化到磁盘中,重启后会自动加到内存中,相当于有灾难恢复机制
      • Memcahced: 只存储到内存中
    • 集群支持:
      • Redis:3.0后就提供了原生的集群支持
      • Memcached: 没有提供原生的集群,依赖客户端往集群分片中写入数据
    • 线程模型:
      • Redis:使用单线程的IO多路复用模式(6.0后 网络数据的读写引入了多线程)
      • Memecahed: 多线程、非阻塞IO复用模式
    • 过期删除策略:
      • Redis: 支持惰性删除和定时删除
      • Memecache: 只支持惰性删除。

Redis常见的数据结构有哪些?底层数据结构

  • 5种基础类型:String:字符串、list:列表、set: 集合、 zset:有序集合、hash: 散列
  • 3中特殊类型:HyperLogLog:基数统计、BitMap:位图、Geospatial: 地理位置、Bloom fileter布隆过滤器

Redis的有序集合底层为什么要用跳表?

  • 跳表诞生的初衷就是为了解决平衡树的一些缺点,使用的是概率平衡而不是严格强制的平衡,因此跳表的插入和删除算法要比平衡树的等效算法简单,速度也更快

使用Redis统计网站UV怎么做?

  • 系统活跃度常用指标:

    • PV: PageView 页面浏览量,能够反映网站页面被浏览或者刷新的次数
    • UV: Unique View 用来统计1天内访问某个站点的用户数
    • VV: Visit View,用来统计1天内用户访问站点的次数
    • IP: 独立IP, 一天内访问站点的独立Ip数量。
  • 简单版本:访问量比较少时

    • 指定网页维护一个哈希表,使用网页id+日期作为key, 每个访问站点的用户ID或IP作为value
    • 当需要为指定网页增加UV时,需要先判断用户id或IP是否存在于set中
    • 计算UV只需要计算set的长度即可。
  • 内存消耗少的版本:使用HyperLogLog基数统计,是一种基数计数概率算法,占用的空间很小,并且计算的结果是近似值,有一定的误差。对于统计UV,这个误差可以忽略不计

    • 相关的操作:
      • PFADD key valus: 用于数据添加,一次可以添加多个,会自动去重。
      • PFCOUNT key: 对数据进行统计
      • PRMERGE destkey sourcekey1 sourcekey2 ...: 将多个表进行合并,可以一次添加多个表,并对重复的元素进行去重。

实现一个排行榜怎么做?

  • 使用Order by:

    • 通过直接用数据库Order by排序查询等到结果,但是对于数据量大时,性能较低。可以通过加索引并限制排序数据量。 如果实际需要排序的数据量小且业务不复杂可以使用orderby.
  • 使用Redis的 Sorted set (zet)

    • Sorted set的介绍: 相对于set,zset增加了一个score的权重参数,使得集合中元素可以按照score有序排列,可以通过score获取元素的列表
    • 常用命令:
命令介绍
ZADD key score1 memeber1 score2 memeber2 ...向有序集合中添加一个或多个元素
ZCARD key获取指定有序集合的数量
ZSCORE key memeber获取指定有序集合中指定元素的score值
ZINTERSTORE destination numkeys key1 key2 key3..将所有有序集合的交集存储到destination中,对相同元素的score值进行sum聚合操作,numkeys表示有序集合的数量
ZUNIONSTORE destination numkeys key1 key2 key3...求并集,其他与ZINTERSTORE一致
ZDIFF destination numkeys key1 key2 key3...求差集, 其他与ZINTERSTORE一致
ZRANGE key start end获取指定有序集合的start 和 end 之间的元素(score从低到高)
ZREVRANGE key start end获取指定有序集合的start 和 end 之间的元素(score从高到低)
ZREVRANK key member获取指定有序集合中指定元素的排名(score从大到小)
  • 案例:
    • 如果查看包含所有用户的排行榜? ZREVRANGE cur_order_set 0 -1
      • ZREVRANGE从大到小, ZRANGE从小到大
    • 只查看包含前三名的排行榜? ZREVRANGE cur_order_set 0 2
    • 查询某个用户的分数: ZSCORE cur_order_set "user1"
    • 查询某个用户具体的排名: ZREVRANGE cur_order_set "user1"
    • 如何对某个用户的排名数据进行更新: ZINCRBY cur_order_set +2 "user1"ZINCRBY cur_order_set -1 "user2"
    • 如何实现多条件排序: 对score做文章,比如要加上时间先后条件,直接对 score 加上时间戳即可。
    • 如何实现指定日期的用户数据排序? 数据的名字使用 时间日期, 如果要求n天的用户数据排行榜,使用ZUNIONSTORE 对这几个日期求并集即可

Redis的单线程模型了解吗?/既然是单线程,为什么可以监听多个客户端连接?/为什么Redis快?

  • Redis基于 Reactor 模式设计了文件事件处理器(file event handler)是单线程运行的,所以说Redis是单线程运行的。
  • 文件事件处理器,使用了IO多路复用,通过监听多个socket,并将感兴趣的事件以及类型(读写)注册到内核,监听这些事件是否发生。
    • 不需要多线程来监听大量的客户端连接,减低了资源消耗。
  • 文件事件处理器分为四个部分:
    • 多个socket(客户端连接)
    • IO多路复用程序(支持多个客户端连接的关键)
    • 文件事件分派器(将对应的连关联到对应的事件处理器上)
    • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

image.png

Redis6.0之前为什么不使用多线程?

  • 主要原因:

    • 单线程编程容易且更容易维护
    • Redis的性能瓶颈不在于CPU,而是在于内存和网络
    • 多线程存在死锁,线程上下文切换等问题,甚至会影响性能。
  • Redis4.0就引入了多线程:主要是对大键值对的删除操作命令,使用这些命令,就会使用主线程之外的线程,进行异步处理,从而减少对主线程的影响。

  • Redis4.0增加的异步命令:

    • UNLINK: 可以看作是 DEL 的异步版本
    • FLUSHALL ASYNC: 用于清除所有数据库中所有的键,不限于当前 SELECT 数据库
    • FLUSHDB ASYNC: 用于清除当前SELECT数据库的所有键。

Redis6.0 之后要引入多线程?

  • 引入多线程的原因:提高网路IO读写性能。 由于Redis的主要性能瓶颈在于 内存和网络。
  • 多线程只在网络读写这类耗时操作上使用,普通命令执行还是单线程,这样就不用担心线程安全问题。
  • Redis6.0 默认关闭多线程,使用主线程,如果需要使用,需要修改redis.conf文件
    • io-threads 4: 设置为1,就只会开启主线程,需要多线程需要>1

Redis给缓存数据设置过期时间有什么用?

  • 有助于缓减内存消耗(内存资源有限,如果一直增长,最终会导致OOM的)
  • 如果使用传统数据库来判断过期,还有进行判断,会很麻烦且性能会差好多。

Redis是如何判断数据是否过期的?

  • Redis中有个过期字典,键指向Redis数据库中某个键, 而value存储过期事件是longlong类型的
  • 查询一个key时,会先查询是否存储于过期字典中,如果没有就直接返回,如果有,判断是否过期,过期就删除,并返回null.

过期数据的删除策略?

  • 惰性删除: 方式时,判断是否过期,过期就删除,对CPU友好,但可能会造成大量缓存未删除
  • 定期删除:周期性的从有过期时间的缓存中抽取一批,逐个检查,过期的就删除。对内存友好,对CPU不友好
  • 延迟队列: 把设置过期时间的数据存放到延时队列中,如果过期就删除,但是维护延迟队列比较麻烦,队列本身也要占空间。
  • 定时删除:每个设置了过期时间的数据都会在到期时立即被删除。这样可以保证内存中不会存在过期数据。但是这个对CPU的压力最大,因为需要为每个键都放个定时器。
  • 最终策略:定期删除+惰性删除

内存淘汰机制了解吗

  • 内存淘汰策略是,内存中的数据达到配置的阈值时触发。通过redis.conf中的maxmemory中配置
  • 8种淘汰策略:
    • volatile-lru:Last Recently Used, 从已设置过期时间的数据中,选择最近最少使用的数据进行淘汰
    • volatile-ttl: 从已设置过期时间的数据集中,挑选将要到期的进行淘汰
    • volatile-random: 从已设置过期时间的数据集中,随机挑选数据进行淘汰
    • allkeys-lru: 从数据集中选取最近最少使用的数据进行淘汰
    • allkeys-random: 从数据集中随机选取数据进行淘汰
    • no-eviction(默认内存淘汰策略):禁止驱逐数据,当内存不足以容纳新数据时,新写入数据时会报错。
    • 4.0之后新增
      • volatile-lfu: Last-Frequently-used,从已设置过期时间的数据集中,选择最不经常使用的数据进行淘汰
      • allkeys-lfu: 从数据集中选取最不经常使用的数据进行淘汰。

怎么保证Redis重启之后,数据可以恢复?

  • 依赖数据持久化机制:Redis通过快照获取存储在内存中数据某个时间点的副本。可以将快照留在原地服务器重启时恢复数据时使用。

什么是RDB持久化?

  • RDB持久化:就是通过快照获取存储在内存中数据的某个时间点的副本。
    • 快照创建之后,可以将快照复制给其他服务器,使得其他服务器具有相同数据的副本。
    • 也可以原地保持不变,服务器重启时恢复数据时使用。
  • RDB快照是默认的持久化机制
  • Redis提供了两个命令来生成RDB快照,区别是否阻塞主线程。
    • save: 同步保存操作,会阻塞主线程
    • bgsave: 会fork一个子线程,子线程执行,不会阻塞主线程。是默认选项。

什么是AOF持久化?

  • AOF: 将每条使得redis数据发生变动的语句,写入AOF缓存区中server.aof_buf,然后写入AOF文件中,再根据持久化方式的配置,决定何时将内核缓冲区的数据同步到到磁盘中。
  • AOF文件的位置与RDB的位置相同,aof的默认文件名为:appendonly.aof

image.png

  • AOF重写:是有歧义的名字与原来的AOF文件无关,是通过读取数据库键值对来实现的,AOF变得太大时会自动重写一个新的AOF文件,与原来的AOF所保存的数据库状态一样,并且体积更小。
    • 重写期间还会维护一个 重写缓冲区,存储重写期间对数据库操作修改的操作。当子进程完成重写后,会将缓冲区的所有内容追加到AOF文件的末尾。最后用新的AOF文件替换旧的AOF文件。

Redis4.0对持久化机制做了哪些优化?

  • 由于RDB和AOF各有优势,所以4.0之后开始使用AOF+RDB的混合模式(默认关闭,通过aof-use-rdb-preamble开启)
  • 如果把混合持久化模式打开,AOF重写时就会直接把RDB内容写到AOF文件开头。结合RDB和AOF的优势,快速加载同时避免丢失过多数据。缺点是,RDB存储到AOF是压缩格式不再是AOF格式,可读性较差。

RDB和AOF的区别

特性RDBAOF
持久化方式快照形式,定期保存内存数据的快照日志形式,记录每个写操作命令
启动速度快,因为只需加载二进制快照文件慢,因为需要重放所有写操作命令
数据安全性较低,存在数据丢失风险较高,数据丢失风险极低
文件大小较小,因为是压缩的二进制文件较大,因为记录了每个写操作命令
适用场景适合冷备份和大规模数据恢复适合数据敏感场景和实时性要求高的应用
性能开销fork子进程时有较大性能开销,但通常较快如果每次写操作都同步,性能开销较大

如何使用Redis事务?

  • 可以使用 EXEC MULTI DISCARD WATCH等命令来实现
    • 开始事务(MULTI
    • 命令入队,FIFO
    • 执行事务(EXEC
  • DISCARD:表示清空事务队列中的保存的所有命令,并取消事务。
  • WATCH监控一个key的值,如果在事务执行时(EXEC),key的值被别的客户端/session所修改,整个事务都不会执行。 如果修改操作与WATCH在同一个session,同一个事务中,就会执行。

Redis事务支持原子性吗?

  • Redis的事务与我们正常理解的关系型数据库的事务不同
  • 事务的的四大特性:
    • 原子性:事务是最小单位,不可分割。事务保证要么全部完成,要么完全不起作用
    • 一致性:执行事务前后,数据保持一致,多个事务对同一个数据读取结果相同
    • 隔离性:并发访问时,一个用户的事务不被其他事务干扰,每个事务的数据库时独立的。
    • 持久性:一个事务提交之后,数据库的变化时持久的,即使数据库发生故障也不会对其有影响。
  • Redis的事务在运行有错误的情况下,除了执行过程中出现错误外,其他正常命令正常执行。另外Redis不支持回滚操作,因此Redis其实是没有原子性的。

Redis事务支持持久性吗?

  • Redis支持持久化,有三个持久化方式:

    • RDB快照
    • AOF只追加文件
    • 混合模式 RDB+AOF
  • 与RDB的持久化相比,AOF的持久化实时性更高,在Redis配置文件中存在三种不同的AOF持久化方式(fsync策略),分别是:

    • appendfsync always:每次数据又修改就会调用fsync函数,同步AOF文件,fsync线程完成后返回,严重降低Redis的速度
    • appendfsync everysec: 每秒调用fsync函数同步一次AOF文件
    • appendfsync no: 让操作系统决定何时进行同步,大约30秒一次。
  • AOF的fsync策略的everysec no ,都会存在丢失部分数据,always可以基本满足持久化要求,但是性能太差,实际开发中不会使用

  • 所有Redis事务持久没办法保证。

Redis事务还有什么缺陷?

  • Redis事务除了满足原子性外,事务的每条命令都会与Redis服务进行网络交互,这是比较浪费资源的行为,明明一次批量执行读条命令就好了。

如何解决Redis事务的缺陷?

  • Redis2.6之后支持了Lua脚本,可以使用Lua脚本批量执行命令,这样就只用和Redis进行一次网络交互,在Redis服务器中一次被执行,大大减少了网络开销。
  • 一段Lua脚本可以看作是一个命令执行,一段Lua脚本执行过程中不会又其他脚本和命令同时执行,保证了操作不会被影响。
  • 不过Lua脚本原型中途出错会直接结束,出错的命令不会被执行,但是出错前执行的命令也无法撤销。所以要保证lua脚本的正确性。 由于不能回滚,所以通过Lua脚本批量执行Redis命令也不算支持原子性。

什么是bigkey? 有什么危害?

  • 一个key对应的value值占用的内存比较大时,那么这个key就可以看作是bigKey了,大概的标准:

    • String类型的value超过1MB
    • 复合类型(list,set,zset,hash)的value元素超过5000个
  • 危害:bigkey会造成阻塞问题

    • 客户端阻塞:由于Redis命令是单线程处理,操作执行bigkey会比较耗时,那么就会阻塞Redis,从客户端看就是很久没有响应
    • 网络阻塞:每次获取bigkey的网络流量较大,如果一个bigkey的大小是1MB,每秒访问量为1000,那么每秒就会产生1000MB的流量,对普通千兆网卡的服务器是一种灾难
    • 工作线程阻塞:如果del大key时,会阻塞工作线程,无法处理后续命令。

如何发现bigkey?

  • 使用Redis自带的--bigkeys参数查找 redis-cli -p 6379 --bigkeys

    • 此命令会扫描所有的key,会对性能造成一些影响,并且这种方式只能找到每种数据结构的top 1 bigkey(String的value超过1MB, 复合类型的元素个数超过5000)
    • 执行此命令时,可以控制频率 redis-cli -p 6379 --bigkeys -i 3 ,每次休息3秒后再执行。
  • Redis自带的SCAN命令:SCAN会按照一定模式和数量返回匹配的key,获取key后,可以通过 STRLEN HLEN LLEN等命令获取value的长度和数量

    • 还可以使用MEMORY USAGE,这个命令会返回键值对的空间。
数据结构命令说明
StringSTRLEN字符串值的长度
ListLLEN列表的元素个数
HashHASH哈希的字段个数
SetSCARD无序集合的元素个数
ZsetZCARD有序集合的元素个数
  • 借助开源工具来分析bigkeys:

    • 领用RDB来分析找出bigkeys

      • redis-rdb-tools:Python 语言写的用来分析 Redis 的 RDB 快照文件用的工具。
      • rdb_bigkeys:Go 语言写的用来分析 Redis 的 RDB 快照文件用的工具,性能更好。
  • 借助公有云的Redis分析服务

如何处理bigkey:

  • 将Bigkey分割成多个小的key
  • 手动清理,使用UNLINK异步清理一个或多个指定key
  • 采用适合的数据结构,比如HyperLogLog 基数计数
  • 开启lazy-free(惰性删除): redis4.0之后,会fork子线程来执行删除动作,异步执行,避免阻塞主线程。

如何避免大量key集过期?

  • key设置随机过期时间
  • 设置开启惰性删除,lazy-free,4.0后引入,是采用异步线程延迟释放key的内存,将该操作交给子线程,避免阻塞主线程。

什么是Redis内存碎片? 为什么会有内存碎片?

  • 内存碎片:那些不可用的空闲内存,比如操作系统分配了一个32字节的内存,但是存储数据只占了24字节,还有8字节的数据是没办法分配给其他使用的。

  • 原因:

    • Redis存储数据时向操作系统请求的空间一般会大于实际存储的空间。
    • 频繁修改Redis数据时也会产生内存碎片: Redis的某个数据删除时,不会轻易释放内存给操作系统。

什么是缓存击穿? 什么是缓存穿透? 什么是缓存雪崩

  • 缓存击穿:高并发时,一个key突然过期,直接将大量访问打在数据库上进行查询
    • 解决方案:
      • 永不过期(不推荐):设置热点数据永不过期或过期时间长
      • 提前预热(推荐):针对热点数据提前预热,将其存入缓存并设置合理的过期时间,比如秒杀场景下,热点数据在秒杀结束前不过期
      • 加锁(看情况):缓存过期后,设置互斥锁保证只有一个请求到数据库并更新缓存
  • 缓存穿透:用户查询的数据,缓存中不存在,数据库也不存在
    • 解决方案:
      • 缓存无效key:如果缓存和数据库都查不到,就将这个无效key写入redis中,并设置过期时间。这样可以解决key变化不频繁的场景,如果变化频繁,会导致redis存储大量无效的key,如果非要使用此种方式,尽量设置过期时间短一点。
      • 布隆过滤器: 将所有可能请求的值都存在bloomfilter中,当用户请求是,先判断用户发来的请求值是否存在于布隆过滤器中,不存在的话,直接返回给客户端错误信息,存在才走下面的流程 image.png
      • 接口限流:根据用户和IP对接口进行限流,对于异常频繁访问行为,还可以设置黑名单,将异常访问的用户放入黑名单。
  • 缓存雪崩:Redis实例宕机或者大量key集体过期,大量请求打在数据库上,导致数据压力骤增甚至宕机。
    • 解决方案:
    • 针对Redis服务不可用的情况:
      • Redis集群:采用Redis集群,避免单实例出现问题导致缓存服务不可用。Redis Cluster和Redis Sentinel都是常用的Redis集群方案
      • 多级缓存:设置多级缓存,比如Redis缓存+本地缓存,即使Redis服务不可用,还可以从本地缓存得到部分数据
    • 针对大量缓存同时失效的情况:
      • 设置随机过期时间:比如在固定的过期时间加上一个随机值,避免大量缓存同时过期
      • 提前预热:针对热点数据放入缓存,并设置热点事件,比如秒杀事件的缓存在秒杀结束前不会失效。
      • 持久缓存策略: 看情况,对于一些关键并且变化不频繁的可以考虑这种策略。

布隆过滤器

  • 什么是布隆过滤器: 一种检索元素是否在给定的大集合中的数据结构,这种数据结构高效且性能好,但是缺点是具有一定的错误识别率和删除难度。 并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大

  • 原理:

    • 当元素往布隆过滤器中添加时:

        1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)
        1. 根据哈希值,在对位数组中把对应下表设置为1
    • 当需要判断一个元素是否存在于布隆过滤器中时:

        1. 对给定元素进行相同的哈希计算,得到哈希值
        1. 根据哈希值,判断数组中对应下表的每个元素值是否都为1, 如果都为1则说明值在布隆过滤器中,如果存在一个值不为1,说明该元素不在布隆过滤器中。
  • 总结:布隆过滤器如果说某个元素存在,有概率会误判;但是如果说某个元素不在,那一定不在。

如何保证缓存和数据库的一致性?

  • 方案一:双写策略

    • 同时更新数据库和Redis缓存,以确保数据的一致性
    • 事务:先将数据写入Redis缓存,再更新MySQL数据库的数据,这个过程增加事务,确保Redis和MySQL的更新动作要么全部成功,要么都失败。
    • 增加重试机制:实际操作可能会遇到网络故障,服务宕机等情况,导致Redis或Mysql更新失败,需要增加重试机制,当更新失败时,自动重试一定次数,直到成功为止。
    • 保证幂等性:多次执行得到相同结果,在更新Mysql时,增加乐观锁或唯一约束避免更新重复
    • 性能影响:双写策略会增加操作系统写操作负担,每次更新都需要同时更新Redis和MySQL。
  • 方案二:延迟双删

    • 先删除缓存的数据,再更新数据库的数据,之后再删除一次缓存的数据。
    • 流程:
      • 先删除Redis缓存的数据,保证后续读取操作不会命中旧数据。
      • 更新MySQL的数据,确保数据持久化,保证数据不丢失
      • 延时删除缓存:更新完数据库数据,等一段时间(几百毫秒),再删除Redis缓存数据。确保高并发场景下,其他线程会读取旧数据。
    • 实现细节:
      • 延迟一段时间再删除缓存,保证一致性
      • 增加重试机制,确保数据可以被更新
      • 幂等性,增加数据的唯一约束或乐观锁,确保不被重复更新
  • 并发场景还是都会出现数据不一致的场景,所以需要平衡性能和一致性,大多数以一致性为主。

如何保证Redis服务的高可用?

  • 使用Redis Sentinel集群

  • 如何检测节点是否下线?

    • 主观下线(Subjective DOWN, SDOWN):某个sentinel认为此节点下线;master节点在指定时间内未响应sentinel的PING命令,或返回无效回复,则认为此master节点主观下线。
    • 客观下线(Objective DOWN, ODOWN):大多数的sentinel都认为下线; 当一个Sentinel认为master主观下线后,向其他sentinel发送is-master-down-by-addr命令,询问其他sentinel是否master主观下线了,如果足够数量(配置)的sentinel的实例认为master主观下线了,那么此master就会被标记为客观下线。
    • 故障转移(fail-over): 只有master节点被标记为ODWON,sentinel才会故障转移,选取新的master节点。
    • Master恢复的处理:
      • SDWON后,ODWON之前恢复,sentinel取消SDOWN标记,不会触发故障转移
      • ODOWN后恢复,已故障转移,sentinel将此节点自动作为master的从节点。
  • 新的master的选举?

    • 优先级:根据配置(replica-priority)手动设置优先级,最大100,值越小,优先级越高。0表示不参与选举。如果没有优先级,根据复制进度选举。
    • 复制进度:sentinel期望选举出数据最完整的节点作为master,复制进度越快,得分越高
    • runid: 如果复制进度相同的多个节点,选择runid最小的。

Sentinel 哨兵有什么用?

  • 监控所有Redis(包括哨兵)的状态是否正常
  • 故障转移:如果master出现故障,哨兵会进行故障转移,会将一台slave升级为master,确保redis系统服务可用性
  • 通知:通知slave新的master连接信息,让他们执行replicaof成为master的slave.
  • 配置更新:客户端向sentinel请求master地址,如果发生故障转移,sentinel会将新的master地址通知客户端。

Redis缓存数量太大怎么办?

  • 使用Redis数据库切片集群:部署多台master节点,这些节点没有主从之说,同时对外提供读写服务。数据库的缓存相对均匀的分布在这些Redis节点上,客户端请求通过路由规则转发到相应的master节点上。
  • 同时为了保证redis的高可用,还有给每个master分配一个或多个slave

Redis虚拟槽分区有什么优点?

  • Redis采用哈希槽,每个键值对对应一个哈希槽,一般为16384个哈希槽。初始化Redis Cluter时,会将哈希槽平均分配到每个节点。
  • 客户端连接RedisCluster时的任意一个master节点,便可访问Cluster的数据。客户端发送请求命令时,先将key经过一些系列计算找到哈希槽,再根据哈希槽和节点的映射关系,找到目标节点。如果哈希槽确实是当前节点的,就直接返回结果。如果不由当前节点负责,返回-MOVED,告知客户端此哈希槽是由哪个节点负责,然后客户端向目标节点请求。
    • 找不到目标节点的原因:扩容或缩容会重新分配哈希槽,可能会导致哈希槽与节点的映射关系有误
  • 解耦了数据与节点之间的关系,增加了集群的横向扩展性和容错性

Redis Cluster节点是如何实现数据一致性的?或者说是怎么通信的?

  • 各个节点就是通过gossip协议通信
  • 有了Redis Cluster就不需要Sentinel了,有gossip协议,相当于内置sentinel,各个节点通过Gossip互探健康状态,出现故障,自动切换。