Q1:Redis 单线程为什么这么快?
- 纯内存操作
- 核心是基于非阻塞的 io 多路复用机制
- 单线程反而避免了多线程频繁切换上下文带来的性能问题
Q2:分布式锁的底层是怎么实现的?
- setnx+setex:存在设置超时时间失败的情况,导致死锁
- set(key,value,nx,ex):将setnx + setex 编程原子操作
- 问题:
- 任务超时,锁自动释放导致并发问题。(使用redisson 解决,看门狗-后台线程定时检查 是否需要 自动续期)
- 加锁和释放锁不是同一个线程(释放的不是自己线程的锁):在value中存入uuid,删除锁的时候判断该标标识(使用lua脚本保证原子操作)
- 不可重入,使用redisson解决(实现机制类似于AQS,计数)
- 异步复制可能造成锁丢失,使用redLock 解决
- 顺序向五个节点请求加锁
- 根据一定的超时时间推断是不是跳过该节点
- 三个节点加锁成功并且花费时间小于锁的有效期
- 认定加锁成功
补充:
- 分布式锁还可以使用mysql的主键机制、排他锁机制、乐观锁机制实现
可以使用zookeeper来实现
惰性过期:只有当访问一个key 时,才会判断key是否已过期,过期则清除。该策略可以最大化的节省cpu资源,却对内存非常不友好。极端情况可能出现大量的过期对key没有再次被访问,从而不会清除,占用大量对内存。
- 定时过期:每个一定时间,会扫描expires字典中一定数量对key,并清除其中已过期的key。该策略时前两者的一种这种方案,通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得cpu和内存资源达到最优的平衡效果。
- expires字典保存了所有设置了过期时间的key 的过期时间数据。其中,key是指向键空间中的某个键的指针,value 是该键的 毫秒精读的 UNIX 时间戳表示的过期时间,键空间是redis集群中的所有键。
- 随机抽取20个key检测是否需要删除,如果超过25%的key过期,则立即进行下一次检查。
Q4:Redis 的持久化机制?
- RDB(Redis DataBase) 将某一个时刻的内存快照以二进制的形式写入磁盘。
- 手动触发:
- save命令:使redis出于阻塞状态,直到rdb完成才会响应其他客户端的操作。(生产环境慎用)
- bgsave命令:fork出一个子进程来进行持久化,主进程只在fork过程中有短暂的阻塞,子进程创建之后,主进程就可以响应客户端请求了。
- 自动触发:
- save m n: 在 m秒内,有n个键发生改变,则自动触发持久化,通过bgsave执行,只要满足其一就会触发,配置文件中有默认配置。
- flushall:用于清空Redis数据库,flushdb清空当前的redis所在的数据库(默认是0号数据库),会清空rdb文件,同时生成dump.rdb 内容为空。
- 主从同步:全量的主从同步时 会触发bgsave命令,生成 rdb 发送给从节点。
- 优点:
- 整个redis 数据库只包含一个dump.rdb,方便持久化
- 容灾性好,方便备份
- 性能最大化,fork子进程用来完成写操作,让主进程继续处理命令,所以是IO最大化。使用单独的子进程来进行持久化,主进程不会进行任何的IO操作,保证了redis 的高性能。
- 缺点:
- 数据的安全性要求比较低。RDB是隔一段时间进行一次持久化的,如果期间redis发生故障,会造成数据丢失,所以这种方式适合数据要求不严谨的时候。
- 由于RDB 是通过fork 子进程进行持久化工作的,因此 当数据比较大时,可能会导致整个服务暂停几百毫秒甚至1秒钟(会占用cpu)
- 手动触发:
- AOF (Apeng Only File) 以日志的形式记录服务器所处理的每一个写、删操作,查询的操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录,调用操作系统命令进行刷盘
- 所有的写命令会追加到AOF 缓冲中
- AOF 缓冲区根据对应的策略向硬盘进行同步操作
- 随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的
- 当Redis 重启时,可以加载AOF文件进行数据恢复
同步策略:
- 每秒同步:异步完成,效率非常高,一旦系统出现宕机现象,那么这一秒之内的修改记录会丢失
- 每修改同步:同步持久化,每次发生的数据变化都会被立即记录到磁盘中,最多丢失一条
- 不同步:由操作系统控制,可能丢失比较多的数据
优点:
- 数据安全
- 通过append 模式写文件,及时中途服务器当即也不会破坏已经存在的内容,可以通过redis-check-aof工具解决数据一致性的问题
- AOF 机制的rewrite 模式,定期对AOF文件进行重写,达到压缩的目的
缺点:
- AOF 文件比RDB文件大,且恢复速度比较慢
- 数据集比较大时,比RDB 启动效率低
- 运行效率没有RDB高
Q5:Redis 集群方案?
主从:
- 哨兵模式(不能很好的解决扩容的问题):
- Sentinel,哨兵是redis集群中非常重要的组件,具备以下功能:
- 集群监控:负责监控redis master 和 slave 进程是否正常工作
- 消息通知:如果某个redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员
- 故障转移:如果master node 挂掉了,会自动转移到slave node 上
- 配置中心:如果故障转移发生了,通知client 客户端新的master 地址
- 基于哨兵实现redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,相互协同工作。
- 故障转移时,判断一个master node 是否宕机了,需要大部分哨兵都同意才行,涉及到了分布式选举
- 及时部分哨兵节点挂掉了,哨兵集群还是可以正常工作的
- 哨兵通常需要3个实例,来保证自己的健壮性
- 哨兵+redis 主从的部署架构,是不保证数据零丢失的,只能保证redis 集群的高可用性
- 对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境都进行充足的测试和演练
- Sentinel,哨兵是redis集群中非常重要的组件,具备以下功能:
- Redis Cluster(多主多从,用的比较多) 是一种服务端Sharding 技术,采用槽的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行
- 方案说明
- 通过哈希的方式,将数据分片,每个节点均分存储一定的哈希槽
- 每份数据分片会存储在多个互为主从的节点上
- 数据写入先写主节点,在同步到从节点
- 同意分片多节点间的数据不保持强一致性
- 读取数据时,当客户端操作的key没有分配在该节点上时,redis 会返回转向指令,指向正确的节点
- 扩容时需要把旧的节点数据迁移一部分到新的节点
- 在redis cluster 架构下,每个redis 要开放两个端口,,比如一个是6379,另一个就是加1w的端口号,比如16379
- 16379 端口号是用来进行节点间通信的,也是cluster bus的通信。用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制协议,gossip协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
- 优点:
- 无中心架构,支持动态扩容,对业务透明
- 具备sentinel 的监控和自动Failover(故障转移能力)
- 客户端不需要链接集群所有节点,连接集群中任何一个可用节点即可
- 高性能,客户端直连redis服务,免去了proxy代理的损耗
- 缺点:
- 运维复杂,数据迁移需要人工干预
- 只能使用0号数据库
- 不支持批量操作(pipeline管道操作)
- 分布式逻辑和存储模块耦合
- 方案说明
- Redis Sharding 是Redis Cluster 之前,普遍使用的Redis 实例集群方法。主要采用哈希算法将Redis 数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。Java redis 客户端驱动redis,支持redis sharding功能,即Shardedjedis 以及结缓存池的shardedjedisPool
- 优点:
- 优势非常简单,服务端的redis实例彼此独立,相互无关联,每个redis实例像单服务一样运行,非常容易线性扩展,系统的灵活性很强
- 缺点:
- sharding在客户端,对运维带来挑战
- 不支持动态增删节点,服务端redis实例变化时,客户端都需要更新调整。连接不能共享。
- 优点:
Q5:Redis 数据结构和应用场景?
- string
- 最简单的数据缓存,也可以缓存某个json格式的字符串、json等
- redis的分布式所功能利用了这种数据结构
- 计数器、全局session等
- hash
- key-value 结构,更适合存储对象
- 购物车等 一对多等场景
- list
- 当作栈
- 当作队列
- 可以用来缓存类似微信公众号、微博等消息流数据
- 双向链表结构
- 关注列表、消息队列等应用
- 常用命令 lpush \lpop \rpush…\ lrange 0 -1 (获取全部数据)
- set
- 和列表类似,但不可重复
- 存储大量数据,并且查询速度快
- 底层其实就是hash,value 为空
- hash结构,没有索引 sadd , smembers ,srem
- 进行交集、并集、差集操作
- sinter key1 [key2] 交集 、 sinterstore destination key1 [key2] 存储到指定集合
- sunion key1 [key2] 并集 、sunionstore destination key1 [key2] (如 sunionstore u3 u1 u2)
- sdiff key1 [key2] 差集 、 sdiffstore destination key1 [key2]
- 共同关注、朋友圈点赞等功能
- 网站访问数据的统计等
- 黑白名单等
- 和列表类似,但不可重复
- zset
- 有序集合,可以用来实现排行榜等功能
- 在set的基础上,增加了 score ,用于保证有序性
- 常用命令:
- zadd key score1 member1 [score2 member2] 添加数据
- zrange key start stop [withscores] 获取全部数据
- (zrange kaoshi 0 -1 不包括score值)
- (zrange kaoshi 0 -1 with scores 包含score 值)
- zrevrange key start stop [withscores] 获取全部数据
- 反相排序
- zrem key member [member…..]
- zrangebyscore key min max [withscores] [limit] 根据score 的范围查询
- zrangebyscore kaoshi 50 100 limit 3
- zrevrangebyscore kaoshi 50 100
- 带有权重的任务/消息队列
- 利用score来记录权重即可
- geo
- 存储地理位置
- 借助sorted set 实现,通过zset的score进行排序可以得到坐标附近的其他元素,通过将score还原成坐标值就可以得到其他元素的原始坐标
- 附近的人等功能
- hyperloglog
- 基数统计,占用空间极小
- 例如网站等访问次数等
- 统计不重复数据
- bitmap(布隆过滤器)
- streams:
- 消息队列
- 订阅和发布
Q6:Redis 主从复制的核心原理?
通过执行slaveof 命令 ,让一个服务去复制另一个服务的数据。主数据库可以进行读写操作,当写操作导致数据变化时,会将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。
全量复制:
- 主节点通过bgsave命令fork子进程进行RDB持久化,该过程非常消耗cpu、内存、磁盘io
- 主节点通过网络将RDB文件发送给从节点,对主从节点对贷款都会带啦很大对损耗
- 从节点清空老数据,载入RDB文件的过程是阻塞的,无法响应客户端的命令
- 如果从节点执行bgrewriteaof,也会带来额外的消耗
部分复制:
- 复制 偏移量:执行复制方法,主从节点分别维护一个复制偏移量 offset (就是记录下从什么地方开始同步)
- 复制 积压缓冲区:主节点内部维护了一个固定长度的,先进先出的队列作为 积压缓冲区,当主从的offset 的差距超过缓冲区的长度时,将无法执行部分复制,只能执行 全量复制
- 服务器运行ID(ruuid):每一个redis节点都有一个ID,运行id由节点在启动时自动生成,主节点会将自己的运行id发送给从节点,从节点会讲主节点的运行id存起来。从节点redis断开重连的时候,根据id来判断同步的进度:
- 如果从节点保存的ruuid 和 主节点的ruuid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(能不能使用部分复制还要看offset 和 复制积压缓冲区的情况)
- 如果从节点保存的 ruuid 与 主节点现在的 ruuid 不同,说明从节点在短线前同步的redis节点并不是当前的主节点,只能进行全量复制
Q7:布隆过滤器原理及优缺点?
原理:
- 添加数据时:
- 将数据进行hash 得到hash值,对应到bit 位,将bit改为1,hash 函数定义多个,则一个数据添加会将多个 bit 改为1,多个hash函数的目的是为了减少hash碰撞的概率
- 查询数据:
- hash函数计算得到hash值,对应到bit中,如果有一个为0,则说明数据不在bit中,如果都为1,则该数据可能在bit中
优点
- 占用内存小
- 增加和查询元素的时间复杂度为O(k),K为哈希函数的个数,与数据量的大小无关
- 哈希函数相互之间没有关系
- 布隆过滤器不需要存储元素本身,保密性高
- 数据量大时,布隆过滤器可以表示全集
- 可以使用布隆过滤器进行交、并、差运算
缺点
- 误判率 因为hash碰撞,不能绝对判断元素在集合中
- 不能获取元素本身
- 一般情况下无法从布隆过滤器中删除元素
Q8:常见的缓存淘汰的算法?
- FIFO
- 先进先出
- LRU
- 最近最少使用(使用hashmap 和 链表配合,保证时间复杂度为O(1)) ```java import java.util.HashMap; import java.util.Map;
/**
- 思路:
- hash 的时间复杂度为o1 ,所以最终使用肯定hash跑不掉
- 最久不实用的 放到 链表的最后面,时间复杂度也是o1
- 类似于手写一个简单的linkedhashmap */
class LRUCache {
/**
* 用来定位节点,hashmap 的时间复杂度为 O(1)
*/
Map<Integer, Node> map;
int capacity;
DoubleLinkedList cache;
public LRUCache(int capacity) {
this.map = new HashMap<>(capacity);
this.capacity = capacity;
// 头尾 两个节点作为哨兵,不使用
cache = new DoubleLinkedList();
}
public int get(int key) {
// 如果不存在返回 -1
if (!map.containsKey(key)) {
return -1;
}
// 如果存在 放到链表的头上去
Node node = map.get(key);
cache.del(node);
cache.addFirst(node);
return node.value;
}
public void put(int key, int value) {
Node node = new Node(key, value);
// 不包含 说明第一次插入这个节点
if (!map.containsKey(key)) {
// 判断是不是满了
if (map.size() == capacity) {
// 满了 删掉最后一个元素
int i = cache.delTail();
map.remove(i);
}
} else {
// 已经包含 说明是在更新
cache.del(map.get(key));
}
// 加入map
map.put(key, node);
// 再加上这个元素
cache.addFirst(node);
}
/**
* 双链表
*/
static class DoubleLinkedList {
// 头节点 最经常使用的
Node head;
// 尾节点 最不经常使用的
Node tail;
public DoubleLinkedList() {
head = new Node();
tail = new Node();
// 头尾相连
head.next = tail;
tail.prev = head;
}
/**
* 添加头节点
* 只需要改变指针即可
*/
void addFirst(Node node) {
Node next = head.next;
next.prev = node;
node.next = next;
head.next = node;
node.prev = head;
}
/**
* 删除某个节点
*/
int del(Node node) {
// 包含元素 则改变指针指向
Node prev = node.prev;
Node next = node.next;
prev.next = next;
next.prev = prev;
// 删除的是哪个元素,map删除时需要
return node.key;
}
/**
* 删掉最后一个元素
*/
public int delTail() {
// 如果没有元素 直接返回-1
if (head.next == tail) {
return -1;
}
return del(tail.prev);
}
}
/**
* 节点
*/
static class Node {
Node prev;
Node next;
int key;
int value;
public Node() {
}
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
}
/**
- Your LRUCache object will be instantiated and called as such:
- LRUCache obj = new LRUCache(capacity);
- int param_1 = obj.get(key);
- obj.put(key,value); */ ```
- LFU
- 最不经常使用
Q9:分布式系统中常用的缓存方案有哪些(用在什么地方)?
- 客户端缓存(localStorage/sessionStorage)
- CDN缓存
- nginx缓存:静态资源
- 服务端缓存:本地缓存,外部缓存
- 数据库缓存:持久层缓存(mybatis\hibernate 多级缓存)、mysql查询缓存
- 操作系统缓存:Page Cache、buffer cache
Q10:缓存穿透、缓存击穿、缓存雪崩解决?
- 缓存穿透
- 缓存中查不到,db中也查不到
- 被恶意利用,频繁查询不存在的数据,造成数据库压力过大
- 解决方案:
- 参数合法性校验
- 数据库中不存在的结果写入缓存(要注意redis中过多的无效key,所以有效期短一点)
- 布隆过滤器(快速判断数据是否存在)见q7
- 缓存中查不到,db中也查不到
- 缓存击穿
- 缓存中没有,数据库中有。
- 一般是出现在key过期的时候,重新写入缓存需要时间,在高并发的情况下,过多的请求瞬间就到了db上
- 解决方案:
- 热点数据永不过期(另启线程定期更新同步这些数据)
- 加载db的时候,防止并发(双重检查锁)
- 缓存中没有,数据库中有。
- 缓存雪崩
- 缓存大面积过期,导致请求大量到达db (类似缓存击穿,不过是大面积)
- 解决方案:
- 把缓存有效时间分散开,防止缓存同时过期
Q11:Redis 事务机制?
- 事务开始:
- multi命令,标志一个事务的开始
- watch命令:
- 是一个乐观锁(CAS)
- 监控一个或者多个key,一旦其中有一个改动,之后的事务就不会执行
- 持续到执行exec
- 命令入队:
- 当客户端开启事务后,服务端会将命令(除了 exec\discard\watch\multi)放入一个事务队列,而不会立即执行
- 首先服务端会检查命令的正确性,如果不正确,则返回错误信息并回滚
- 如果命令没有问题,运行时出错,则不会回滚
- 事务按照fifo的方式保存
- 事务执行
- 客户端发送exec命令,服务器执行事务
Q12:Redis 和 Mysql 保持数据一致性(只能保证最终一致性)?
- 先更新db,再更新缓存
- 存在并发问题
- 先删除缓存,在更新db
- 存在脏数据的问题
- a线程删除缓存,b线程更新db,a线程更新db,本来应该a先更新
- 先删除缓存,再更新db,在删除缓存(延时双删)
- 写操作频繁,仍然有脏数据的问题(并发比较高的时候)
- 先写数据库,再删除缓存
- 问题:删除缓存可能失败
- 给缓存设置过期时间 (过期时间内,缓存不会更新)
- 引入mq(两个消费者,一个操作db,一个操作redis,保证原子性)
- 热点数据永不过期,后台线程扫描key,对于过期的key进行删除
- 加锁(效率低)
Q13:Redis 的操作命令是原子性的吗?
- redis对命令的操作是单线程的
- 多条命令不一定是原子性,如果想让多条命令同样是原子性,需要使用redis的