# Redis常见面试问题

Redis是一种运行在内存当中的非关系型数据库，使用c语言编写，由于他的高效读写能力，Redis通常用作缓存，分布式锁，分布式Session等场景

Redis原生还支持事务，持久化，集群方案等等。

在Redis之前，Memcached也经常拿来做缓存，只不过后来Memcached被淘汰了，两者的简单比较

相同点：

* 都是基于内存的数据库，一般都来作为缓存组件
* 两者的性能都非常优秀

不同点：

* Redis支持更加丰富的数据类型，除了Memcached仅仅支持的KV外，还支持list、set、zset、hash等数据结构
* Redis具有持久化特性，因此具有灾难恢复机制，Memcached没有（可用性）
* Redis可以将硬盘作为扩展存储当内存用完的时候使用，而Memcached会直接报错（扩展性）
* Redis原生支持集群方案，而Memcached需要在客户端内实现集群部署逻辑（可扩展性）
* Redis在6.0之前是单线程的多路IO复用模型，而Memcached是多线程，非阻塞的IO复用模型

数据缓存的一个处理流程：

用户查询Redis是否存在数据，如果存在直接返回

如果不存在，则访问数据库，如果数据库不存在则返回null

如果数据库存在，则更新Redis缓存并返回数据结果

经过这个流程的处理，系统并发量将不会直接被MySQL的并发上限所限制，增大了系统的吞吐量和响应时间。（高性能和高并发的角度）

Redis常见五大数据结构：

string数据结构，简单的kv类型，可以用于计数的场景，可以计算用户访问次数，文章访问量等等

是动态字符串，内部存储类似于Java当中的ArrayList，使用数组来完成空间的预分配，如果长度是小于1M（220）的时候扩容是直接翻倍的，如果是大于1M（220）时候扩容会增加1M空间，最大长度限制为512M

```
127.0.0.1:6379> set number 1
OK
127.0.0.1:6379> incr number
(integer) 2
127.0.0.1:6379> get number
"2"
127.0.0.1:6379> decr number
(integer) 1
127.0.0.1:6379> get number
"1"
```

list数据结构，类似于Java当中的LinkedList，双向链表实现，插入删除非常快，Redis这种数据结构常被作为内存队列来进行使用

```
127.0.0.1:6379> rpush myList v1
(integer) 1
127.0.0.1:6379> rpush myList v2 v3
(integer) 3
127.0.0.1:6379> lpop myList
"v1"
127.0.0.1:6379> rpop myList
"v3"
```

hash：和HashMap类似，内部使用数组+链表实现，特别适合用于存储对象

但是在使用过程当中是保证了他的可用性的，这点体现在了他的Rehash过程当中，也是和HashMap类似具有扩容特性，一旦扩容就会进行rehash，Redis为了保证rehash过程期间hash的可用性，是采用了渐进式rehash的，如需要将1扩容成2，创建一个大小为2的数组，进行数据迁移，前面的数据查询依然使用原来的字典，在复制过程当中只有全部完成了，才会使用新字典去代替原字典，保证流程不会阻塞

```
127.0.0.1:6379> hset userInfo name LuckyCurve age 21
(integer) 2
127.0.0.1:6379> hget userInfo name
"LuckyCurve"
127.0.0.1:6379> hget userinfo age
(nil)
127.0.0.1:6379> hget userInfo age
"21"
127.0.0.1:6379> hset userInfo age 22
(integer) 0
127.0.0.1:6379> hget userInfo age
"22"
```

set：类似于Java中的HashSet，具有唯一性的特性，也是使用哈希这种存储结构来保证元素的唯一性的，之所以可以保证唯一性，是减少对象间equals方法调用次数，并且支持集合的交并补运算

```
127.0.0.1:6379> sadd mySet1 v1 v2
(integer) 2
127.0.0.1:6379> sadd mySet2 v2 v3
(integer) 2
127.0.0.1:6379> sinterstore mySet3 mySet1 mySet2
(integer) 1
127.0.0.1:6379> smembers mySet3
1) "v2"
```

zset：有序集合，类似于HashMap具有了TreeMap的排序功能一样，适用于对某些场景需要根据对象字段进行排序的情况文件事件，底层实现是跳表。

增加了一列score来完成评分

跳表就是一个多层级的链表，高层级的链表是低层级链表的子集，构建这个层级链表的规则是将每两个节点提取一个节点出来做索引，将这些索引节点再构成一个链表，不断重复这个动作，直到当前层的节点个数为两个

删除数据的操作比较简单，只需要删除多层级的链表当中的每个元素就行了，但是插入时候可能导致两个索引节点之间的数据过于庞大，有一个随机策略来避免跳表退化

之所以选用跳表而不选择红黑树取决于以下两方面：

1. 范围查询时候跳表效率高
2. 插入删除较之于红黑树的涂色简单的多

Redis是基于Reactor模型（反映模型模型，基于事件驱动）设计出来的，和Netty类似，Reactor好像成为了NIO开发的基石了，Redis的核心为文件事件处理器，因为文件事件处理器是单线程方式运行的，因此我们说Redis是单线程的。

这里的文件事件处理器非常类似于NIO当中的Selector组件，Selector是实现了对大量Channel的管理，这里的文件事件管理器同时监听大量的Socket，来并发处理大量请求

体现Redis具有反应式编程的特性在于：Redis是等待套接字执行请求，发送事件给文件事务管理器，文件事务管理器通过调用不同的事件处理器来完成响应

Redis之所以不愿意使用多线程，一直保持单线程到6.0是因为单线程容易维护，且不存在性能瓶颈，同时避免了上下文切换和锁竞争的消耗

Redis6.0之后引入多线程仅仅是针对较大的网络数据的读写上了，核心还是单线程

Redis可以设置过期时间来保证一些长时间没有被使用到的数据被清除，Redis的过期时间除了可以缓解内存压力，我们还可以利用过期这种特性来完成特定业务逻辑，如短信验证码60s之内有效

是使用一个hash数据结构【过期字典】来维持的过期时间，key指向一个数据，value存储的是key所指对象的过期时间

Redis过期数据的删除策略：

* 惰性删除：只有在取出key的时候才对数据进行过期检查，对CPU友好，对内存不利【空间换时间】
* 定期删除：每隔一段时间根据过期字典删除一批数据，尽可能少影响CPU，对内存有利【时间换空间】

Redis采取的是定期删除和惰性删除结合的策略，

【问题】：如果此时key比较多，惰性删除和定期删除可能漏掉大量的key，就需要使用到内存淘汰机制了。

内存淘汰机制不仅可以解决过期删除策略导致的key堆积问题主动淘汰一些key，还可以对热点数据进行筛选并保留，存在以下几种策略：

volatile-LRU：从过期字典中挑选最近使用最少的数据淘汰

volatile-TTL：从过期字典中挑选将要过期的数据进行淘汰

Randon：从过期字典中随机挑选数据进行淘汰

allkeys-lru：当内存不足时候，从过期字典中移除最少使用的数据【最常用】

allkeys-random：从数据集当中挑选任意数据淘汰

no-evictiion：禁止驱逐数据（用的非常少了）

Redis持久化机制

Redis有两种持久化方案，快照和只追加文件

快照相当于是某个时间点上内存数据的一个副本，可以将快照放在磁盘上等待下次的数据恢复，也可以直接将快照传给其他的数据库来实现Redis的主从复制结构，快照就是Redis默认的持久化方案，**不要感觉奇怪，因为Redis数据变换是非常频繁的，直接使用快照反而有时候有优势，但是快照方式容易漏数据**

另一种持久化方案是只追加文件，类似于MySQL的redo log，直接在磁盘上记录更改Redis数据的命令。

4.0之后支持两个混合，但是默认依旧还是快照方式，混合之后就相当于基于某个版本的数据然后记录修改指令。

快照实现方式是fork一个子进程，这时候这两个进程所占有的空间其实是指向同一片内存区玉的（操作系统优化 CopyOnWrite），但是一旦来了写入请求后，这时候就会完成数据复制，然后将数据域当中的内容直接形成一个新的RDB文件，数据是fork时候的数据来源，只有在生成了新的RDB文件之后才会完成新RDB到原来RDB的替换操作，保证数据一致性。

1GB的文件加载到内存当中来花费20s\~30s的时间。

Redis提供数据保障容灾的几种方式：

* 哨兵模式（类似于非分布式场景下的容灾处理）：

哨兵可以理解成一个独立的进程，来保证当Master宕机了之后可以将Slave节点切换为Master节点，并且通过发布订阅模型来将这个变换操作通知到其他的Slave节点更改配置文件

* Redis Cluster（分布式场景，因为有很多限制，主要是单机QPS支持不上去，即便通过硬件扩容，可是内存瓶颈、网卡瓶颈都是客观存在的问题）

核心思想就是将数据分摊到不同的机器上面去，几种数据分布方式：

1. 顺序分布：如前多少条数据往其中一个机器当中存储，当这个机器存储容量达到阈值时候、下一段数据又往下一个机器上存储了，更加常见的是关系型数据库的分库分表设计当中
2.  哈希分布：可以进行进一步划分

    1.  节点取余分区，hash(key) % node 根据结果值来决定将当前数据存储在那一台机器上面

        > 存在一个很大的问题，当增加机器时候，会导致其中存储的数据都需要重新进行哈希运算来重新分区
        >
        > 建议做法是成倍地增加机器来减少数据迁移量，如从3台增加到6台，其中只有50%的数据会发生迁移
        >
        > > 以上故障可以人为避免，但是模拟另一种情况：其中一台服务器宕机，这时候整个缓存就不可用了，因为服务器数量变化，hash规则改变，所有缓存需要重新rehash，导致缓存雪崩问题
    2.  一致性哈希分区：当前服务器IP地址对232进行取余，将服务器打到这个hash环上面来，此时新来了数据之后，同样的使用哈希运算然后对232取余数，打到这个hash环上面来，数据会顺着这个环来找到下一个节点，这个节点负责对这个数据的存储

        ![img](https://www.zsythink.net/wp-content/uploads/2017/02/020717_1707_7.png)

        分配到token环上的数据，会通过向后嗅探的方式来完成存储位置的寻找，并且将数据存储到对应的机器上面去，即使其中一台服务器发生问题以后，也不会导致整个集群的不可用 存在的问题：可能主机存放不均，因为主机在环上的地址也是通过hash算出来的，可能出现这种情况：

        ![img](https://www.zsythink.net/wp-content/uploads/2017/02/020717_1707_13.png)

        也就是hash环的偏斜，导致缓存极度不均匀的分布在各节点上

    解决方法：使用虚拟节点，在hash环当中设置多个虚拟节点，通常都是32个甚至更多，我们只需要维护虚拟节点到实际节点的映射即可完成数据的存储。
3.  虚拟槽分区 共有16384个槽，每个节点都会负责一个槽范围，当数据进来之后，会完成hash运算并对16384进行取余，然后看是在那个节点负责的机器上，然后去对应的机器上完成数据的读取或写入即可

    当节点挂掉之后，会通知其他节点完成对槽的平分，这时候只需要进行少量的数据交换即可

    这里的Redis Cluster也就是根据这里的虚拟槽分区来完成的，多台Master节点负责一个槽范围，并且每个节点都是Master-Slave的结构设计的

[https://www.zsythink.net/archives/1182](https://www.zsythink.net/archives/1182)

Redis的事务，Redis的事务不同于传统的关系型数据库具有ACID特性，不支持事务回滚，因此不支持原子性和持久性，主要是为了性能考虑。Redis事务仅仅是提供命令打包功能，按顺序执行打包的所有操作，并且不会被中途打断。

使用Redis的两个常见问题：缓存穿透和缓存雪崩

缓存穿透：大量key不存在于Redis中，请求直接打到MySQL上了

布隆过滤器的核心就是一个byte数组，并且配合一系列的哈希函数进行扰动，对每个存在于MySQL中的元素，都会进行全部的哈希运算，得到多个哈希值，并将多个算出的哈希值置一。在查找的时候，也是同样的逻辑，将该元素进行哈希运算得到多个哈希值，判断这些哈希值对应的下标是否都为1，如果都为1则可能存在（主要存在一个哈希冲突，可以通过增大数组的容量和选择更优秀的哈希算法来解决），如果其中一个不为1则肯定不存在。

布隆过滤器的删除过程则比较复杂了。

缓存雪崩：主要指的是缓存数据大面积失效，导致请求直接打到了数据库上来，很可能导致数据库直接宕机

造成缓存大面积失效的原因可能是：Redis服务直接不可用、热点缓存数据突然失效

第一种解决方法：部署Redis集群，避免Redis服务宕机、限流，避免请求直接全部打到Redis上来

第二种解决方法：增加Redis所占内存，让内存中存取更多数据，从而导致缓存失效的比率降低。2、设置过期策略，设置成永不失效，防止一部分热点数据被丢弃

看到了一个缓存雪崩非常好的例子，假如我抢购页面是零点开始，而我现在在中午十二点缓存了数据，过期时间定为了12小时，刚好抢购时候缓存大面积失效，直接打到数据库上去，这就是缓存雪崩的一种案例。

解决方法：设置随机的过期时间，避免同一时间点缓存大面积失效

保证数据的一致性

无法做到短时间内的数据一致性，但是可以保证整体的数据一致性，即引入缓存的代价就是带来了短时间的数据不一致。

主要的解决策略为：更新DB的时候删除缓存，因为DB更新具有原子性，不用管，如果DB成功了但是cache删除失败了，建议定一个重试次数去一直重试。

通常，我们将保证数据一致性的策略叫做缓存读写策略，主要包括三种：

* 旁路缓存模式

适合读多写少的场景，也是最常用到的，读操作与上面说的一样，写操作则是先写DB，然后在删除cache。

为什么要先删除DB再删除cache呢？主要是因为cache的读写速度会比DB快的多，让DB先执行，cache后执行可以最大可能的减少数据不一致的时间。当然该方案只能减少数据不一致时候的时间，无法完全避免

缺陷：

1、首次请求的时候一定是不命中数据的

解决方法：预先加载一部分热数据进行预热

2、Redis和MySQL不具备强一致性

某个时间点仍然会存在Redis和MySQL数据不一致的问题，解决方法：使用分布式锁

* 读写穿透

运用的比较少，让应用直接和Redis打交道，Redis自身去做将数据写入DB的工作，这是非常少见的，因为Redis不支持将数据写入到DB当中

操作逻辑为：

读操作：从cache中获取数据，获取到了直接返回，如果获取不到，从DB加载到cache中，然后再由cache返回

写操作：先查cache，如果不存在则直接更新DB，如果存在则更新cache，然后cache自己去更新DB

缺陷：

1、首次请求数据必然不会命中缓存

* 异步缓存写入

感觉使用的更少了，是在读写穿透的基础上进行了改变，在数据更新的时候如果数据存储在cache中，读写穿透是cache同步调用DB去进行数据更新，而异步缓存写入则是只更新缓存，不直接更新DB，而是改为异步批量的方式更新DB、

可以看出写性能是非常高的，直接将数据改变完全写入到内存当中，适用于数据经常变化但是对数据一致性要求没那么高的场景。

缺陷：

1、首次访问数据必然不会出现在缓存

2、数据一致性可能比较差

Redis集群部署方式：

1、主从哨兵模式（这个是为了）
