基础
1. 为什么要使用redis做缓存:高性能和高并发:
- redis主要作为缓存层
- 高性能:因为数据库的数据是存放在磁盘的,如果不使用redis,每次访问数据都要从磁盘中读取,而磁盘的读写速度又慢,那么整个系统的反映速度就变慢。但是使用redis作为缓存层存储数据的话,redis会将数据存放在内存中,内存的读写速度比磁盘多得多。从而提高了系统的性能。
- 高并发:redis能承受的并发远大于后端数据库,官方给出的数据是每秒10w的qps(每秒查询量)。使用redis作为缓存层,不仅能够大大减轻后端数据库的并发压力,从而使整个系统更稳定
2. Redis和Memcache
- 两者都是NO-SQL
- 数据类型
- Memcached仅支持字符串类型,
- Redis支持五种不同的数据类型
- 数据持久化
- Redis有RDB和AOF
- Memcache无法持久化
- 分布式
- Redis集群实现了分布式
- Memcache只能在单机
数据结构
1. redis常用数据结构
- String:KV形式,键值都是字符串。值最大512MB。
- 指令:
- 设置key:set key value [ex seconds] [millseconds] [nx|xx]
- 获取key:get key
- 自增:incr key。不是整数返回错误,不存在则从0自增
- 自减:decr key
- 应用
- 缓存
- 计数器:预减库存,接口限流防刷
- 分布式session:分布式系统可能会将用户的登录状态负载均衡分发到不同的服务器上,用户状态丢失。可以使用redis缓存实现分布式session,将user和token分别存入cookie和redis,访问页面时,通过web拦截器先在cookies和页面的attribute中查找当前user的token,然后再查找redis中是否存在该token,如果都存在,那么表明用户登录状态未改变
- Hash:key field-value
- 指令:
- 设置:hset key field value
- 获取:hgey key field
- 应用
- 缓存用户信息,每一个用户属性用一个field-value
- List:存储多个有序的字符串,最多Integer.MAX_VALUE个。可以两端插入和弹出。
- 指令
- 左右插入:lpush/rpush key value1 value2
- 左右弹出:lpop/rpop key
- 获取:lrange key start end
- 左右阻塞:blpop/brpop key
- 应用
- 栈:rpush/rpop
- 消息队列:lpush + brpop。左侧插入,右侧阻塞获取
- Set:存储多个不重复的字符串,最多Integer.MAX_VALUE,支持并集、交集、差集
- 指令
- 添加:sadd key element1 element2
- 删除:srem key element1 element2
- 应用
- 存标签
- Zset:存储多个不重复的排序字符串,为元素分配了一个分数
- 指令
- 添加:zadd key score member [score member]
- 排名:zrank/zrevrank key member
- 删除:zrem key member
- 排行榜:zrange key start end [withscores]
- 应用
- 排行榜
2. Zset底层是什么。为什么不用红黑树
- Zset底层是ziplist和skiplist。一般都是使用skiplist
- 跳表在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。下图演示了查找 22 的过程。
[图片上传失败...(image-f731fa-1709175007295)]
- 过程:
- 第一次:搜L3,没有,往下一层
- 第二次:搜L2,0-7,没有;7-25,有。往下一层
- 第三次:搜L1,7-13,没有;13-15,没有;15-25,有,往下一层
- 第四次:搜BL,15-22,找到目标值
- 既然两个数据结构时间复杂度都是O(logN),zset为什么不用红黑树
- 跳表实现简单,且红黑树每次插入都要通过旋转以维持平衡,实现复杂
持久化(redis高可用)
1. Redis为什么要持久化
- 故障恢复。因为redis是将数据存放在内存的,内存一旦断电,数据丢失.因此需要开启持久化将数据保存到磁盘
2. 持久化有几种实现方式(重点)
- redis持久化有两种方式,rdb和aof
- rdb:是redis默认的持久化方式,当触发持久化时,fork一个子线程来进行持久化。保存到一个二进制文件dump.rdb
- save second keys。在seconds秒内有keys个key被修改,保存这段时间内的操作
- 缺点:数据完整性不高。如果这段时间内,key改动数未达到保存快照的条件,但是却发生了断电,那么这段时间的操作会丢失
- 优点:rdb恢复数据快
- aof:三种方式:always,everysec,no。每次,每秒,不
- 优点:aof可以最大程度保证数据不丢失
- 缺点:aof文件大,恢复速度慢
- 如果aof文件太大,可以进行重写aof文件,合并重复的命令,减少内存的消耗,提高了恢复数据的速度。
- 使用rdb做备份,进行快速的数据恢复。aof保证数据不丢失。
集群(redis高可用和高并发——主从架构、集群)
1. redis集群有哪些方式:主从复制、哨兵模式、集群模式(重点)
- 主从复制:读写分离,减轻服务器压力,一主二从
- 主从复制就是常说的master/slave模式,主服务器负责读写,从服务器负责读,因为一般来说读多写少,主从服务器之间数据同步
- 配置主从复制(可修改配置文件或直接使用命令行)
- 配置文件配置:从服务器节点的配置文件,将slaveof的port端口改成主服务器的port,进入主服务器,使用info replication命令查看是否配置成功
- 命令行配置:从服务器配置slaveof of 主服务器port
- 主从负责有什么缺点:如果主机宕机了,整个集群就没有可写的节点。
- 哨兵模式:为了解决主从复制的缺点,当主机宕机之后,选取一个从机作为主机,主机重连之后成为从机
- 配置哨兵模式:配置sentinel.conf。sentinel monitor 主机名 主机ip 主机port 触发切换的哨兵数量
- 哨兵模式有三个主要任务
- 监视:监视redis集群的主从服务器
- 提醒:当服务器出现问题时,报告
- 自动故障迁移:当主机出现问题后,哨兵之后会进行投票,选举一个从服务器作为主机,哨兵网络至少有1个哨兵存活就能实现故障迁移,一个哨兵会随机选取
- sentinel特点
- 原来的主机重连只能作为从机
- sentinel集群最好是独立的一台服务器,防止redis的服务器挂了导致哨兵集群也挂了
- sentinel最好设置多个,哨兵之间也会相互监视
- cluster集群:多个主从复制+哨兵模式组合而成。将数据同步到多个redis集群。
- 配置cluster。3台服务器每台两个redis,一主一从。一共6个redis。然后通过cluster create xxxx... --cluster-replicas 1创建集群
- cluster特点:
- 多个redis网络数据共享
- 所有节点都是主从模式
- 支持在线增删节点
- redis集群将数据分片存入hash槽(hash slot)
事务
1. Redis能够实现事务吗,如何实现,事务能否保证原子性,隔离性呢(重点)
- redis的事务是所有命令依次执行,其他服务器提交的命令不会插入到当前事务
- multi开启事务、exec执行事务,discard取消事务
- redis事务无法保证原子性,单条命令是原子性性,事务无法保证原子性,因为redis事务不会回滚。当事务中命令出现错误时,不会影响其他命令执行。
- redis事务没有隔离级别的概念,multi开启事务之后,执行exec之前,命令会先入队列,但是不会立即执行,也就不存在事务内的查询会看到其他事务的更新,外部事务的查询也无法查询到事务内的更新
其他
1. redis是单线程的吗,线程安全的吗,为什么单线程还这么快(重点)
- redis是单线程的,是线程安全
- 单线程还这么快主要有4个原因
- 最主要的是redis是基于内存,数据存放在内存中,读写速度快,影响它的只有网络和内存
- 单线程有单线程的好,单线程不会发生多线程的竞态问题
- 非阻塞的io多路复用模型
2. io多路复用模型
- io多路复用,多路指的是多个网络连接,复用指的是复用同一个线程处理连接。io多路复用模型可以让单个线程高效处理多个网络连接请求,减少了网络连接的时间
- redis采用的是reactor方式:
- 处理器叫文件事件处理器,这个处理器是单线程的,但是通过io多路复用机制监听多个socket连接。
- 文件时间处理器分为四个部分:多个socket、io多路复用、文件事件分发器、事件处理器
- 如果是客户端连接redis,关联到连接应答处理器
- 如果是客户端要写数据到redis,关联到命令请求处理器
- 如果是客户端要从redis读数据,关联到命令回复处理器
- redis的reactor方式:https://upload-images.jianshu.io/upload_images/8494967-c8f6145377b56cc0.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp
- 客户端对redis发起一次建立连接的请求流程
- 客户端连接到redis的server socket请求建立连接,发起连接时产生一个事件,写入到io多路复用程序中。
- io多路复用程序将事件写入到队列中,文件事件分发器从队列中获取事件进行处理
- 连接应答处理器处理该事件,建立连接
3. 热key处理:某个key被大量访问,对redis服务器造成了很大的压力。
- 使用服务器缓存,将热点数据key缓存到服务器的内存,利用hashmap,访问时先判断是否是热点key,如果是,直接从hashmap中读取,如果不是,走redis,注意要保持缓存一致性
- 怎样知道是热key:
- 客户端统计
- redis自统计
4. Redis过期策略
- 定期删除:redis默认每100ms随机抽取一些设置了过期时间的key进行检查,如果key已经过期,删除。
- 为什么是选择一部分:防止大量检查CPU负载高
- 出现问题:大量过期的key未被删除
- 惰性删除:当获取key时才检测,过期删除,未过期,返回。
5. Redis的key淘汰策略
- noeviction,内存达到最大值,直接返回错误。
- allkeys-lru:所有键里最近最少使用;
- allkeys-random:所有键随机回收;
- volatile-lru:设置过期时间的键中,回收最近最少使用的,
- volatile-random:设置过期集合键中,随机回收。
- volatile-ttl:设置过期时间的键中,回收存活时间较短的键。
5. 设置key存活时间和失效时间
- 设置:expire key second。
- 查看(-2,不存在。-1,永久。大于0,剩余时间):ttl key。
- 清除:persist key
6. redis如何实现分布式锁,setNX做了什么(底层通过lua脚本加锁,可以实现原子性)(重点)
- setNX指令:setnx key value
redis> EXISTS job # job 不存在
(integer) 0
redis> SETNX job "programmer" # job 设置成功
(integer) 1
redis> SETNX job "code-farmer" # 尝试覆盖 job ,失败
(integer) 0
redis> GET job # 没有被覆盖
"programmer"
- setNX的意思是:如果给定的key已经存在,则什么都不做,只有当key不存在时,才会设置。
8. 缓存击穿、缓存穿透、缓存雪崩(重点)
- 缓存击穿:针对热key,缓存中没有,数据库中有。热key在高并发下突然过期,导致大量的请求打到数据库
- 互斥锁:如果一个请求从缓存中获取的数据为null,先锁住该请求,然后去数据库中查找,找到之后先存入缓存,然后再释放锁
- 缓存穿透:缓存和数据库中没有该数据,每次请求都会打到数据库,比如恶意请求
- 使用布隆过滤器,先将数据库中数据预存到布隆过滤器中,当请求的从缓存中获取的数据为null时,先查询布隆过滤器中是否有该数据,
- 如果布隆过滤器中有,因为布隆过滤器的自身特性,也不能保证数据库中一定有该数据,所以先查询数据库,如果数据库中确实有该数据,将数据存入缓存,然后返回value
- 如果布隆过滤器中没有,则数据库中必定没有该数据,直接返回null
- 缓存雪崩:缓存中大量的key同时失效,导致大量的请求打到数据库
- 将key的存活时间设置为random,减少缓存雪崩的几率
9. 布隆过滤器
- 布隆过滤器底层使用bit数组,使用多个哈希函数计算数据的哈希值,存入bit数组,如果一个数据查询时结果集中有0,说明该数据必定不存在。如果一个数据查询的结果集全为1,也不能保证一定有。
- 布隆过滤器解决黑名单URL:如果一个黑名单网站包含100亿个黑名单网页,每个网页最多占64B,设计一个系统,判断当前的URL是否在这个黑名单当中,要求额外空间不超过30GB,允许误差率为万分之一。
- 假设一个网页黑名单有URL为100亿,每个样本为64B,失误率为0.01%,经过布隆过滤器的公式计算后,需要布隆过滤器大小为25GB,这远远小于使用哈希表的640GB的空间。并且由于是通过hash进行查找的,所以基本都可以在O(1)的时间完成!
10. 布隆过滤器案例
- 导入依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
- 布隆过滤器
public class Test {
private static int size = 1000000;//预计要插入多少数据
private static double fpp = 0.01;//期望的误判率
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);
public static void main(String[] args) {
//插入数据
for (int i = 0; i < 1000000; i++) {
bloomFilter.put(i);
}
int count = 0;
for (int i = 1000000; i < 2000000; i++) {
if (bloomFilter.mightContain(i)) {
count++;
System.out.println(i + "误判了");
}
}
System.out.println("总共的误判数:" + count);
}
}
- 拦截
public String get(String key) {
String value = redis.get(key);
if (value == null) {
// redis中不存在该缓存
if(!bloomfilter.mightContain(key)){
//布隆过滤器也没有,直接返回
return null;
}else{
//布隆过滤器中能查到,不代表一定有,再查数据库,查出来放入redis,同样也可以避免缓存穿透
value = db.get(key);
redis.set(key, value);
}
}
return value;
}