目录

本文引用redis源码版本为3.0

redis面试题

Linux中的fork()函数

什么是redis?

redis支持的数据类型?

String

Hash

List

Set

ZSet

Redis对象

 跳表(SkipList)

redis数据库底层实现

redis持久化机制

RDB(redis database)

AOF(append only file)

如何选择使用哪种持久化方式?

什么是缓存穿透?

什么是缓存击穿?

什么是缓存雪崩?

布隆过滤器与缓存穿透

redis与memcahch的区别?

redis为什么是单线程的?

redis速度快的原因?

redis缓存过期策略(怎么删除过期的缓存)

redis官方的集群搭建方案

一致性hash算法

redis分布式锁

单server + 单redis

server集群 + 单redis

server集群 + redis集群

redis分布式锁(官方)

如何保证redis和数据库数据一致性?

redis容器报错

在docker中使用指定配置文件的方式启动redis-server失败(容器启动后立刻退出)

在docker容器中使用redis-cli的shutdown命令报错

大厂面试题

字节跳动


本文引用redis源码版本为3.0

redis面试题

Linux中的fork()函数

fork()函数通过系统调用创建一个与原来进程完全相同的子进程,系统会给子进程分配资源(父子进程不共享空间),然后把父进程里面所有的数据复制一份到子进程中,只有少数值与原来不同,等于克隆了一个父进程。两个进程可以做相同的事,如果初始参数或传入的变量不同,两个进程也可以做不同的事。

什么是redis?

redis是一个非关系型数据库,数据都以键值对的方式存储在内存中,并且支持数据的持久化。redis能存储的值的类型包括:string、list、set、zset(有序集合)和hash类型。

redis支持的数据类型?

String

  • 最基本的数据类型,一个key对应一个value,一个键最大能存储512M,可以存储任何数据,如图片或序列化的对象。
  • String类型是二进制安全的,如果数据在传输过程汇总被篡改,可以立刻检测出来。
  • 整数和字符串都以String类型进行存储,在内存中和序列化文件(如RDB文件)中,存储的格式为【ENCODING | DATA】,其中ENCODING为编码,如:REDIS_ENCODING_RAW代表字符串,REDIS_ENCODING_INT8、REDIS_ENCODING_INT16、REDIS_ENCODING_INT31代表不同长度的字符串。例子如:【REDIS_ENCODING_RAW | "hello"】和【REDIS_ENCODING_INT8 | 123】。通过编码能知道数字类String的字节长度,然后读取数字,再根据数字读取相应长度的字符串数据。

Hash

hash是一个string类型的field和value的映射表,适合存储对象,类似Java中的HashMap。通过hash表、压缩列表(ziplist)实现,redis内部实现了hash表数据结构。

如:hmset users:1 id 1 username zhangsan age 22.

redis的定期删除 redis定期删除ttl_面试

List

双向链表,类似Java中的LinkedList(Deque,双端队列)。可以用来做消息队列。底层实现包括:双向链表和压缩列表(ziplist)。

redis的定期删除 redis定期删除ttl_数据_02

Set

  • set是无序集合,集合内成员是唯一的,通过字典、intset来实现。下面是set添加数据的源代码:
/* set添加。subject为set对象,value为插入元素 */
int setTypeAdd(robj *subject, robj *value) {
    long long llval;
    // 若set对象的底层实现为字典
    if (subject->encoding == REDIS_ENCODING_HT) {
        // 将(value, null)插入字典中。函数原型:int dictAdd(dict *d, void *key, void *val)
        if (dictAdd(subject->ptr,value,NULL) == DICT_OK) {
            // ...
            // return
        }
    // 若set对象底层实现为intset
    } else if (subject->encoding == REDIS_ENCODING_INTSET) {
        // 判断value是否是整数类型
        if (isObjectRepresentableAsLongLong(value,&llval) == REDIS_OK) {
            // 插入
            // return
        } else {
            // 如果value不是整数类型,则将intset转换成普通set再插入
            // return
        }
    } else {
        redisPanic("Unknown set encoding");
    }
    return 0;
}

/* intset底层数据结构 */
typedef struct intset {
    uint32_t encoding;        // 元素编码,表示contents中元素的数据类型
    uint32_t length;          // contents中元素个数
    int8_t contents[];        // 存储数据的数组,int8_t为元素初始类型
} intset;
  • 可以统计一些具有唯一性的数据,如访问网站的IP。

ZSet

  • 有序集合,集合内成员是唯一的,底层数据结构为压缩列表(ziplist)、intset、跳表(skiplist)其中之一,CRUD的时间复杂度都是O(lgn)。
  • zset中的每个元素都会关联一个double类型的数,redis就是根据这个数来进行从小到大排序的,这个数是可以重复的。
  • 可以用来做一些排行榜的应用。

Redis对象

redis中的基本对象类型为:string、list、hash、set、zset,但在源码中使用较多的是封装的抽象类型redisObject,数据结构如下:

typedef struct redisObject {
    // 对象类型,如:string、list等
    unsigned type:4;
    // 对象编码,当前类型底层使用的数据结构
    unsigned encoding:4;
    // 对象最后一次被命令访问的时间,用于计算对象的空转时长
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    // 对象引用计数,用于对象的回收
    int refcount;
    // 对象实际内存指针
    void *ptr;
} robj;

redis的定期删除 redis定期删除ttl_数据库_03

 跳表(SkipList)

跳表是zset类型的实现方式之一,是有序的链表。数据结构如下:

redis的定期删除 redis定期删除ttl_redis的定期删除_04

redis的定期删除 redis定期删除ttl_面试_05

 

redis的定期删除 redis定期删除ttl_redis_06

更直白一点如下图,每次插入元素时都会根据幂次定律(越大的数出现的概率越小),随机生成一个介于1~32之间的数作为level数组的大小,这个大小也是插入元素的层数。

redis的定期删除 redis定期删除ttl_数据_07

  • 时间复杂度:增删改查的时间复杂度都为O(logn),与表的最大高度相关。

 

redis的定期删除 redis定期删除ttl_数据库_08


redis数据库底层实现

  • 数据结构:redis数据库底层使用dict(字典)实现,里面包含2个hash表,一个用于存储数据,一个用于rehash。

redis的定期删除 redis定期删除ttl_数据库_09

  • hash算法:使用Murmurhash算法计算出hahs值,hash & sizemask(sizemask为hash表的长度减1)。Murmurhash计算的速度非常快,而且即使输入的键是有规律的,得出来的hash仍然保持较高的随机分布性。
  • 冲突:字典使用链地址法解决hash冲突。
  • rehash算法:随着操作的执行,hash表中保存的键值对会不断增加或减少,为了让hash表的负载因子维持在一个合理值,要进行rehash,具体分为缩容或扩容。在此期间,CRUD操作在两张表上进行。

redis的定期删除 redis定期删除ttl_数据库_10

  • rehash触发时机:如果执行了BGSAVE指令,就会触发持久化操作。copy on write就是copy一部分键值对,持久化,再copy一部分,再持久化。如果copy了50%,突然发生了rehash,那子进程就不知道接下来从哪里copy了,必须重头开始copy。

redis的定期删除 redis定期删除ttl_数据_11

  •  渐进式rehash:如果hash表中的数据量非常大,那么一次性完成rehash耗费的时间很长,因此有了渐进式的rehash。在此期间,CRUD操作在两张表上进行。

redis的定期删除 redis定期删除ttl_redis_12

redis持久化机制

redis的持久化机制有两种:RDB和AOF。

RDB(redis database)

RDB持久化方式就是在指定的条件下对内存中的数据进行快照存储,数据存储到一个.rdb的二进制文件中。

RDB优点:

  • 能很方便的保存过去某个时间点的数据,且保存的快照文件体积小,数据恢复的速度快,非常适合用来做备份。
  • 通过fork(见上面fork()介绍)出一个redis子进程来做保存,能最大化redis的性能。fork的时机在redis.conf中配置如下:
save [seconds changes]——在s秒之内如果有c个key被更新,则进行持久化
    #Redis默认配置文件中提供了三个条件:
    #save 900 1
    #save 300 10
    #save 60 10000

#也可以手动执行命令进行持久化:
	1)bgsave命令是异步保存,此时redis可以继续提供服务,但是命令执行后新的更新不会被保存。
	2)save命令时同步保存,当子进程保存时,redis会停止接受新的命令。

RDB缺点:

  • redis意外终止可以会导致上一次持久化之后的更新数据的丢失。

AOF(append only file)

通过每隔一段时间(指定的),保存所有对服务器的更新操作来做持久化,所有操作按顺序添加到一个文件末尾。

AOF优点:

  • 通过创建一个后台线程来执行fsync()函数来将缓冲区中的用户命令追加到AOF文件末尾,能提高redis性能。在redis.conf中的配置如下:
1)appendfsync no
    2)appendfsync always
    3)appendfsync everysec    每隔1s就将缓存中的数据写到AOF文件末尾
  • 在上面的策略下,能最大限度地保证数据的完整性(默认配置下,最多丢失1s的数据)。

AOF缺点:

  • 对相同的数据集而言,AOF文件的体积要大于RDB文件的体积。
  • 当数据集比较大时,AOF在数据的恢复上比RDB要慢。

如何选择使用哪种持久化方式?

  • 若你不关心几分钟内数据的丢失就可以只选择RDB方式。
  • 若你对数据的安全性要求很高,那最好两种方式都使用。
  • 不推荐只使用AOF一种方式,因为RDB在数据备份和数据恢复方面要比AOF的速度要快。

什么是缓存穿透?

  • 定义:用户查询的key缓存和数据库中都没有,以后每次查询这个key都会请求数据库,因为数据库中查不到自然也不会写到缓存中。当对这类key的并发访问量非常大时,会给底层数据库带来非常大的负担。
  • 解决:key在数据库中也查询不到时,把这个空的结果缓存,然后设置一个过期时间  ; 布隆过滤器。

什么是缓存击穿?

  • 定义:用户查询的key缓存中没有,但是数据库中有。某个key查询的并发量非常大,如果这个key失效了,失效的瞬间会产生大量的数据库请求,给数据库带来很大的负担。
  • 解决:热点数据设置永不过期 ; 使用分布式锁,将查到的数据库的值写回缓存,然后释放锁。
// 服务器的数据请求
Object res = getFromRedis(key);    // 同一时间大量线程访问到这里,如果缓存为空
if (res == null){
    synchronized (key){        // 在分布式环境下synchronized 会失效,要用setnx来加锁
        Object res = getFromRedis(key);
        if (res == null){
            res = getFromMysql(key);        // 就都从数据库获取
            setToRedis(key);                // 将结果放入redis中
        }
    }
}    
return res;

什么是缓存雪崩?

  • 定义:在某一时间有大量的key失效,造成大量的数据库请求,给数据库造成巨大的压力。
  • 解决:将key的失效时间均匀分开 ; 采用分布式锁,获取锁失败的线程自旋或阻塞或放入消息队列,之后从缓存中获取数据。

布隆过滤器与缓存穿透

  • 功能:能够概率性知道哪些数据不存在(判断不存在的一定不存在,判断存在的也可能不存在)。能过滤掉大部分不存在的数据,剩下的少数误判会导致请求数据库,但带来的负担大大减轻。所以这是一种缓存穿透解决办法。
  • 数据结构:bit数组和若干个hash函数的结合。数组位置上只存储0和1,hash函数用来对添加的数据做映射。只能添加,不能删除。
  • 原理:当添加数据时,将数据通过若干个hash函数映射到bit数组中,映射位置的值设为1。当查询某个数据是否存在时,通过若干个hash函数查出映射位置的值,若有一个位置为0,说明不存在。
  • 效率:效率与hash函数的个数和数组的长度有关。一般来说,数组长度越长,准确率越高;hash函数少了,准确率低,多了数组容易被填满。所以要平衡这两个参数。
  • 注意:若查询某个数据,所有位置都为1,那这个数据也不一定存在,这意味着查询不存在的数据时还是会直接访问数据库。
  • redis缓存穿透应用:能够知道某个key是否存在,若不存在直接返回,避免访问数据库。
  • 缺陷:但就如上面所说,判断是有误差的,查询不存在的数据时可能还是会直接访问数据库;当缓存过期或者数据库中数据被删除时,由于bit数组不会一起更新,所以下次查询被删除的数据时会判断存在,进而直接访问数据库。(不能解决缓存失效或雪崩)

redis与memcahch的区别?

  1. redis支持数据的持久化,memcahch不支持持久化。
  2. redis支持string、list、hash、set、zset等数据类型,memcahch只支持string。
  3. redis的value最大为512M,memcahch最大为1M。
  4. redis支持单线程,memcahch支持多线程。
  5. 集群方面。。。

redis为什么是单线程的?

  • 可行性:因为不像mysql把数据存在磁盘上,redis的数据都是存在内存中的,内存中的数据读写速度是非常快的,即使在单线程的情况下也能达到每秒平均10w次的读写速度(官方数据),因此单线程方案是可行。这样一来,线程数量(CPU)不是redis的瓶颈,内存大小才是,内存越大,redis就能存储更多的数据,更多的用户请求就能达到内存级别的响应速度。(后面要测试mysql每秒的读写速度)。
  • 优点:同时,使用单线程也有一些好处。使用单线程能够避免加锁、释放锁的销号,不用考虑死锁的问题,也能减少进线程切换带来的系统开销。

下面是官方的bench-mark数据: 

测试完成了50个并发执行100000个请求。

设置和获取的值是一个256字节字符串。

Linux box是运行Linux 2.6,这是X3320 Xeon 2.5 ghz。

文本执行使用loopback接口(127.0.0.1)。

结果:读的速度是110000次/s,写的速度是81000次/s 。

redis速度快的原因?

  1. 数据存储在内存中(最基础最重要)。
  2. 单线程,减少了上下文的切换。
  3. 采用非阻塞IO多路复用机制。

redis缓存过期策略(怎么删除过期的缓存)

redis中过期缓存的删除采用的是:定期删除+惰性删除+内存淘汰策略。

  • 定期随机删除:redis默认每隔100ms随机检查一批数据,并将其中过期的数据删除。
  • 惰性删除:每次用户查询数据时,检查数据是否过期,过期则删除。
  • 内存淘汰策略:过期删除和惰性删除都具有随机性,在极端情况下会出现redis存在大量过期数据的情况,这会占用大量内存资源,严重影响redis性能。所以redis使用内存淘汰策略来防止这种情况的发生。

内存淘汰策略

在redis.conf中有两个属性:

  • maxmemory [bytes]:用来指定redis的能够占用的最大内存。
  • maxmemory-policy [policy]:当redis内存超过maxmemery后,执行指定的内存淘汰策略。

redis5.0中的内存淘汰策略包括:

  • volatile-lru:从已设置过期时间的数据中挑选最近最少使用的数据。
  • volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
  • volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
  • volatile-random:从已设置过期时间的数据集中选择任意数据淘汰。
  • allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
  • allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰。
  • allkeys-random:从数据集(server.db[i].dict)中选择任意数据淘汰
  • no-enviction(驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,读操作可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失。

redis官方的集群搭建方案

redis从3.0开始,官方提供了一种分布式的集群方案。

  • 分布式:在数据的存储上没有采用一致性hash,而是引入了hash槽。redis集群有16384个hash槽,每一个节点负责一部分hash槽。通过将key进行运算然后对16384取模,决定数据存在哪一个节点上。
  • 集群:集群至少有3个master节点,官方推荐至少设立6个节点,3主3从,当主节点挂掉时,从节点替代成为主节点。
  • 缺点:redis并不能保证强一致性,这意味着可能发生丢失写操作的情况。
  • 1.因为主从复制发生在对客户端发送回复命令之后,若回复之后复制完成之前主节点宕机,则数据会丢失。
  • 2.网络分区。client向A节点写入数据,但client和A被分为一区,与其它节点孤立开来,若A1在网络回复之前被选举为master,则写操作丢失。

一致性hash算法

一致性hash算法主要是为了解决分布式缓存的问题。

好刚: 7分钟视频详解一致性hash 算法_哔哩哔哩_bilibili

  • 传统缓存方式:假设有3台图片缓存服务器A、B、C,当插入一张图片a.jpg时,按照:hash(a.jpg)%3来决定将图片插入或读取哪一台服务器。
  • 问题:当添加或减少一台服务器时,3变成4或2,会直接影响取模的结果,导致读取到其它的服务器上,出现缓存雪崩,压垮后台服务器。

一致性hash算法将服务器和插入的图片hash到一个0~2^32-1的hash环上,图片按顺时针存储在最近的服务器上(通过比较图片和所有服务器的hash值,能快速确认),当一个新的服务器节点D加入进来后,仅仅映射在节点C和节点D之间的图片会定位到错误的服务器而导致访问后台数据库,其它范围的数据还是能正常从缓存中读取。

redis的定期删除 redis定期删除ttl_redis_13

注:也可能出下面情况,一个节点缓存了大量的数据,当D如下图加入时,会有大量的缓存失效,引起缓存雪崩。解决办法就是缓存大量数据的节点增加许多虚拟节点,使得缓存数据均匀分布。

redis的定期删除 redis定期删除ttl_redis的定期删除_14

redis分布式锁

这里以减库存的例子作介绍,多个线程对redis中的库存数量进行-1操作,要保证线程安全。

单server + 单redis

单server下可以不用setnx,直接用synchronized进行并发控制,只有获取锁的线程才能对redis的库存进行--。

server集群 + 单redis

server集群下,synchronized就没用了,要用setnx来设置分布式锁。

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系,Redis中可以使用SETNX命令实现加锁。

将 key 的值设为 value ,当且仅当 key 不存在。 若给定的 key 已经存在,则 SETNX 不做任何动作。

redis的定期删除 redis定期删除ttl_数据_15


解锁:使用 del key 命令就能释放锁;或者给key设置自动过期的时间。

解决死锁:

1)通过Redis中expire()给锁设定最大持有时间,如果超过,则Redis来帮我们释放锁。

2) 使用 setnx key “当前系统时间+锁持有的时间”和get set key “当前系统时间+锁持有的时间”组合的命令就可以实现。

注意:当客户端A获取锁了,使用完资源之后会释放锁,即del key,但是如果A执行时间过长,导致锁过期了,此时锁被另一个客户端B获取了,A再释放锁,所导致A释放了B的锁,引发程序错误,所以这里下面要用clientId(只要是唯一值即可)座位value,来保证A删除的是自己的锁,而不是B的。

redis的定期删除 redis定期删除ttl_redis_16

server集群 + redis集群

如果是多redis服务,则要逐一给所有的redis服务器发送setnx请求,只有获取到了n/2+1个服务器的锁,才能是获取锁成功。获取锁成功且使用完后或获取锁失败后,都要在所有redis服务器上释放锁。详情见:REDIS distlock -- Redis中国用户组(CRUG)因此要在上面减库存的代码中,更改setnx代码,对所有的redis服务器进行setnx,只有在超过半数的redis上设置成功才算是获取锁成功。

redis分布式锁(官方)

REDIS distlock -- Redis中国用户组(CRUG)

如何保证redis和数据库数据一致性?

  • 能忍受数据不一致:设置缓存失效时间,比如商品的库存数量,设置失效时间为1分钟,1分钟内可能出现数据不一致情况,但失效之后就会更新保持一致。
  • 不能忍受数据不一致(读多写少):分布式读写锁,保证数据的强一致性。
  • 不能忍受数据不一致(读多写多):异步写回。应用程序只面向redis进行读写,同时在后台开启线程进行异步写回数据库,根据业务场景设置异步写回的频率。

redis容器报错

在docker中使用指定配置文件的方式启动redis-server失败(容器启动后立刻退出)

使用的命令如下,创建并启动一个redis容器,然后启动redis容器的server服务并指定server服务的启动配置文件。

docker run 
-p 6379:6379 #端口映射
-v /usr/local/docker/redis/redis.conf:/usr/local/etc/redis/redis.conf #将本地的redis.conf映射到启动的redis容器中
-v /usr/local/docker/redis/data:/data #同上
--name redis #创建的容器名称
-d           #后台启动
redis:5.0    #容器的镜像名称
redis-server /usr/local/etc/redis/redis.conf #指定redis启动的配置文件

在使用docker安装redis时,不能在redis.conf中将daemonize 属性设置为yes,否则通过指定配置文件的方式启动redis-server会失败,docker容器启动后会立刻退出。

在docker容器中使用redis-cli的shutdown命令报错

成功启动redis容器后,使用以下命令后报错:

docker exec -it redis redis-cli -a xxxxxx
set a 10
shutdown

Failed opening the RDB file dump.rdb (in server root dir /data) for saving: Permission denied

当客户端关闭redis服务器的时候(shutdown命令),redis会执行持久化操作,将内存中的数据更新到dump.rdb文件中。显然上面说的是dump.rdb文件不能打开,没有权限,可是我容器的/data目录下根本就没有dump.rdb文件,所以这个错误就让人摸不着头脑了。查了一下,解决办法如下:

重启后再启动docker容器会出现Error response from daemon: error creating overlay mount to xxx merged: invalid argument的错误,此时要:

  • 修改docker的配置。(注意要删除docker镜像:docker rm $(docker ps -a -q))。

大厂面试题

字节跳动

1.zset结构

9870_Redis数据库 | ProcessOn免费在线作图,在线流程图,在线思维导图

2.hash扩容

3.key-value设置了ttl(过期时间),到期了一定会被删除吗?

不一定,因为redis中删除到期数据是要触发一定的条件的,条件详情参考上面“缓存过期策略”。

4.redis应用场景题,如何保证缓存与数据库一致性?