Redis

1991/6/26 面试基础

# 1.什么是 redis,它能做什么

redis 即 Remote Dictionary Server,用中文翻译过来可以理解为远程数据服务或远程字典服务。其是使用 C 语言的编写的key-value存储系统

应用场景:缓存,数据库,消息队列,分布式锁,点赞列表,排行榜等等

  • 数据缓存功能
  • 分布式锁的功能
  • 支持数据持久化
  • 支持事务
  • 支持消息队列

# 2.redis的优缺点

优点

  • 完全基于内存操作,性能极高,读写速度快,Redis 能够支持超过 100KB/s 的读写速率

  • 使用单线程模型来处理客户端的请求,避免了上下文的切换

  • IO 多路复用机制

  • 自身使用 C 语言编写,有很多优化机制,比如动态字符串 sds

  • 支持高并发,支持10万级别的并发读写

  • 支持主从模式,支持读写分离与分布式

  • 具有丰富的数据类型与丰富的特性(发布订阅模式)

  • 支持持久化操作,不会丢失数据

缺点

  • 数据库容量受到物理内存的限制,不能实现海量数据的高性能读写
  • 相比关系型数据库,不支持复杂逻辑查询,且存储结构相对简单
  • 虽然提供持久化能力,但实际更多是一个 disk-backed 功能,与传统意义上的持久化有所区别

# 3.Memcache与redis的区别

Memcache 也是一个开源、高性能、分布式内存对象缓存系统。所有数据均存储在内存中,在服务器重启之后就会消失,需要重新加载数据,采用 hash 表的方式将所有数据缓存在内存中,采用 LRU 算法来逐渐把过期的数据清除掉。

  1. 数据类型:Memcache 仅支持字符串类型,Redis 支持 5 种不同的数据类型
  2. 数据持久化:Memcache 不支持持久化,Redis 支持两种持久化策略,RDB 快照 和 AOF 日志
  3. 分布式:Memcache 不支持分布式,只能在客户端使用一致性哈希的方式来实现分布式存储,Redis3.0 之后可在服务端构建分布式存储,Redis集群没有中心节点,各个节点地位平等,具有线性可伸缩的功能。
  4. 内存管理机制:Memcache数据量不能超出系统内存,但可以调整内存大小,淘汰策略采用LRU算法。Redis增加了 VM 特性,实现了物理内存的限制,它们之间底层实现方式以及客户端之间通信的应用协议不一样。
  5. 数据大小限制:Memcache 单个 key-value 大小有限制,一个Value最大容量为 1MB,Redis 最大容量为512 MB

# 4.redis 有哪八种数据类型,有哪些应用场景

redis 总共有八种数据结构,五种基本数据类型和三种特殊数据类型

五种基本数据类型:

  • 1.string:字符串类型,常被用来存储计数器,粉丝数等,简单的分布式锁也会用到该类型
  • 2.hashmap:key - value 形式的,value 是一个map
  • 3.list:基本的数据类型,列表。在 Redis 中可以把 list 用作栈、队列、阻塞队列。
  • 4.set:集合,不能有重复元素,可以做点赞,收藏等
  • 5.zset:有序集合,不能有重复元素,有序集合中的每个元素都需要指定一个分数,根据分数对元素进行升序排序。可以做排行榜

  • 1.geospatial: Redis 在 3.2 推出 Geo 类型,该功能可以推算出地理位置信息,两地之间的距离
  • 2.hyperloglog:基数:数学上集合的元素个数,是不能重复的。这个数据结构常用于统计网站的 UV
  • 3.bitmap: bitmap 就是通过最小的单位 bit 来进行0或者1的设置,表示某个元素对应的值或者状态。一个 bit 的值,或者是0,或者是1;也就是说一个 bit 能存储的最多信息是2。bitmap 常用于统计用户信息比如活跃粉丝和不活跃粉丝、登录和未登录、是否打卡等

# 5.redis中String类型的实现原理

Redis 底层实现了简单动态字符串的类型(Simple Dynamic String,SDS)来表示 String 类型。没有直接使用C语言定义的字符串类型。

SDS 实现相对于C语言String方式的提升

  • 避免缓冲区移除。对字符修改时,可以根据 len 属性检查空间是否满足要求
  • 获取字符串长度的复杂度较低
  • 减少内存分配次数
  • 兼容C字符串函数,可以重用C语言库的一部分函数

# 6.Redis为何直接以内存存储

Redis 直接以内存的方式存储可以达到最快的读写速度,如果开启了持久化则通过异步的方式将数据写入磁盘,因此Redis 具有快速和数据持久化的特征。

在内存中操作本身就比从磁盘操作更快,且不受磁盘I/O速度的影响。如果不将数据放在内存中而是保存到磁盘,磁盘I/O速度会严重影响到Redis 的性能,而数据集大小如果达到了内存的最大限定值则不能继续插入新值。

如果打开了虚拟内存功能,当内存用尽时,Redis就会把那些不经常使用的数据存储到磁盘,如果Redis中的虚拟内存被禁了,它就会操作系统的虚拟内存(交换内存),但这时Redis的性能会急剧下降。如果配置了淘汰机制,会根据已配置的数据淘汰机制来淘汰旧数据。

# 7.Redis如何进行内存优化

1、尽可能使用哈希表(hash 数据结构):Redis 在储存小于100个字段的Hash结构上,其存储效率是非常高的。所以在不需要集合(set)操作或 list 的push/pop 操作的时候,尽可能使用 hash 结构。

2、根据业务场景,考虑使用 BitMap

3、充分利用共享对象池:Redis 启动时会自动创建【0-9999】的整数对象池,对于 0-9999的内部整数类型的元素,整数值对象都会直接引用整数对象池中的对象,因此尽量使用 0-9999 整数对象可节省内存。

4、合理使用内存回收策略:过期数据清除、expire 设置数据过期时间等

# 8.Redis如何实现分布式锁

Redis 能够用来实现分布式锁的命令有 INCR、SETNX、SET,并利用过期时间命令 expire 作为辅助

  • 方式1:利用 INCR

    如果 key 不存在,则初始化值为 0,然后再利用 INCR 进行加 1 操作。后续用户如果获取到的值大于等于 1,说明已经被其他线程加锁。当持有锁的用户在执行完任务后,利用 DECR 命令将 key 的值减 1,则表示释放锁。

  • 方式2:利用 SETNX

    先使用 setnx 来争抢锁,抢到之后利用 expire 设置一个过期时间防止未能释放锁。setnx 的意义是如果 key 不存在,则将key设置为 value,返回 1。如果已存在,则不做任何操作,返回 0。

  • 方式3:利用 SET

    set 指令有非常复杂的参数,相当于合成了 setnxexpire 两条命令的功能。其命令格式如:set($Key,$value, array('nx', 'ex'=>$ttl))

# 9.Redis高性能的原因

  1. 完全基于内存
  2. 数据结构简单,操作方便,并且不同数据结构能够应对于不同场景
  3. 采用单线程(网络请求模块使用单线程,其他模块仍用了多线程),避免了不必要的上下文切换和竞争条件,也不存在多进程或多线程切换导致CPU消耗,不需要考虑各种锁的问题。
  4. 使用多路I/O复用模型,为非阻塞I/O
  5. Redis 本身设定了 VM 机制,没有使用 OS 的Swap,可以实现冷热数据分离,避免因为内存不足而造成访问速度下降的问题

# 10.redis 的持久化机制有哪些,优缺点说说

redis 有两种持久化的方式,AOF**(Append Only File)** 和 RDB

# AOF(Append Only File)持久化

AOF 全称是 Append Only File(追加文件)。当 Redis 处理每一个写命令都会记录在 AOF 文件中,可以看做是命令日志文件。该方式需要设置 AOF 的同步选项,因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区中,同步选项有三种配置项选择:

  • always:同步刷盘,可靠性高,但性能影响较大
  • everysec:每秒刷盘,性能适中,最多丢失 1 秒的数据
  • no:操作系统控制,性能最好,可靠性最差

为了解决 AOF 文件体检不断增大的问题,用户可以向 Redis 发送 bgrewriteaof 命令,可以将 AOF 文件进行压缩,也可以选择自动触发,在配置文件中配置

auto-aof-rewrite-precentage 100
auto-aof-rewrite-min-zise 64mb
1
2
  • AOF 的优点:

    • 实现持久化,数据安全,AOF持久化可以配置 appendfsync 属性为 always,每进行一次命令操作就记录到AOF文件中一次,数据最多丢失一次
    • 通过 append 模式写文件,即使中途服务器宕机,可以通过 Redis-check-aof 工具解决数据一致性问题
    • AOF 机制的 rewrite 模式。AOF 文件的文件大小触碰到临界点时,rewrite 模式会被运行,重写内存中的所有数据,从而缩小文件体积
  • 示例说明

    • 1.AOF可以更好的保护数据不丢失,一般AOF会以每隔1秒,通过后台的一个线程去执行一次fsync操作,如果redis进程挂掉,最多丢失1秒的数据
    • 2.AOF是将命令直接追加在文件末尾的,写入性能非常高
    • 3.AOF日志文件的命令通过非常可读的方式进行记录,这个非常适合做灾难性的误删除紧急恢复,如果某人不小心用 flushall 命令清空了所有数据,只要这个时候还没有执行 rewrite,那么就可以将日志文件中的 flushall 删除,进行恢复
  • AOF 的缺点:

    • AOF 文件大,通常比 RDB 文件大很多
    • 比 RDB 持久化启动效率低,数据集大的时候较为明显
    • AOF 文件体积可能迅速变大,需要定期执行重写操作来降低文件体积
  • 示例说明:

    • 1.对于同一份数据源来说,一般情况下AOF 文件比 RDB 数据快照要大
    • 2.由于 .aof 的每次命令都会写入,那么相对于 RDB 来说需要消耗的性能也就更多,当然也会有 aof 重写将 aof 文件优化。
    • 3.数据恢复比较慢,不适合做冷备。

# RDB(Redis DataBase)持久化

RDB 是 Redis 中默认的持久化机制,按照一定的时间将内存中的数据以快照的方式保存到磁盘中,它会产生一个特殊类型的文件 .rdb 文件,同时可以通过配置文件中的 save 参数来定义快照的周期

在 RDB 中有两个核心概念 forkcow,在执行备份的流程如下:

在执行bgsave的时候,Redis 会 fork 主进程得到一个新的子进程,子进程是共享主进程内存数据的,会将数据写到磁盘上的一个临时的 .rdb 文件中,当子进程写完临时文件后,会将原来的 .rdb 文件替换掉,这个就是 fork 的概念。那 cow 全称是 copy-on-write ,当主进程执行读操作的时候是访问共享内存的,而主进程执行写操作的时候,则会拷贝一份数据,执行写操作。

  • RDB的优点:
    • 只有一个文件 dump.rdb ,方便持久化
    • 容错性好,一个文件可以保存到安全的磁盘
    • 实现了性能最大化,fork 单独子进程来完成持久化,让主进程继续处理命令,主进程不进行任何 I/O 操作,从而保证了Redis的高性能
    • RDB 是一个紧凑压缩的二进制文化,RDB重启时的加载效率比AOF持久化更高,在数据量大时更明显
  • 示例说明:
    • 1.它是将某一时间点redis内的所有数据保存下来,所以当我们做大型的数据恢复时,RDB的恢复速度会很快
    • 2.由于RDB的FROK子进程这种机制,队友给客户端提供读写服务的影响会非常小
  • RDB的缺点:
    • 可能出现数据丢失,在两次RDB持久化的时间间隔中,如果出现宕机,则会丢失这段时间中的数据
    • 由于RDB是通过fork子进程来协助完成数据持久化,如果当数据集较大时,可能会导致整个服务器间歇性暂停服务
  • 示例说明:
    • 举个例子假设我们定时5分钟备份一次,在10:00的时候 redis 备份了数据,但是如果在10:04的时候服务挂了,那么我们就会丢失在10:00到10:04的整个数据
    • 1:有可能会产生长时间的数据丢失
    • 2:可能会有长时间停顿:我们前面讲了,fork 子进程这个过程是和 redis 的数据量有很大关系的,如果数据量很大,那么很有可能会使redis暂停几秒

# 11. Redis的过期键的删除策略

  • 定时过期

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

    特点: 对内存友好,对 CPU 不友好。存在较多过期键时,利用定时器删除过期键会占用相当一部分CPU

  • 惰性过期

    只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。

    特点:对 CPU 友好,对内存不友好。不会花费额外的CPU资源来检测Key是否过期,但如果存在较多未使用且过期的Key时,所占用的内存就不会得到释放

  • 定期过期

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

    特点: 定期删除是对上面两种过期策略的折中,也就是对内存友好和CPU友好的折中方法。每隔一段时间执行一次删除过期键任务,并通过限制操作的时长和频率来减少对CPU时间的占用。

# 12.Redis的同步机制

Redis 主从同步分为增量同步和全量同步Redis 会先尝试进行增量同步,如果不成功则会进行全量同步。

增量同步:

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

全量同步:

Slave 初始化时它会发送一个 psync 命令到主服务器,如果是第一次同步,主服务器会做一次bgsave,并同时将后续的修改操作记录到内存 buffer 中,待 bgsave 完成后再将 RDB 文件全量同步到从服务器,从服务器接收完成后会将 RDB 快照加载到内存然后写入到本地磁盘,处理完成后,再通知主服务器将期间修改的操作记录同步到复制节点进行重放就完成了整个全量同步过程。

# 13.Redis的内存淘汰策略

在Redis中,最大使用内存大小由Redis.conf中的参数maxmemory决定,默认值为0,表示不限制,这时实际相当于当前系统的内存。但如果随着数据的增加,如果对内存中的数据没有管理机制,那么数据集大小达到或超过最大内存的大小时,则会造成Redis崩溃。因此需要内存数据淘汰机制。

设有过期时间

  1. volatile-lru:尝试回收最少使用的键
  2. volatile-random:回收随机的键
  3. volatile-ttl:优先回收存活时间较短的键

没有过期时间

  1. allkey-lru:尝试回收最少使用的键
  2. allkeys-random:回收随机的键
  3. noeviction:当内存达到限制并且客户端尝试执行新增,会返回错误

淘汰策略的规则

  • 如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用 allKeys-lru
  • 如果数据呈现平等分布,也就是所有的数据访问频率大体相同,则使用 allKeys-random
  • 关于 lru 策略,Redis中并不会准确的删除所有键中最近最少使用的键,而是随机抽取5个键(个数由参数maxmemory-samples决定,默认值是5),删除这5个键中最近最少使用的键。

# 14.缓存击穿、缓存穿透、缓存雪崩

# 缓存穿透:

  • 缓存穿透是指用户请求的数据在缓存中不存在并且在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍,然后返回空。

解决方案:

  • 使用布隆过滤器:将查询的参数都存储到一个 bitmap 中,在查询缓存前,如果 bitmap 存在则进行底层缓存的数据查询,如果不存在则进行拦截,不再进行缓存的数据查询
  • 缓存空对象:如果数据库查询的为空,则依然把这个数据缓存并设置过期时间,当多次访问的时候可以直接返回结果,避免造成多次访问数据库,但要保证当数据库有数据时及时更新缓存。

# 缓存击穿

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

解决方案:

  • 设置热点数据永不过期
  • 可以使用互斥锁更新,保证同一进程中针对同一个数据不会并发请求到 DB,减小DB的压力
  • 使用随机退避方式,失效时随机 sleep 一个很短的时间,再次查询,如果失败再执行更新

# 缓存雪崩

  • 缓存雪崩是指缓存中不同的数据大批量到过期时间,而查询数据量巨大,请求直接落到数据库上导致宕机。

解决方案:

  • 对于不同的key设置不同的过期时间,让缓存失效的时间点尽量均匀,比如我们可以在原有的失效时间基础上增加一个随机值,比如1~5分钟随机,这样每一个缓存的过期时间的重复率就会降低。
  • 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个Key只允许一个线程查询和写缓存,其他线程等待
  • 缓存永不过期
  • 设置二级缓存,或者双缓存策略。
  • 通过缓存 reload 机制,预先去更新缓存,在即将发生高并发访问前手动触发加载缓存

# 15.Redis如何进行缓存降级

缓存降级,其实都应该是指服务降级。在访问量剧增、服务响应出现问题(如响应延迟或不响应)或非核心服务影响到核心流程的性能的情况下,仍然需要保证核心服务可用,尽管可能一些非主要服务不可用,这时就可以采取服务降级策略。

服务降级的最终目的是保证核心服务可用,即使是有损的。服务降级应当事先确定好降级方案,确定哪些服务是可以降级的,哪些服务是不可降级的。根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心服务的正常运行。

降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也可以随机提供服务。根据服务范围:可以暂时禁用某些功能或禁用某些功能模块。总之服务降级需要根据不同的业务需求采用不同的降级策略。主要的目的就是服务虽然有损但是总比没有好。

# 16.Redis如何进行缓存更新

  • 数据实时同步失效或更新:这是一种增量主动型的方案,能保证数据强一致性,在数据库数据更新之后,主动请求缓存更新
  • 数据异步更新:这是一种增量被动型方案,数据一致性稍弱,数据更新会有延迟,更新数据库数据后,通过异步方式,用多线程方式或消息队列来实现更新
  • 定时任务更新:这是一种增/全量被动型方案,通过定时任务按一定频率调度更新,数据一致性最差

# 17.Redis 的热 key 问题怎么解决

热 key 就是说,在某一时刻,有非常多的请求访问某个 key,流量过大,导致该 redi 服务器宕机

解决方案:

  • 可以将结果缓存到本地内存中
  • 将热 key 分散到不同的服务器中
  • 设置永不过期

# 18. Redis的内存满了怎么办

实际上Redis定义了8种内存淘汰策略用来处理redis内存满的情况:

  • 1.noeviction:直接返回错误,不淘汰任何已经存在的redis键
  • 2.allkeys-lru:所有的键使用lru算法进行淘汰
  • 3.volatile-lru:有过期时间的使用lru算法进行淘汰
  • 4.allkeys-random:随机删除redis键
  • 5.volatile-random:随机删除有过期时间的redis键
  • 6.volatile-ttl:删除快过期的redis键
  • 7.volatile-lfu:根据lfu算法从有过期时间的键删除
  • 8.allkeys-lfu:根据lfu算法从所有键删除

# 19.Redis 有哪些部署方式

  • 单机模式:这也是最基本的部署方式,只需要一台机器,负责读写,一般只用于开发人员自己测试
  • 哨兵模式:哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。它具备自动故障转移、集群监控、消息通知等功能。
  • cluster集群模式:在redis3.0版本中支持了cluster集群部署的方式,这种集群部署的方式能自动将数据进行分片,每个master上放一部分数据,提供了内置的高可用服务,即使某个master挂了,服务还可以正常地提供。
  • 主从复制:在主从复制这种集群部署模式中,我们会将数据库分为两类,第一种称为主数据库(master),另一种称为从数据库(slave)。主数据库会负责我们整个系统中的读写操作,从数据库会负责我们整个数据库中的读操作。其中在职场开发中的真实情况是,我们会让主数据库只负责写操作,让从数据库只负责读操作,就是为了读写分离,减轻服务器的压力。

# 20.Redis的哨兵模式

Sentinel(哨兵)适用于监控 Redis 集群中 Master 和 Slave 状态的工具,是Redis的高可用性解决方案

主要作用

  1. 监控。哨兵会不断检查用户的Master和Slave是否运作正常
  2. 提醒。当被监控的某个Redis节点出现问题时,哨兵可以通过API向管理员或其他应用程序发送通知
  3. 自动故障迁移。当一个Master不能正常工作时,哨兵会开始一次自动故障迁移操作,它会将集群中一个Slave提升为新的Master,并让其他Slave改为与新的Master进行同步。当客户端试图连接失败的Master时,集群也会想客户端返回新的Master地址。当主从服务器切换后,新Master的Redis.conf,Slave的Redis.conf和Sentinel的Redis.conf三者配置文件都会发生相应的改变。

# 21.哨兵选举过程是怎么样的

  • 1.第一个发现该master挂了的哨兵,向每个哨兵发送命令,让对方选举自己成为领头哨兵
  • 2.其他哨兵如果没有选举过他人,就会将这一票投给第一个发现该master挂了的哨兵
  • 3.第一个发现该master挂了的哨兵如果发现由超过一半哨兵投给自己,并且其数量也超过了设定的quoram参数,那么该哨兵就成了领头哨兵
  • 4.如果多个哨兵同时参与这个选举,那么就会重复该过程,知道选出一个领头哨兵

选出领头哨兵后,就开始了故障修复,会从选出一个从数据库作为新的master

# 22.Redis的Pipeline管道

问题背景

Redis 是基于TCP协议的请求/响应服务器,每次通信都要经过TCP协议的三次握手,所以当需要执行的命令足够复杂时,会产生很大的网络延迟,并且网络的传输时间成本和服务器开销没有计入其中,总的延迟可能更大。

Pipeline解决

  • Pipeline 主要就是为了解决存在这种情况的场景,使用Pipeline模式,客户端可以一次性发送多个命令,无需等待服务端返回,这样可以将多次I/O往返的时间缩短为一次,大大减少了网络往返时间,提高了系统性能。
  • Pipeline 是基于队列实现,基于先进先出的原理,满足了数据顺序性。同时一次提交的命令很多的话,队列需要非常大量的内存来组织返回数据内容,如果大量使用Pipeline的话,应当合理分批次提交命令。
  • Pipeline的默认同步个数为53个,累加到 53 条数据时会把数据提交

注意: Redis 集群中使用不了 Pipeline,对可靠性要求很高,每次操作都需要立即获取本次操作结果的场景都不适合用 Pipeline

# 23.Redis如何进行性能优化

  1. Master 最好不要做 RDB 持久化,因为这时 save 命令调度 rdbSave 函数,会阻塞主线程的工作,当数据集比较大时可能造成主线程间断性暂停服务
  2. 如果数据比较重要,将某个 Slave 节点开启AOF数据备份,策略设置为每秒一次
  3. 为了主从复制速度和连接的稳定性,Master 和 Slave 最好在同一个局域网中
  4. 尽量避免在运行压力很大的主库上增加从库
  5. 主从复制不要用图状结构,用单向链表结构更为稳定,Mater->Slave1->Slave2->Slave3... 这样的结构方便解决单点故障问题,实现 Slave 对 Master 的替换,如果 Master 崩溃,可以立即启用 Slave1替换Mater,而其他依赖关系则保持不变。

# 24.Redis的数据一致性问题

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

    这种是常规的做法,但是如果更新缓存失败,将会导致缓存是旧数据,数据库是新数据

  • 先删除缓存,再写入数据库

    这种方式能够解决方式1的问题,但是仅限于低并发的场景,不然如果有新的请求在删完缓存之后,写数据库之前进来,那么缓存就会马上更新数据库更新之前数据,造成数据不一致的问题

  • 延时双删策略

    这种方式是先删除缓存,然后更新数据库,最后延迟个几百毫秒再删除缓存

  • 直接操作缓存,定期写入数据库

# 25.Redis如何实现事务

虽然Redis的Transactions 提供的并不是严格的 ACID的事务(如一串用EXEC提交执行的命令,如果在执行中服务器宕机,那么会有一部分命令执行一部分命令未执行),但这些Transactions还是提供了基本的命令打包执行的功能(在服务器不出问题的情况下,可以保证一连串的命令是顺序在一起执行的。

Redis 事务的本质就是四个原语:

  1. multi:用于开启一个事务,它总是返回 OK,当 multi 执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会被立即执行,而是放到一个队列中,当 exec 命令被调用的时候,所有队列d 命令才会执行
  2. exec:执行所有事务队列内的命令,返回事务内所有命令的返回值,当操作被打断的时候,返回空值 nil
  3. watch:是一个乐观锁。可以为 redis 事务提供 CAS 操作,可以监控一个或多个键。一旦其中有一个键被修改(删除),之后的事务就不会执行,监控一直持续到 exec 命令执行之后
  4. discard:调用 discard,客户端可以清空事务队列中的命令,并放弃执行事务

事务支持一次执行多个命令,一个事务中的所有命令都会被序列化。在事务执行的过程中,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令队列中。Redis 不支持回滚事务,在事务失败的时候不会回滚,而是继续执行余下的命令。

# 26.Redis如何实现集群

  • Cluster 3.0

    这是Redis 自带的集群功能,它采用的分布式算法是哈希槽,而不是一致性Hash。支持主从结构,可以扩展多个从服务器,当主节点挂了可以很快切换到一个从节点作主节点,然后其他从节点都会切换读取最新的主节点。

  • Twemproxy

    Twitter 开源的一个轻量级后端代理。可以管理 Redis 或 Memcache 集群。相对于 Redis 集群来说,易于管理。它的使用方法和Redis集群没有任何区别,只需要设置多个Redis实例后,在本需要连接 Redis 的地方改为连接 Twemproxy ,它就会以一个代理的身份接收请求并使用一致性Hash算法,将请求连接到具体的Redis节点上,将结果再返回Twemproxy。对于客户端来说,Twemproxy 相当于是缓存数据库的总入口,它不需要知道后端如何部署的。Twemproxy 会检测与每个节点的连接是否正常,如果存在异常节点就会将其剔除,等一段时间后,Twemproxy 还会再次尝试连接被剔除的节点。

  • Codis

    它是一个 Redis 分布式的解决方法,对于应用使用 Codis Proxy 的连接和使用Redis的服务没有明显区别,应用能够像使用单机 Redis 一样,让 Codis 底层处理请求转发,实现不停机实现数据迁移等工作。

# 27.cluster集群模式是怎么存放数据的

一个cluster集群中总共有16384个节点,集群会将这16384个节点平均分配给每个节点,当然,我这里的节点指的是每个主节点,就如同下图:

# 28.cluster的故障恢复是怎么做的

判断故障的逻辑其实与哨兵模式有点类似,在集群中,每个节点都会定期的向其他节点发送ping命令,通过有没有收到回复来判断其他节点是否已经下线。

如果长时间没有回复,那么发起ping命令的节点就会认为目标节点疑似下线,也可以和哨兵一样称作主观下线,当然也需要集群中一定数量的节点都认为该节点下线才可以,我们来说说具体过程:

  • 1.当A节点发现目标节点疑似下线,就会向集群中的其他节点散播消息,其他节点就会向目标节点发送命令,判断目标节点是否下线
  • 2.如果集群中半数以上的节点都认为目标节点下线,就会对目标节点标记为下线,从而告诉其他节点,让目标节点在整个集群中都下线

# 29.Redis的脑裂问题

脑裂问题通常是因为网络问题导致的。让 master、slave 和 sentinel 三类节点处于不同的网络分区。此时哨兵无法感知到 master 的存在,会将 slave 提升为 master 节点。此时就会存在两个 master,就像大脑分裂,那么原来的客户端往继续往旧的 master 写入数据,而新的master 就会丢失这些数据

如何解决

通过配置文件修改两个参数

min-slaves-to-write 3  # 表示连接到 master 最少 slave 的数量
min-slaves-max-lag 10  # 表示slave连接到master最大的延迟时间
--------------------新版本写法-----------------
min-replicas-to-write 3
min-replicas-max-lag  10
1
2
3
4
5

配置这两个参数之后,如果发生集群脑裂,原先的master节点接收到写入请求就会拒绝,就会减少数据同步之后的数据丢失

# 30.Redis如何实现异步队列

一般使用 List 结构作为队列。Rpush 生产消息,Lpop 消费消息。当 Lpop 没有消费的时候,需要适当 sleep 一会再重试。但是重复 sleep 会耗费性能,所以我们可以利用 list 的 blpop 指令,在还没有消息到来时,它会阻塞直到消息到来。

我们也可以使用 pub/sub 主题订阅者模式,实现 1:N 的消费队列,但是在消费者下线的时候,生产的消息会丢失

# 31.Redis如何实现延迟队列

可以使用 zset 结构,可以拿时间戳作为 score,消息的内容作为key,通过调用 zadd 来生产消息,消费者使用 zrangebyscore 指令轮询获取 N 秒之前的数据进行处理

# 32.Redis中哈希槽的概念

Redis Cluster提供了自动将数据分散到各个不同节点的能力,但采用的策略并不是一致性Hash,而是哈希槽。Redis 集群将整个Key的数值域分成16384个哈希槽,每个Key通过 CRC16检验后对16384驱魔来决定放置到那个槽中,集群的每个节点都负责其中一部分的哈希槽。

# 33.Redis的应用场景

1、数据缓存

经典的场景,现在几乎是所有中大型网站都在用的提升手段,合理地利用缓存能够提升网站访问速度

2、排行榜

可以借助Redis提供的有序集合(sorted set)能力实现排行榜的功能

3、计数器

可以借助Redis提供的 incr 命令来实现计数器功能,因为是单线程的原子操作,保证了统计不会出错,而且速度快

4、分布式session共享

集群模式下,可以基于 Redis 实现 session 共享

5、分布式锁

在分布式架构中,为了保证并发访问时操作的原子性,可以利用Redis来实现分布式锁的功能

6、最新列表

可以借助Redis列表结构,LPUSHLPOPLTRIM等命令来完成内容的查询

7、位操作

可以借助Redis中 setbitgetbitbitcount 等命令来完成数量上千万甚至上亿的场景下,实现用户活跃度统计

8、消息队列

Redis 提供了发布(Publish)与订阅(Subscribe)以及阻塞队列能力,能够实现一个简单的消息队列系统

# 34.Redis实现签到表功能

# Set 结构

以日期为 key,以用户 ID(对应数据库的 Primary Id)组成的集合为 value

  • 查询某个用户的签到状态 sismember key member
  • 插入签到状态 sadd key member
  • 统计某天用户的签到人数 scard key

# bitMap 结构

Key的格式为u:sign:uid:yyyyMM,Value则采用长度为4个字节(32位)的位图(最大月份只有31天)。位图的每一位代表一天的签到,1表示已签,0表示未签。

# 用户2月17号签到
SETBIT u:sign:1000:201902 16 1 # 偏移量是从0开始,所以要把17减1

# 检查2月17号是否签到
GETBIT u:sign:1000:201902 16 # 偏移量是从0开始,所以要把17减1

# 统计2月份的签到次数
BITCOUNT u:sign:1000:201902

# 获取2月份前28天的签到数据
BITFIELD u:sign:1000:201902 get u28 0

# 获取2月份首次签到的日期
BITPOS u:sign:1000:201902 1 # 返回的首次签到的偏移量,加上1即为当月的某一天
1
2
3
4
5
6
7
8
9
10
11
12
13
14

两者对比

  • 使用 set 的方式所占用的内存只与数量相关,和存储哪些 ID 无关
  • 使用 bitmap 的方式所占用的内存与数量没有绝对的关系,而是与最高位有关,比如假设 ID 为 500 W的用户签到了,那么从 1~4999999 用户不管是否签到,所占用的内存都是 500 w个bit,这边是最坏的情况
  • 使用 bitmap 最大可以存储 2^32-1也就是 512M 数据
  • 使用 bitmap 只适用存储只有两个状态的数据,比如用户签到,资源(视频、文章、商品)的已读或未读状态

# 35.Redis中zset的底层实现

Redis中 ZSet 是选择使用 跳表 而不是红黑树

# 什么是跳表

  • 跳表是一个随机化的数据结构,实质上就是一种可以进行二分查找的有序链表。
  • 跳表在原有的有序链表上增加了多级索引,通过索引来实现快速查找
  • 跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能

总结:

  1. 跳表是可以实现二分查找的有序链表
  2. 每个元素插入时随机生成它的 level
  3. 最底层包含所有的元素
  4. 如果一个元素出现在 level(x),那么它肯定出现在 x 以下的 level 中
  5. 每个索引节点包含两个指针,一个向下,一个向右
  6. 跳表查询、插入、删除的时间复杂度为 O(log n),与平衡二叉树接近

# 为什么不选择红黑树来实现

首先来分析下 Redis 的有序集合支持的操作:

  1. 插入元素
  2. 删除元素
  3. 查找元素
  4. 有序输出所有元素
  5. 查找区间内的所有元素

其中前 4 项红黑树都可以完成,且时间复杂度与跳表一致,但是最后一个红黑树的效率就没有跳表高了。在跳表中,要查找区间的元素,只要定位到两个区间端点在最低层级的位置,然后按顺序遍历元素就可以了,非常高效。

# 36.主从同步原理是怎样的

  • 1.当一个从数据库启动时,它会向主数据库发送一个SYNC命令,master收到后,在后台保存快照,也就是我们说的RDB持久化,当然保存快照是需要消耗时间的,并且redis是单线程的,在保存快照期间redis受到的命令会缓存起来
  • 2.快照完成后会将缓存的命令以及快照一起打包发给slave节点,从而保证主从数据库的一致性。
  • 3.从数据库接受到快照以及缓存的命令后会将这部分数据写入到硬盘上的临时文件当中,写入完成后会用这份文件去替换掉RDB快照文件,当然,这个操作是不会阻塞的,可以继续接收命令执行,具体原因其实就是fork了一个子进程,用子进程去完成了这些功能。

因为不会阻塞,所以,这部分初始化完成后,当主数据库执行了改变数据的命令后,会异步的给slave,这也就是我们说的复制同步阶段,这个阶段会贯穿在整个中从同步的过程中,直到主从同步结束后,复制同步才会终止。

# 37.无硬盘复制是什么

我们刚刚说了主从之间是通过RDB快照来交互的,虽然看来逻辑很简单,但是还是会存在一些问题,但是会存在着一些问题。

  • 1.master禁用了RDB快照时,发生了主从同步(复制初始化)操作,也会生成RDB快照,但是之后如果master发成了重启,就会用RDB快照去恢复数据,这份数据可能已经很久了,中间就会丢失数据
  • 2.在这种一主多从的结构中,master每次和slave同步数据都要进行一次快照,从而在硬盘中生成RDB文件,会影响性能

为了解决这种问题,redis在后续的更新中也加入了无硬盘复制功能,也就是说直接通过网络发送给slave,避免了和硬盘交互,但是也是有io消耗

# 38.听说 redis 6.0之后又使用了多线程,不会有线程安全的问题吗

不会

其实 redis 还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程,所以是不会有线程安全的问题。

之所以加入了多线程因为 redis 的性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。