不断的学习,我们才能不断的前进
一个好的程序员是那种过单行线马路都要往两边看的人

Redis 面试题

redissi-wei-dao-tu
Redis采用的是基于内存的,采用的是单进程单线程模型的 KV 数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。Redis的特点有:

  • 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。它的,数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
  • 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
  • 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  • 使用I/O多路复用模型,非阻塞IO;
  • 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求

1. Redis的持久化?

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

  • RDB是将某个时间点上的数据库状态保存到一个RDB文件中,
  • AOF是通过保存Redis服务器所执行的写命令来记录数据库状态的,每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,因为这个模式是只追加的方式。

因为RDB是保存整个数据库的状态,所以会耗费较长时间,在停机时会出现大量的数据丢失,所以需要AOF来配合使用。

在AOF持久化开启后优先加载AOF文件

RDB

RDB通过BGSAVE命令会派生出一个子进程,然后由子进程创建RDB文件,服务器进程继续处理命令请求。RDB可以通过save选项设置多个保存条件,只要满足一个条件就进行RDB持久化。
优点

  • 会生成多个数据文件,每个数据文件分别都代表了某一时刻Redis里面的数据
  • RDB对Redis的性能影响非常小,是因为在同步数据的时候他只是fork了一个子进程去做持久化的,而且他在数据恢复的时候速度比AOF来的快。

缺点
RDB都是快照文件,都是默认五分钟甚至更久的时间才会生成一次,这意味着你这次同步到下次同步这中间五分钟的数据都很可能全部丢失掉。AOF则最多丢一秒的数据

AOF

AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的,命令首先会写入缓冲区里面。分为三个步骤:命令追加(append)、文件写入和文件同步(sync):

  1. 命令追加:当AOF持久化功能开启后,服务器在执行一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
  2. AOF文件的写入与同步:通过配置appendfsync的值来决定写入和同步的操作:
    • always:将 aof_buf 缓冲区中的所有内容写入并同步到 AOF 文件。
    • everysec:将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 如果上次同步 AOF 文件的时间距离现在超过一秒钟, 那么再次对 AOF 文件进行同步。
    • no:将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 但并不对 AOF 文件进行同步

PS:文件的写入和同步
为了提高文件的写入效率,在操作系统汇总,当用户调用write函数将一些数据写入到文件的时候,操作系统通常会将写入数据暂时保存在一个内存缓冲区里面,等到缓冲区的空间被填满、或者超过指定的时限之后,才会真正的将缓冲区的数据写入到磁盘里面。

AOF重写
随着服务器运行时间变长,AOF文件中的内容会越来越多,文件体积会越来越大,如果不处理的话,对严重影响redis的性能,而且AOF文件体积越大,AOF文件来进行数据还原的时间就越多。
AOF重写通过创建一个新的AOF文件来代替现有的AOF文件,新旧两个文件保存的数据库状态相同,但是新文件里面不会包含任何浪费空间的冗余命令。其原理就是开辟一个子进程对内存进行遍历转换成一系列Redis的操作指令,序列化到一个新的AOF日志文件中。序列化完毕后再将操作期间发生的增量AOF 日志追加到这个新的AOF日志文件中,追加完毕后就立即替代旧的AOF日志文件了,瘦身工作就完成了。

使用BGREWRITEAOF命令读取当前的服务器状态来完成AOF重写。

单独用RDB会丢失很多数据,单独用AOF,你数据恢复没RDB来的快。
通常是两者一起使用,因为RDB恢复数据比较快,然后AOF做数据补全

2. 为什么RDB 要 fork 子进程而不是线程

因为redis是单线程的,直接使用主线程的话,redis的其他请求就会被阻塞。

3. redis基本数据类型

redis的基本数据结构

4. zset的底层数据结构,跳表何时增加高度 ?

有序集合的数据结构

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

缓存是高并发场景下提高热点数据访问性能的一个有效手段,在开发项目时会经常使用到。缓存的类型分为:本地缓存、分布式缓存和多级缓存。
本地缓存
本地缓存就是在进程的内存中进行缓存,比如我们的 JVM 堆中,可以用 LRUMap 来实现,也可以使用 Ehcache 这样的工具来实现。本地缓存是内存访问,没有远程交互开销,性能最好,但是受限于单机容量,一般缓存较小且无法扩展。
分布式缓存
分布式缓存可以很好得解决这个问题。
分布式缓存一般都具有良好的水平扩展能力,对较大数据量的场景也能应付自如。缺点就是需要进行远程请求,性能不如本地缓存。
多级缓存
为了平衡这种情况,实际业务中一般采用多级缓存,本地缓存只保存访问频率最高的部分热点数据,其他的热点数据放在分布式缓存中。

淘汰策略
不管是本地缓存还是分布式缓存,为了保证较高性能,都是使用内存来保存数据,由于成本和内存限制,当存储的数据超过缓存容量时,需要对缓存的数据进行剔除,内存淘汰策略有:FIFO、LRU、LFU(最近使用频率最低):

  • noeviction:返回错误,当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
  • allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
  • volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
  • allkeys-random: 回收随机的键使得新添加的数据有空间存放。
  • volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键
  • volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
    如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了

缓存一致性解决方案

保持缓存一致性的策略有:

  • 先删除缓存,再更新数据库
  • 先更新数据库,再删除缓存
  • 先更新数据库,再更新缓存:有问题,缓存结果的计算可能花费的代价很高,如果有大量的写请求,每次都要更新缓存,则代价会很高。
  • 先更新缓存,再更新数据库。:不考虑,

读写请求串行化解决一致性问题
串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低

先删除缓存再更新数据库
先删除缓存,数据库还没有更新成功,此时如果读取缓存,缓存不存在,去数据库中读取到的是旧值,缓存不一致发生。解决方案就是延迟双删:

  1. 线程1删除缓存,然后去更新数据库
  2. 线程2来读缓存,发现缓存已经被删除,所以直接从数据库中读取,这时候由于线程1还没有更新完成,所以读到的是旧值,然后把旧值写入缓存
  3. 线程1,根据估算的时间,sleep,由于sleep的时间大于线程2读数据+写缓存的时间,所以缓存被再次删除
  4. 如果还有其他线程来读取缓存的话,就会再次从数据库中读取到最新值

主要思路就是:为了避免更新数据库的时候,其他线程从缓存中读取不到数据,就在更新完数据库之后,再sleep一段时间,然后再次删除缓存。(sleep时间大于线程读写缓存时间即可)

先更新数据库,再删除缓存
更新数据库成功,如果删除缓存失败或者还没有来得及删除,那么,其他线程从缓存中读取到的就是旧值,还是会发生不一致。
解决方案有:

  • 消息队列:先更新数据库,然后删除缓存,如果删除失败的话,往消息队列发送删除缓存的消息,然后使用消费者监听这个消息队列,消费者监听到到消息后再尝试删除缓存。
  • binlog日志:更新数据的时候,Mysql会记录binlog日志,所以可以订阅 Mysql 数据库的 binlog 日志对缓存进行删除操作,如果删除失败的话就存入MQ队列里面,然后再监听这个队列,再进行删除缓存。
  • 设置缓存过期时间:对一致性要求不高的场景,主要思路是,只修改数据库,不管缓存,等缓存失效后从数据库里面读取数据。

缓存的策略里面为什么是删除缓存而不是更新缓存
最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern:

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
  • 更新的时候,先更新数据库,然后再删除缓存。

这里删除缓存而不是更新缓存是因为,缓存可能关联了多个字段,而不是简单的取出值。另外如果频繁进行更新数据库的话,缓存更新代价是很高。所以采用懒加载的方式。

缓存不一致的解决方案

  • 对删除缓存进行重试,数据的一致性要求越高,越是重试得快。
  • 定期全量更新,简单地说,就是我定期把缓存全部清掉,然后再全量加载。
  • 给所有的缓存一个失效期。

6. 缓存穿透,击穿,雪崩以及处理方法?

缓存穿透
缓存穿透是指用户查询的数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库。
解决方案:

  • 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

缓存击穿
缓存击穿是指一个Key非常热点,并发访问非常高,当这个key过期时导致所有请求直接打到db上。跟缓存雪崩不同的是:缓存雪崩是大面积的失效。
解决方案:

  • 设置热点数据永远不过期。
  • 或者加上互斥锁就能搞定了:比如请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写入缓存,再返回给用户,这样后面的请求就可以从缓存中拿到数据了。

缓存雪崩
缓存雪崩:当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了,会有大量的请求进来直接打到DB上,而对数据库CPU和内存造成巨大压力,这样可能导致整个系统的崩溃。
解决方案:

  • 加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写。
  • 将缓存失效时间分散开,在原有缓存时间上面增加一个随机值。
  • 限流,如果redis宕机,可以进行限流。

缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

缓存更新
缓存的数据在数据源发生变更时需要对缓存进行更新,数据源可能是 DB,也可能是远程服务。更新的方式可以是主动更新。数据源是 DB 时,可以在更新完 DB 后就直接更新缓存。

redis 的String 是怎么实现的? 为什么不直接用c的?

String 类型是 Redis 中最常使用的类型,内部的实现是通过 SDS(Simple Dynamic String )来存储的。SDS 类似于 Java 中的 ArrayList,可以通过预分配冗余空间的方式来减少内存的频繁分配。

SDS简单动态字符串

跟C的字符串相比较的优点有:

  • 获取字符串的长度时间复杂度为O(1)
  • API是安全的,不会造成缓冲区溢出
  • 减少字符串修改时带来的内存重分配次数
  • 二进制安全的,c是以“\0”结束,SDS存储的字符串的长度
  • 解决C字符串不能存储文本数据

redis 是单线程的吗? 为什么这么快?

redis的速度非常的快,单机的redis就可以支撑每秒10几万的并发,相对于mysql来说,性能是mysql的几十倍。速度快的原因主要有几点:

  • 完全基于内存操作
  • C语言实现,优化过的数据结构,基于几种基础的数据结构,redis做了大量的优化,性能极高
  • 使用单线程,无上下文的切换成本
  • 基于非阻塞的IO多路复用机制

为什么早期选择单线程
因为 Redis 是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是 机器内存的大小 或者 网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。使用单线程带来的好处就是:

  1. 用单线程模型能带来更好的 可维护性,方便开发和调试;
  2. 使用单线程模型也能 并发 的处理客户端的请求;(I/O 多路复用机制)
  3. Redis 服务中运行的绝大多数操作的 性能瓶颈都不是 CPU
    redis为什么是单线程的

redis6.0为什么又改用成多线程呢?
redis使用多线程并非是完全摒弃单线程,redis还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程。

这样做的目的是因为redis的性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。

Redis为什么使用跳跃表而不是红黑树?

因为 zset 要支持随机的插入和删除,所以它 不宜使用数组来实现,关于排序问题,我们也很容易就想到 红黑树/ 平衡树 这样的树形结构,为什么 Redis 不使用这样一些结构呢?

  • 性能考虑: 在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部 (下面详细说);
  • 实现考虑: 在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观;
    基于以上的一些考虑,Redis 基于 William Pugh 的论文做出一些改进后采用了 跳跃表 这样的结构。
    跳跃表插入节点时,都需要调用一个随机算法给它分配一个合理的层数。

Redis分布式锁?

SET EX NX 实现分布式锁

Redis分布式锁主要通过SET EX NX来争抢锁并设置过期时间。

SET key value EX seconds NX

将值 value 关联到 key ,并将 key 的生存时间设为 seconds (以秒为单位)。SETEX是原子操,关联值和设置生存时间两个动作会在同一时间内完成。NX表示如何锁已经存在则返回nil。
存在问题

  • 持有锁的线程,异常推出,导致锁得不到释放,解决方案:set ex nx 设置锁过期时间。
  • 在业务还没执行完毕,锁就过期了,然后当自己删除锁的时候,删除的是别的线程的锁;解决方案:给value设置为UUID,唯一标识当前线程的锁:
  • 存在问题:如果对比当前线程的UUID 跟 锁的value的时候锁过期了怎么办? 所以要设置对比值 和 删除锁操作为原子性操作,使用LUA操作。
  • redis分布式锁的核心是:加锁保证原子性(SET EX NX),解锁保证原子性(LUA)

RedissonCLient(推荐使用)

redission分布式锁使用的是lock = RedissonClient.get() 获取到锁;
lock.lock 可以进行加锁,:

  • 如果设置过期时间的话,到时间,会自动解锁,如果业务逻辑还没处理完就会出异常;
  • 如果不设置过期时间的话,默认是30s,如果过期的话会自动续期,使用的是redis定时器,每隔三分之一的过期时间就会执行定时器,重新设置过期时间。

redission 可以实现公平锁、读写锁、信号量、栏栅锁CountDownLatch、互斥锁、共享锁

    /**
	 * 配置文件里面整合redission,实现分布式锁
	 * @return
	 */
	@Bean
	public RedissonClient getRedisson(){
		Config config = new Config();
		//单机模式  依次设置redis地址和密码
		config.useSingleServer()
				.setAddress("redis://"+redisHost+":"+redisPort) // "redis://IP地址:6379"
				.setDatabase(database)
				.setPassword(password); // "password"
		return Redisson.create(config);
	}
    /**
    * 正常业务逻辑实现分布式加锁代码
    **/
    public void getBlogDetailByRedission(Long id) {
		// 获取一把锁,只要锁的名字一样就是同一把锁
		RLock lock = redissonClient.getLock("detail_blog_" + id);
		// 加锁 会
		lock.lock(1, TimeUnit.SECONDS); // 设定了时间会自动解锁,解锁时间要大于业务的处理时间
		try {

		}finally {
			lock.unlock();
		}
	}

RedLock

Redlock 是实现 Redis分布式锁的一种方式:

  • 安全特性:互斥访问,即永远只有一个 client 能拿到锁
  • 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
  • 容错性:只要大部分 Redis 节点存活就可以正常提供服务

Redis的高可用性和可扩展性?

通过redis主从复制和哨兵来实现高可用性通过集群来实现可扩展性

Reds主从复制

主从模式是最简单的实现高可用的方案,核心就是主从同步。主从同步的原理如下:

  1. slave发送sync命令到master
  2. master收到sync之后,执行bgsave,生成RDB全量文件
  3. master把slave的写命令记录到缓存
  4. bgsave执行完毕之后,发送RDB文件到slave,slave执行
  5. master发送缓存中的写命令到slave,slave执行

复制:

  • 初次复制:从服务器以前没有复制过任何主服务器,或者从服务器当前要复制的主服务器和上一次复制的主服务器不同
  • 断线后重复制:处于命令传播阶段的主从服务器因为网络原因中断了复制,但从服务器通过自动重连上了主服务器,并继续复制主服务器。

sync的缺点:旧版的复制功能在连接上后会向主服务器重新发送SYNC命令,主服务器会重新生成RDB文件,然后发送给从服务器,这一步非常耗费资源,其实只需要复制断线后主服务器执行的命令即可。

使用PSYNC来代替SYNC效率低下的问题,PSYNC命令具有完整重同步和部分重同步两种模式:

  • 完整重同步跟SYNC一样。
  • 部分重同步用于处理断线后重复制的情况,在从服务器断线重连到主服务器后,如果条件允许,主服务将断线期间执行的写命令发送给从服务器,从服务器只需要执行这些写命令就可以更新到主服务器当前的状态。

部分重同步由三个部分组成:主从服务器的复制偏移量、主服务器的复制积压缓冲区、服务器的运行ID
复制偏移量
主从服务器都会维护一个复制偏移量,主服务每次向从服务器传播数据时就将自己的复制偏移量加上N,从服务每次接受到主服务的数据时,也将自己的复制偏移量加上N。
通过对比主从服务器的复制偏移量就可以知道主从服务器是否处于一致状态。

复制积压缓冲区
主服务器会维护一个固定长度的FIFO队列。复制积压缓冲区:
主服务器进行命令传播时,不仅会将写命令发送给所有从服务器,还会将写命令复制到积压缓冲区里面。所以积压缓冲区保存着一部分最近传播的写命令,也会记录相应的复制偏移量。

当从服务器重连上主服务器后,会通过PSYNC命令将自己的复制偏移量发送给主服务器:

  • 如果偏移量之后的数据处于复制积压缓冲区里面,主服务器执行部分重同步操作
  • 如果不存在则执行完整重同步操作

服务器运行ID
每个Redis服务器都有自己的运行ID,初次复制时,主服务器会把自己的ID发给从服务器,从服务器就会保存这个ID,当断线重连后,从服务器就会发送这个ID给主服务器,如果从服务器保存的运行ID和当前运行的主服务器的ID相同的话,说明断线之前复制的就是当前连接的主服务器;否则就说明不是同一个服务器,则执行完整重复制。

基于主从方案的缺点还是很明显的,假设master宕机,那么就不能写入数据,那么slave也就失去了作用,整个架构就不可用了。而哨兵(sentinel)负责监管redis结点,哨兵的主要功能有:

  • 集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
  • 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
  • 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
  • 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
    哨兵可以同时监视多个主从服务器,并且在被监视的master下线时,自动将某个slave提升为master,然后由新的master继续接收命令。

Redis可扩展性

redis集群是redis提供的分布式数据存储方案,集群通过数据分片sharding来进行数据的共享,同时提供复制和故障转移的功能。redis集群

故障转移
如果节点A向节点B发送ping消息,节点B没有在规定的时间内响应pong,那么节点A会标记节点B为pfail疑似下线状态,同时把B的状态通过消息的形式发送给其他节点,如果超过半数以上的节点都标记B为pfail状态,B就会被标记为fail下线状态,此时将会发生故障转移,优先从复制数据较多的从节点选择一个成为主节点,并且接管下线节点的slot,整个过程和哨兵非常类似,都是基于Raft协议做选举。

Redis的主从同步?

主从同步也就是读写分离,master结点负责写操作,slave结点负责读操作,master每次写操作后就把数据同步给别的slave机器。这样可以分发掉大量的请求。
Redis的复制功能

Redis的内存淘汰机制?LRU算法?

Redis的过期策略,是有定期删除+惰性删除两种。过期删除策略
定时删除
定时删除策略通过使用定时器,在创建键的过期时间的同时,创建一个定时器。定时删除策略可以保证过期键尽可能快地被删除,并释放过期键占用的内存。对CPU不够友好,redis没有使用 这种方式。
定期删除
定期删除指的是redis每隔一段时间对数据库做一次检查,删除里面的过期key。由于不可能对所有key去做轮询来删除,所以redis会每次随机取一些key去做检查和删除。

惰性删除
惰性删除指的是当我们查询key的时候才对key进行检测,如果已经达到过期时间,则删除。显然,他有一个缺点就是如果这些过期的key没有被访问,那么他就一直无法被删除,而且一直占用内存。

定期+惰性都没有删除过期的key怎么办
假设redis每次定期随机查询key的时候没有删掉,这些key也没有做查询的话,就会导致这些key一直保存在redis里面无法被删除,这时候就会走到redis的内存淘汰机制:

  1. volatile-lru:从已设置过期时间的key中,移出最近最少使用的key进行淘汰
  2. volatile-ttl:从已设置过期时间的key中,移出将要过期的key
  3. volatile-random:从已设置过期时间的key中随机选择key淘汰
  4. allkeys-lru:从key中选择最近最少使用的进行淘汰
  5. allkeys-random:从key中随机选择key进行淘汰
  6. noeviction:当内存达到阈值的时候,新写入操作报错

RDB对过期键的处理
在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中
在启动Redis服务器时,如果服务器只开启了RDB持久化,那么服务器将会载入RDB文件:

  • 如果服务器以主服务器模式运行,在载入RDB文件时,程序会对文件中保存的键进行检查,未过期的键会被载入到数据库中,过期键会被忽略
  • 如果服务器以从服务器模式运行,在载入RDB文件时,文件中保存的所有键,不论是否过期,都会被载入到数据库中

AOF对过期键的处理
如果数据库中的某个键已经过期,并且服务器开启了AOF持久化功能,当过期键被惰性删除或者定期删除后,程序会向AOF文件追加一条DEL命令,显式记录该键已被删除。
在执行AOF文件重写时,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中

复制功能对过期键的处理
在主从复制模式下,从服务器的过期键删除动作由主服务器控制

  • 主服务器在删除一个过期键后,会显式地向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键
  • 从服务器在执行客户端发送的读命令时,即使发现该键已过期也不会删除该键,照常返回该键的值。
  • 从服务器只有接收到主服务器发送的DEL命令后,才会删除过期键。

如何访问redis中大量的key?

keys算法是遍历算法,复杂度是O(n),也就是数据越多,时间复杂度越高。数据量达到几百万,keys这个指令就会导致 Redis 服务卡顿,因为 Redis 是单线程程序,顺序执行所有指令,其它指令必须等到当前的 keys 指令执行完了才可以继续。
解决方案 ,使用scan命令:

  • 复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程
  • 提供 count 参数,不是结果数量,是redis单次遍历字典槽位数量(约等于)
  • 同 keys 一样,它也提供模式匹配功能;
  • 服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;
  • 返回的结果可能会有重复,需要客户端去重复,这点非常重要;
  • 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零
# MATCH <返回和给定模式相匹配的元素>
# count 每次迭代所返回的元素数量
SCAN cursor [MATCH pattern] [COUNT count]

SCAN命令是增量的循环,每次调用只会返回一小部分的元素。所以不会让redis假死,SCAN命令返回的是一个游标,从0开始遍历,到0结束遍历。

redis 一致性Hash算法?

redis本身没有使用一致性hash算法,而是使用了hash槽slot的概念。
Redis Cluster 采用虚拟哈希槽slot分区,所有的键根据哈希函数映射到 0 ~ 16383 整数槽内,每个key通过CRC16校验后对16384取模来决定放置哪个槽(Slot),每一个节点负责维护一部分槽以及槽所映射的键值数据。

使用哈希槽的好处就在于可以方便的添加或移除节点:

  • 当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;
  • 当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了

节点自身维护槽的映射关系
16384÷8÷1024=2kb, 而且redis集群主节点数量基本不可能超过1000个。

redis-slot

在使用Hash算法的时候,通过hashcode跟集群中结点的数量做取模操作,找到key存储的位置,但是如果集群中结点的数量改变后,就会导致取模的结果错误
一致性Hash算法正是为了解决这种问题,不在是跟集群中结点的数量做取模运算,而是跟2^32-1做取模操作,并且整个集群中的结点按顺时针方向组织
如果某个结点下线后,不影响其他的结点,把下线的结点重新定位到顺时针方向的下一个结点上面
如果增加某一个结点,也是类似。
所以受影响的数据仅仅是此服务器到其环空间中前一台服务器(逆时针方向)。

一致性Hash存在的问题
数据倾斜的问题,如果服务节点太少时,容易因为节点分部不均匀而造成数据倾斜。解决方案是:引入虚拟结点,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器IP或主机名的后面增加编号来实现。

redis哪里会阻塞?

AOF 持久化的阻塞
aop持久化是以追加的方式写入的,先执行写操作,再追加日志,写入的线程是主线程,所以会导致阻塞,可能会阻塞下一个写操作。也就是在写日志并同步到文件的时候。

Fork 操作的阻塞

bgsave、bgrewriteof命令会fork一个子进程,fork操作,这时候也会阻塞主线程。fork会生成一个子线程,跟父线程共享一样的数据副本,fork的时候会把主进程的页表复制一份给子进程,所以他们就共享同一个物理地址,但是会标记权限为只读。当子/父进程向这个内存发生写操作时,就会触发缺页中断,这是因为违反权限导致的,然后操作系统就会进行物理内存的复制,并重新设置映射关系到子进程的页表,这就叫写时复制,是为了避免fork子进程时进行物理内存的复制会导致父进程长时间阻塞。所以这里有两个阻塞

  • 一个是fork操作,复制父进程的页表的时候;
  • 一个是子进程创建完之后,如果子进程或者父进程修改了共享数据,就会发生写时复制,就会阻塞拷贝物理内存。子进程重写完成之后,就会发给父进程一个信号。

redis 大 Key、热 Key

大 Key

通常将含有较大数据或含有大量成员、列表数的 Key 称之为大 Key,下面我们将用几个实际的例子对大Key的特征进行描述:

  • 一个STRING类型的Key,它的 value 非常大
  • 一个LIST类型的Key,它的列表数量为 非常多
  • 一个ZSET类型的Key,它的成员数量为 非常多

大 Key 的危害

  • Client发现Redis变慢;
  • Redis内存不断变大引发 OOM,或达到maxmemory设置值引发写阻塞或重要Key被逐出;
  • Redis Cluster中的某个node内存远超其余node,但因Redis Cluster的数据迁移最小粒度为Key而无法将node上的内存均衡化;
  • 大Key上的读请求使Redis占用服务器全部带宽,自身变慢的同时影响到该服务器上的其它服务;
  • 删除一个大Key造成主库较长时间的阻塞引发同步中断或主从切换

大 Key 的解决方案

  • 对大 Key 进行拆分,对有大量成员的key进行拆分
  • 对大 Key 进行清理。
  • 对过期数据进行清理。

热 Key

在某个Key接收到的访问次数、显著高于其它 Key 时,我们可以将其称之为热 Key ,常见的热 Key 有:

  • 某Redis实例的每秒总访问量为10000,而其中一个Key的每秒访问量达到了7000(某个 key 的访问次数显著高于其他 key)
  • 对一个拥有数万个成员的ZSET Key每秒发送大量的ZRANGE(CPU时间占用显著高于其它Key)

热 Key 的危害

  • 热Key占用大量的Redis CPU时间使其性能变差并影响其它请求;
  • Redis Cluster中各 node 流量不均衡造成 Redis Cluster的分布式优势无法被Client利用,一个分片负载很高而其它分片十分空闲从而产生读/写热点问题;
  • 在抢购、秒杀活动中,由于商品对应库存Key的请求量过大超出Redis处理能力造成超卖;
  • 热Key的请求压力数量超出Redis的承受能力造成缓存击穿,此时大量强求将直接指向后端存储将其打挂并影响到其它业务;

热 Key 的解决方案

  • 在Redis Cluster结构中对热Key进行复制,存在问题就是联动修改,就带来了一致性问题,更新一个key 变成了更新多个key
  • 使用Redis 读写分离架构,
  • 使用阿里云的QueryCache特性,可以查询到redis 中的热点key,并且会缓存热点key的查询结果。

Reference

Redis常见问题
如何巧用Redis数据结构实现亿级数据统计?


目录