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
命令统计出当天登录过的用户总数。