redis面试题
String数据类型、List 数据类型、Hash数据类型(散列类型)、set数据类型(无序集合)、Sorted Set数据类型 (zset、有序集合)。
redis底层的数据结构有6种,包括「动态字符串、双向链表、压缩列表(ziplist)、hash表、跳表(skip list)和整数数组」。

1、String是 redis 最基本的类型,最大能存储 512MB 的数据,String类型是二进制安全的,即可以存储任何数据、比如数字、图片、序列化对象等。INCR key: key值递增加1 ( key值必须为整数)、DECR key: key值递增减1 (key值必须为整数)、GETSET key value: 获取key值并返回,同时给key设置新值。
2、Hash数据类型(散列类型)、hash用于存储对象。可以采用这样的命名方式:对象类别和ID构成键名,使用字段表示对象的属性,而字段值则存储属性值。 如:存储 ID 为 2 的汽车对象。如果Hash中包含很少的字段,那么该类型的数据也将仅占用很少的磁盘空间。每一个Hash可以存储4294967295个键值对。
3、Sorted Set数据类型,有序集合,元素类型为Sting,元素具有唯一性, 不能重复每个元素都会关联–个double类型的分数score(表示权重),可以通过权重的大小排序,元素的score可以相同应用范围:可以用于一个大型在线游戏的积分排行榜。每当玩家的分数发生变化时,可以执行ZADD命 令更新玩家的分数,此后再通过ZRANGE命令获取积分TOP10的用户信息。
4、Redis内部使用一个redisObject对象来表示所有的key和value,type代表一个value对象具体是何种数据类型,encoding是不同数据类型在redis内部的存储方式,比如:type=string代表value存储的是一个普通字符串,那么对应的encoding可以是raw或者是int,如果是int则代表实际redis内部是按数值型类存储和表示这个字符串的,当然前提是这个字符串本身可以用数值表示,比如:"123" "456"这样的字符串。
问:redis为什么快
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,数据以key:value的格式存储在散列表中,对数据操作也简单,Redis中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、使用多路I/O复用模型,非阻塞IO;

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
什么是Redis持久化?Redis有哪几种持久化方式?优缺点是什么?
持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。
Redis 提供了两种持久化方式:写后日志(AOF),一种是内存快照(RDB)(默认)
比较:
1、aof文件比rdb更新频率高,优先使用aof还原数据。
2、aof比rdb更安全也更大
3、rdb性能比aof好
4、如果两个都配了优先加载AOF
ps:
1.AOF日志: AOF日志记录了每一条收到的命令,redis故障宕机恢复时,可以加载AOF日志中的命令进行重放来进行故障恢复。AOF有3种同步策略,如下图:

如果不是对丢失数据特别敏感的业务,推荐使用everysec,对主线程的阻塞少,故障后丢失数据只有1s。
2.RDB快照: RDB快照是一个内存快照,记录了redis某一时刻的全部数据。
3. 混合日志: 从redis4.0开始,AOF文件也可以保存RDB快照,AOF重写的时候redis会把AOF文件内容清空,先记录一份RDB快照,这份数据以"REDIS"开头。记录RDB内容后,AOF文件会接着记录AOF命令。故障恢复时,先加载AOF文件中RDB快照,然后回放AOF文件中后面的命令。
4.主从同步: redis主从同步时,主节点会先生成一份RDB快照发送给从节点,把快照之后的命令写入主从同步缓存区(replication buffer),从节点把RDB文件加载完成后,主节点把缓存区命令发送给从节点。
5.AOF重写: AOF日志是用记录命令的方式追加的,这样可能存在对同一个key的多条命令,这些命令是可以合并成1条的。比如对同一个key的多个set操作日志,可以合成一条。
6.阻塞点: AOF重写和RDB快照执行的过程中,redis都会fork一个子进程来执行操作,子进程执行过程中是不是阻塞主线程的。
2点:」fork子进程的过程中,redis主线程会拷贝一份内存页表(记录了虚拟内存和物理内存的映射关系)给子进程,这个过程是阻塞的,redis主线程内存越大,阻塞时间越长;- 子进程和
redis主线程共用一块儿物理内存,如果新的请求到来,必须使用copy on write的方式,拷贝要修改的数据页到新的内存空间进行修改。如下图:注意:如果开启了内存大页,每次拷贝都需要分配2MB的内存。
redis通讯协议(RESP ),能解释下什么是RESP?有什么特点?
RESP 是redis客户端和服务端之前使用的一种通讯协议;
RESP 的特点:实现简单、快速解析、可读性好
什么是缓存穿透?如何避免?什么是缓存雪崩?何如避免?
缓存穿透
一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
如何避免?
1:对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。
2:对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。
缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。
如何避免?
1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
2:做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期
3:不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

三、redis的命中率
用户在访问缓存中的数据时,并不是100%会返回的,可能有不返回的情况,这种情况下就得去DB查询数据,为了提高系统的性能,我们需要提高缓存的命中率。redis中info命令可以查询到基本信息。
命中率 = keyspace_hits / (keyspace_misses + keyspace_hits)
合理的redis配置可以提高命中率
如何保证mysql和redis数据一致性
1.延迟双删 在写库前后都进行【redis.del(key)】操作,并且设定合理的超时时间;
2.异步更新缓存(基于订阅binlog的同步机制)
技术整体思路:
MySQL binlog增量订阅消费+消息队列+增量数据更新到redis
1)读Redis:热数据基本都在Redis
2)写MySQL:增删改都是操作MySQL
3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis
Redis更新
(1)数据操作主要分为两大块:
一个是全量(将全部数据一次写入到redis)一个是增量(实时更新)
这里说的是增量,指的是mysql的update、insert、delate变更数据。
(2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。
这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。
其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。
这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。
当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis
Redis 的数据淘汰策略有哪些
a、noeviction:返回错误当内存限制达到,并且客户端尝试执行会让更多内存被使用的命令。
b、allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
c、volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
d、allkeys-random: 回收随机的键使得新添加的数据有空间存放。
e、volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键
f、volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
volatile-lfu和allkeys-lfu策略是4.0版本新增的。
- 「lru」是按照数据的最近最少访问原则来淘汰数据,可能存在的问题是如果大批量冷数据最近被访问了一次,就会占用大量内存空间,如果缓存满了,部分热数据就会被淘汰掉。
- 「lfu」是按照数据的最小访问频率访问次数原则来淘汰数据,如果两个数据的访问次数相同,则把访问时间较早的数据淘汰。
redis实现延时队列的两种方式
一,redis的过期key监控
1.开启过期key监听 在redis的配置里把这个注释去掉 notify-keyspace-events Ex 然后重启redis
2. 使用redis过期监听实现延迟队列 继承KeyExpirationEventMessageListener类,实现父类的方法,就可以监听key过期时间了。当有key过期,就会执行这里。这里就把需要的key过滤出来,然后发送给消息队列。
注意:尽量单机运行,因为多台机器都会执行,浪费cpu,增加数据库负担。二是,机器频繁部署的时候,如果有时间间隔,会出现数据的漏处理。
一,redis的zset实现延迟队列
1.生产者很简单,其实就是利用zset的特性,给一个zset添加元素而已,而时间就是它的score。
1 public void produce(Integer taskId, long exeTime) { 2 System.out.println("加入任务, taskId: " + taskId + ", exeTime: " + exeTime + ", 当前时间:" + LocalDateTime.now()); 3 4 RedisOps.getJedis().zadd(RedisOps.key, exeTime, String.valueOf(taskId)); 5 }
2.消费者代码,把已经过期的zset中的元素给删除掉,然后处理数据。
1 public void consumer() { 2 Executors.newSingleThreadExecutor().submit(new Runnable() { 3 @Override 4 public void run() { 5 while (true) { 6 Set<String> taskIdSet = RedisOps.getJedis().zrangeByScore(RedisOps.key, 0, System.currentTimeMillis(), 0, 1); 7 if (taskIdSet == null || taskIdSet.isEmpty()) { 8 System.out.println("没有任务"); 9 10 } else { 11 taskIdSet.forEach(id -> { 12 long result = RedisOps.getJedis().zrem(RedisOps.key, id); 13 if (result == 1L) { 14 System.out.println("从延时队列中获取到任务,taskId:" + id + " , 当前时间:" + LocalDateTime.now()); 15 } 16 }); 17 } 18 try { 19 TimeUnit.MILLISECONDS.sleep(100); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 } 24 } 25 }); 26 }
redis为什么变慢了:redis变慢原因主要有两类:「阻塞主线程和操作系统限制」。
1.主线程阻塞
1.1 AOF重写和RDB快照
前面已经讲过了,redis在AOF重写时,主线程会fork出一个bgrewriteaof子进程。
redis进行RDB快照时主线程会fork出一个bgsave子进程。
这两个操作表面上看不阻塞主线程,但fork子进程的这个过程是在主线程完成的。fork子进程时redis需要拷贝内存页表,如果redis实例很大,这个拷贝会耗费大量的CPU资源,阻塞主线程的时间也会变长。
1.2 内存大页
redis默认支持内存大页是2MB,使用内存大页,一定程度上可以减少redis的内存分配次数,但是对数据持久化会有一定影响。
redis在AOF重写和RDB快照过程中,如果主线程收到新的写请求,就需要CopyOnWrite。使用了内存大页,即使redis只修改其中一个大小是1kb的key,也需要拷贝一整页的数据,即2MB。在写入量较多时,大量拷贝就会导致redis性能下降。
1.3 命令复杂度高
执行复杂度高的命令是造成redis阻塞的常见原因。比如对一个set或者list数据类型执行SORT操作,复杂度是O(N+M*log(M))。
1.4 bigkey操作
如果一个key的value非常大,创建的时候分配内存会很耗时,删除的时候释放内存也很耗时。
redis4.0以后引入了layfree机制,可以使用子进程异步删除,从而不影响主线程执行。用UNLINK命令替代DEL命令,就可以使用子进程异步删除。
redis6.0增加了配置项lazyfree-lazy-user-del,配置成yes后,del命令也可以用子进程异步删除。
如果lazyfree-lazy-user-del不设置为yes,那redis是否采用异步删除,是要看删除的时机的。对于String类型和底层采用整数数组和压缩列表的数据类型,redis是不会采用异步删除的。
1.5 从节点全量同步
从节点全量同步过程中,需要先清除内存中的数据,然后再加载RDB文件,这个过程中是阻塞的,如果有读请求到来,只能等到加载RDB文件完成后才能处理请求,所以响应会很慢。
另外,如果redis实例很大,也会造成RDB文件太大,从库加载时间长。所以尽量保持redis实例不要太大,比如单个实例限制4G,如果超出就采用切片集群。
1.6 AOF同步写盘
appendfsync策略有3种:always、everysec、no,如果采用always,每个命令都会同步写盘,这个过程是阻塞的,等写盘成功后才能处理下一条命令。
除非是严格不能丢数据的场景,否则尽量不要选择always策略,推荐尽量选择everysec策略,如果对丢失数据不敏感,可以采用no。
1.7 内存打到maxmemory
内存达到maxmemory,需要使用淘汰策略来淘汰部分key。即使采用lazyfree异步删除,选择key的过程也是阻塞的。(可以选择较快的淘汰策略,比如用随机淘汰来替换LRU和LFU算法淘汰。也可以扩大切片数量来减轻淘汰key的时间消耗)
2.操作系统限制
2.1 使用了swap
使用swap的原因是操作系统不能给redis分配足够大的内存,如果操作其他开启了swap,内存数据就需要不停地跟swap换入和换出,对性能影响非常大。
操作系统没有能力分配内存的原因也可能是其他进程使用了大量的内存。
2.2 网络问题
如果网卡负载很大,对redis性能影响会很大。这一方面有可能redis的访问量确实很高,另一方面也可能是有其他流量大的程序占用了带宽。
这个最好从运维层面进行监控。
2.3 线程上下文切换
redis虽然是单线程的,但是在多核cpu的情况下,也可能会发生上下文切换。如果主线程从一个物理核切换到了另一个物理核,那就不能使用CPU高效的一级缓存和二级缓存了。如下图所示:为防止这种情况,可以把redis绑定到一个CPU物理核。

2.4 磁盘性能较低
对于AOF同步写盘的使用场景,如果磁盘性能低,也会影响redis的响应。可以优先采用性能更好的SSD硬盘。
使用redis 设计排行榜功能
redis的zset类型保存了分数值,可以方便的实现排行榜的功能。
比如要统计10篇文章的排行榜,可以先建立一个存放10篇文章的zset,每当有读者阅读一篇文章时,就用ZINCRBY命令给这篇文章的分数加1,最后可以用range命令统计排行榜前几位的文章。
使用redis 实现分布式锁
1.redis单节点的分布式锁
如下图,一个服务部署了2个客户端,获取分布式锁时一个成功,另一个就失败了。

redis一般使用setnx实现分布式锁,命令如下: SETNX KEY_NAME VALUE 设置成功返回 1,设置失败返回 0。
使用单节点分布式锁存在一些问题。
1.1 客户端1获取锁之后发生了故障 结果锁就不能释放了,其他客户端永远获取不到锁。解决方法是用下面命令对key设置过期时间:SET key value [EX seconds] [PX milliseconds] NX
1.2 客户端2误删了锁 解决方法是对key设置value时加入一个客户端表示,比如在客户端1设置key时在value前拼接一个字符串application1,删除的时候做一下判断。
2 redis 红锁
redis单节点会有可靠性问题,节点故障后锁操作就会失败。redis为了应对单点故障的问题,设计了多节点的分布式锁,也叫红锁。主要思想是客户端跟多个redis实例请求加锁,只有超过半数的实例加锁成功,才认为成功获取了分布式锁。
如下图,客户端分别跟3个实例请求加锁,有2个实例加锁成功,所以获取分布式锁成功:

数据倾斜
什么是数据倾斜?看下面这个面试题:如果redis有一个热点key,qps能达到100w,该如何存储?
如果这个热点key被放到一个redis实例上,这个实例面临的访问压力会非常大。如下图,redis3这个实例保存了foo这个热点key,访问压力会很大:

「解决方法主要有两个:」
1.使用客户端本地缓存来缓存key,这样改造会有两个问题:
- 客户端缓存的热点
key可能消耗大量内存 - 客户端需要保证本地缓存和
redis缓存的一致性
2.给热点key加一个随机前缀,让它保存到不同的redis实例上,这样也会存在两个问题:
- 客户端在访问的时候需要给这个
key加前缀 - 客户端在删除的时候需要根据所有前缀来删除不同实例上保存的这个
key
bigmap使用
有一道经典的面试题,10亿整数怎么在内存中去重排序?
我们先算一下10亿整数占的内存,java一个整数类型占四字节,占用内存大小约: 10亿 * 4 / 1024 / 1024 = 3.7G 占得内存太大了,如果内存不够,怎么办呢?
1.bigmap介绍: bitmap类型使用的数据结构是String,底层存储格式是二进制的bit数组。假如我们有1、4、6、9四个数,保存在bit数组中如下图:

2. 使用场景
2.1 员工打卡记录
在一个有100个员工的公司,要统计一个月内员工全勤的人数,可以每天创建一个bitmap,签到的员工bit位置为1。
要统计当天签到的员工只要用BITCOUNT命令就可以。
要统计当月全勤的员工,只要对当月每天的bitmap做交集运算就可以,命令如下:BITOP AND srckey1 srckey2 srckey3 ... srckey30 (srckeyN表示第N天的打卡记录bitmap)
2.2 统计网站日活跃用户
比如网站有10万个用户,这样我们创建一个长度为10万的bitmap,每个用户id占一个位,如果用户登录,就把bit位置为1,日终的时候用BITCOUNT命令统计出当天登录过的用户总数。


浙公网安备 33010602011771号