面试后端开发的职位,相信大家经常被问到有关redis问题。Redis作为缓存系统的代表很有必要弄熟搞懂,无论是在工作当中还是求职面试过程中都是大有裨益的,本文将详细介绍一些redis的一些典型问题,并给出了一些参考解答。
由于作者水平有限,可能会有存在一些问题,欢迎大家不吝批评指教。文中参考了网友的一些资料,在这里先他们表示感谢。本文全文约4000字,阅读完大概需要10分钟时间。
常见问题
Redis性能为什么高?
单线程的redis如何利用多核cpu机器?
Redis的缓存淘汰策略?
Redis如何持久化数据?
Redis有哪几种数据结构?
Redis集群有哪几种形式?
有海量key和value都比较小的数据,在redis中如何存储才更省内存?
如何保证redis和DB中的数据一致性?
如何解决缓存穿透和缓存雪崩?
如何用redis实现分布式锁?
问题参考列表
Redis性能为什么高?
Redis是key-value存储的nosql数据库,具有以下一些性质,使其性能优异。
完全基于内存操作,绝大多数操作都在内存中完成,非常高效。
内部采用多路I/O复用模型,非阻塞式IO。Redis会根据系统情况优先调用高效的IO复用模型,例如linux的epoll多路IO函数;
数据结构和数据操作简单,redis中的数据结构是专门进行设计的;
处理请求模块采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
关于底层模型为了更有效的通讯,redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
单线程的redis如何利用多核cpu机器?
Redis的数据读取和处理性能非常强大,对于一般服务器配置,cpu都不会是性能瓶颈。Redis的性能瓶颈主要集中在内存和网络方面。举例来说,运行在一台普通的Linux机器上至少能处理50万并发请求。所以使用的redis命令多为O(N)、O(log(N))时间复杂度,那么基本上不会出现cpu瓶颈的情况。
但是如果你确实需要充分使用多核cpu的能力,那么需要在单台服务器上运行多个redis实例(主从部署/集群化部署),并将每个redis实例和cpu内核进行绑定来实现充分利用多核CPU。
Redis的缓存淘汰策略?
Redis缓存淘汰策略与Redis键的过期删除策略并不完全相同,前者是在Redis内存使用超过一定值的时候(一般这个值可以配置)使用的淘汰策略;而后者是通过定期删除+惰性删除两者结合的方式进行内存淘汰的。
有六种缓存淘汰策略:
volatile-lru:从已设置过期时间的数据中挑选最近最少使用的数据淘汰;
volatile-ttl:从已设置过期时间的数据中挑选将要过期的数据淘汰;
volatile-random:从已设置过期时间的数据中任意选择数据淘汰;
allkeys-lru:从数据集中挑选最近最少使用的数据淘汰;
allkeys-random:从数据集中任意选择数据淘汰;
no-enviction(驱逐):禁止驱逐数据
注意这里的6种机制,volatile和allkeys都会对已设置过期时间的数据集淘汰数据。allkeys会从全部数据集淘汰数据,后面的lru、ttl以及random是三种不同的淘汰策略,再加上一种no-enviction永不回收的策略。
Redis如何持久化数据?
Redis支持两种数据持久化方式:RDB方式和AOF方式。前者会根据配置的规则定时将内存中的数据持久化到硬盘上,后者则是在每次执行写命令之后将命令记录下来。两种持久化方式可以单独使用,但是通常会将两者结合使用。
1、RDB方式
RDB方式的持久化是通过快照的方式完成的。当符合某种规则时,会将内存中的数据全量生成一份副本存储到硬盘上,这个过程称作”快照”。举例来说,根据配置规则进行自动快照流程:
用户执行SAVE, BGSAVE命令;
执行FLUSHALL命令;
执行复制(replication)。
2、AOF方式
在使用Redis存储非临时数据时,一般都需要打开AOF持久化来降低进程终止导致的数据丢失,AOF可以将Redis执行的每一条写命令追加到硬盘文件中,这一过程显然会降低Redis的性能,但是大部分情况下这个影响是可以接受的,另外,使用较快的硬盘能提高AOF的性能。
Redis有哪几种数据结构?
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
Redis集群有哪几种形式?
redis有三种集群方式:主从复制,哨兵模式和集群。 详细内容参见:Redis集群管理方式
有海量key和value都比较小的数据,在redis中如何存储才更省内存?
可以考虑通过大幅减少key的数量来降低内存的消耗。
实现:在客户端通过分组将海量的key根据一定的策略映射到一组hash对象中,由于value较小,故hash类型的对象会使用占用内存较小的ziplist编码。
例如:如存在100万个键,可以映射到1000个hash中,每个hash保存1000个元素。
如何保证redis和DB中的数据一致性?
大多情况下,缓存策略是:读缓存,读取不到就读数据库然后同步到缓存中。
问题出现场景
在并发访问中,不论是先写库,再删除缓存;还是先删缓存,再写库,都有可能出现数据不一致的情况
1、在并发中是无法保证读写的先后顺序的,如果删掉了缓存还没来得及写库,另一个线程就过来读取发现缓存为空就去数据库读取并写入缓存,此时缓存中为脏数据。
2、如果先写了库,再删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
3、如果是redis集群,或者主从模式,写主读从,由于redis复制存在一定的时间延迟,也有可能导致数据不一致。
双删 + 超时
在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。这样最差的情况是在超时时间内存在不一致,当然这种情况极其少见,可能的原因就是服务宕机。此种情况可以满足绝大多数需求。
当然这种策略要考虑redis和数据库主从同步的耗时,所以在第二次删除前最好休眠一定时间,比如500毫秒,这样毫无疑问又增加了写请求的耗时。
如何解决缓存穿透和缓存雪崩?
缓存雪崩:是指缓存同一时间大面积的失效,瞬间请求全落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。
缓存穿透: 是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案
接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap拦截掉,从而避免了对底层存储系统的查询压力
如何用redis实现分布式锁?
在分布式环境下多个不同线程对共享资源进行访问,传统的锁比如Java的锁机制就无法实现了,这个时候就必须借助分布式锁来解决分布式环境下共享资源的同步问题。
为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
互斥性。在任意时刻,只有一个客户端能持有锁
不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。[1]
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final Long RELEASE_SUCCESS = 1L;
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
参考文献
[1]: Redis分布式锁的正确实现方式
欢迎大家关注我的CSDN博客,您的行动就是对我最大的鼓励!