1. redis支持的数据类型有哪些
- String(字符串)
- 格式:set key value
- 一个键最大能存储512MB
- 是二进制安全的,意思是string可以包含任何数据,比如jpg图片或者序列化的对象
- Hash(哈希)
- 格式:hmset name key1 value1 key2 value2
- 是一个键值(key=>value)对集合
- 是一个string类型的field和value的映射表,特别适合用于存储对象
- List(列表)
- 格式:lpush name value
- 列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边)
- Set(集合)
- 格式:sadd name value
- 是string类型的无序集合,集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)
- zset(sorted set:有序集合)
- 格式:zadd name score value
- 和 set 一样也是string类型元素的集合,且不允许重复的成员
- 不同的是每个元素都会关联一个double类型的分数,redis正是通过分数来为集合中的成员进行从小到大的排序
- zset的成员是唯一的,但分数(score)却可以重复
2. Redis 过期策略有哪些
- 惰性删除:查询某个 key 的时候,redis 会先检查一下,查看这个key是否设置了过期时间,如果设置了过期时间,那判断是否过期,过期了就删除
- 定期删除:redis 默认是每隔 100 ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期了就删除
- 主动删除:如果定期删除漏了很多过期的key,也没有频繁的查询key,过期的key堆积多了导致内存不够,这时候会走内存淘汰机制
3. Redis 内存淘汰机制有哪些
redis在内存空间不足的时候,为了保证命中率,就会选择一定的数据淘汰策略;我们可以在 redis.config 中设置最大的内存参数,默认是关闭的。redis5.0为我们提供了八个不同的内存置换策略,以前是6种。
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
- volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰
- allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
- allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失
这八种大体上可以分为4中,lru、lfu、random、ttl。
4. 手写一下LRU代码实现
public class LRUCache<Object> {
/**
* 默认缓存大小
*/
private int CAPACITY = 0;
private LinkedList<Object> list;
public LRUCache(int capacity){
this.CAPACITY = capacity;
list = new LinkedList<Object>();
}
public synchronized void put(Object object){
if (list != null && list.contains(object)){
list.remove(object);
}
removeLeastVisitElement();
list.addFirst(object);
}
/**
* 移除最近访问次数最少的元素
*/
private synchronized void removeLeastVisitElement() {
int size = size();
//注意,这儿必须得是CAPACITY - 1否则所获的size比原来大1
if(size > (CAPACITY - 1) ) {
Object object = list.removeLast();
System.out.println("本次被踢掉的元素是:" + object.toString());
}
}
public int size() {
if(list == null) {
return 0;
}
return list.size();
}
public synchronized Object get(int index) {
return list.get(index);
}
public synchronized void clear() {
list.clear();
}
@Override
public String toString() {
return list.toString();
}
}
// 测试
LRUCache<String> lruCache = new LRUCache<>(3);
lruCache.put("a");
System.out.println(lruCache.toString());
lruCache.put("b");
System.out.println(lruCache.toString());
lruCache.put("c");
System.out.println(lruCache.toString());
lruCache.put("d");
System.out.println(lruCache.toString());
lruCache.put("e");
System.out.println(lruCache.toString());
测试结果:
[a]
[b, a]
[c, b, a]
本次被踢掉的元素是:a
[d, c, b]
本次被踢掉的元素是:b
[e, d, c]
5. Redis 为什么那么快
- redis 使用内存进行存储,没有磁盘IO上的开销
- redis 使用单线程处理请求,避免多个线程之间上下文切换和争抢锁资源的开销
- redis 使用多路IO复用技术 epoll
- Redis有诸多可以直接应用的优化数据结构的实现,应用层可以直接使用原生的数据结构提升性能
6. 说一说 Redis 持久化机制
RDB机制
RDB持久化是指在指定的时间间隔内将内存中的数据集以快照的方式写入磁盘二进制文件中,默认的文件名为dump.rdb。
RDB触发机制:
-
save触发方式:该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。
-
bgsave触发方式:执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求,原理是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令。
-
自动触发:由我们的配置文件来完成的。在redis.conf配置文件中,里面有如下配置
save m n # m表示时间秒 n表示更新的key个数 save 900 1 # 表示900秒内如果至少有1个key的值变化,则保存
RDB机制的优点:
- RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复
- 生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作
- RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快
RDB机制的缺点:
RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据。
AOF机制
redis会将每一个收到的写命令都通过write函数追加到AOF文件中,就跟记录日志一样。
AOF重写原理:
AOF的方式也同时带来了另一个问题。持久化文件会变的越来越大。为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写。重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。
AOF也有三种触发机制
- 每修改同步always:同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好
- 每秒同步everysec:异步操作,每秒记录 如果一秒内宕机,有数据丢失
- 不同no:从不同步
AOF机制的优点:
(1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。
(2)AOF日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损。
(3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。
(4)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据
AOF机制的缺点:
(1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大
(2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的
(3)以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。
RDB和AOF到底该如何选择
两者结合,具体看项目业务需求
7. 说一说缓存雪崩、缓存穿透、缓存击穿
缓存穿透
是指查询一个一定不存在的数据,从数据库查不到的数据不会写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决方案
- 最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
- 如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存(或者缓存一个特殊的结果),并设置过期时间,这个过期时间会很短,最长不超过五分钟。
缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决方案
- 我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
缓存击穿
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案
-
使用互斥锁(mutex key):在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
public String get(key) { String value = redis.get(key); if (value == null) { //代表缓存值过期 //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功 value = db.get(key); redis.set(key, value, expire_secs); redis.del(key_mutex); } else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可 sleep(50); get(key); //重试 } } else { return value; } }
-
提前使用互斥锁(mutex key):在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中
-
热点数据不设置超时时间
8. 简述 Redis 事务实现
Redis事务通常会使用MULTI,EXEC,WATCH等命令来完成。redis的事务不支持回滚,事务执行时会阻塞其它客户端的请求执行。
MULTI:执行该命令的客户端从非事务状态切换成事务状态。这一切换是通过在客户端状态的flags属性中打开REDIS_MULTI标识完成
redis > MULTI
OK
打开事务标识的客户端里,用户输入的命令都会被暂存到一个命令队列里,不会因为用户会的输入而立即执行
redis > SET "username" "jack"
redis > SET "password" 123
redis > GET "username"
EXEC:执行事务队列里的命令
redis > EXEC
注意:在客户端打开了事务标识后,只有命令:EXEC,DISCARD,WATCH,MULTI命令会被立即执行,其它命令服务器不会立即执
WATCH:是一个乐观锁,它可以在EXEC命令执行之前,监视任意数量的数据库键,并在执行EXEC命令时判断是否至少有一个被watch的键值被修改如果被修改就放弃事务的执行,如果没有被修改就清空watch的信息,执行事务列表里的命令
UNWATCH:取消对一个键值的"监听"
DISCARD:清空客户端的事务队列里的所有命令,并取消客户端的事务标记,如果客户端在执行事务的时候watch了一些键,则discard会取消所有键的watch。
9. Redis集群
主从模式
- 主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库
- 从数据库一般都是只读的,并且接收主数据库同步过来的数据
- 一个master可以拥有多个slave,但是一个slave只能对应一个master
- slave挂了不影响其他slave的读和master的读和写,重新启动后会将数据从master同步过来
- master挂了以后,不影响slave的读,但redis不再提供写服务,master重启后redis将重新对外提供写服务
- master挂了以后,不会在slave节点中重新选一个master
工作原理:
当slave启动后,主动向master发送SYNC命令。master接收到SYNC命令后在后台保存快照(RDB持久化)和缓存保存快照这段时间的命令,然后将保存的快照文件和缓存的命令发送给slave。slave接收到快照文件和命令后加载快照文件和缓存的执行命令。复制初始化后,master每次接收到的写命令都会同步发送给slave,保证主从数据一致性。
缺点:master节点在主从模式中唯一,若master挂掉,则redis无法对外提供写服务。
Sentinel(哨兵)模式
-
sentinel模式是建立在主从模式的基础上,如果只有一个Redis节点,sentinel就没有任何意义
-
当master挂了以后,sentinel会在slave中选择一个做为master,并修改它们的配置文件,其他slave的配置文件也会被修改,比如slaveof属性会指向新的master
-
当master重新启动后,它将不再是master而是做为slave接收新的master的同步数据
-
sentinel因为也是一个进程有挂掉的可能,所以sentinel也会启动多个形成一个sentinel集群
-
多sentinel配置的时候,sentinel之间也会自动监控
-
当主从模式配置密码时,sentinel也会同步将配置信息修改到配置文件中,不需要担心
-
一个sentinel或sentinel集群可以管理多个主从Redis,多个sentinel也可以监控同一个redis
-
sentinel最好不要和Redis部署在同一台机器,不然Redis的服务器挂了以后,sentinel也挂了
工作原理:
- 每个sentinel以每秒钟一次的频率向它所知的master,slave以及其他sentinel实例发送一个 PING 命令
- 如果一个实例距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被sentinel标记为主观下线。
- 如果一个master被标记为主观下线,则正在监视这个master的所有sentinel要以每秒一次的频率确认master的确进入了主观下线状态
- 当有足够数量的sentinel(大于等于配置文件指定的值)在指定的时间范围内确认master的确进入了主观下线状态, 则master会被标记为客观下线
- 在一般情况下, 每个sentinel会以每 10 秒一次的频率向它已知的所有master,slave发送 INFO 命令
- 当master被sentinel标记为客观下线时,sentinel向下线的master的所有slave发送 INFO 命令的频率会从 10 秒一次改为 1 秒一次
- 若没有足够数量的sentinel同意master已经下线,master的客观下线状态就会被移除;
- 若master重新向sentinel的 PING 命令返回有效回复,master的主观下线状态就会被移除
注意:当使用sentinel模式的时候,客户端就不要直接连接Redis,而是连接sentinel的ip和port,由sentinel来提供具体的可提供服务的Redis实现,这样当master节点挂掉以后,sentinel就会感知并将新的master节点提供给使用者。
slave的选举主要会评估slave的以下几个方面:
- 与master断开连接的次数
- Slave的优先级
- 数据复制的下标(用来评估slave当前拥有多少master的数据)
- 进程ID
Cluster模式
sentinel模式基本可以满足一般生产的需求,具备高可用性。但是当数据量过大到一台服务器存放不下的情况时,主从模式或sentinel模式就不能满足需求了,这个时候需要对存储的数据进行分片,将数据存储到多个Redis实例中。cluster模式的出现就是为了解决单机Redis容量有限的问题,将Redis的数据根据一定的规则分配到多台机器。
cluster可以说是sentinel和主从模式的结合体,通过cluster可以实现主从和master重选功能,所以如果配置两个副本三个分片的话,就需要六个Redis实例。因为Redis的数据是根据一定规则分配到cluster的不同机器的,当数据量过大时,可以新增机器进行扩容。
使用集群,只需要将redis配置文件中的cluster-enable配置打开即可。每个集群中至少需要三个主数据库才能正常运行,新增节点非常方便。
-
多个redis节点网络互联,数据共享
-
所有的节点都是一主一从(也可以是一主多从),其中从不提供服务,仅作为备用
-
不支持同时处理多个key(如MSET/MGET),因为redis需要把key均匀分布在各个节点上,
并发量很高的情况下同时创建key-value会降低性能并导致不可预测的行为 -
支持在线增加、删除节点
-
客户端可以连接任何一个主节点进行读写
10. Redis 主从复制的核心原理
当slave启动后,主动向master发送SYNC命令。master接收到SYNC命令后在后台保存快照(RDB持久化)和缓存保存快照这段时间的命令,然后将保存的快照文件和缓存的命令发送给slave。slave接收到快照文件和命令后加载快照文件和缓存的执行命令。复制初始化后,master每次接收到的写命令都会同步发送给slave,保证主从数据一致性。
11. 说说 Redis 哈希槽的概念
Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群有16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽;