现在关于Java面试的资料是层出不穷,对于选择困难症的同学来说,无疑是陷入了一次次的抉择与不安中,担心错过了关键内容,现在小曾哥秉持着"融百家之所长,汇精辟之文档"的思想,整理一下目前主流的一些八股文,以达到1+1 > 2 的效果!
Redis 就是一个使用 C 语言开发的数据库,不过与传统数据库不同的是 Redis 的数据是存在内存中的 ,也就是它是内存数据库,所以读写速度非常快,因此 Redis 被广泛应用于缓存方向。
数据类型:
1、Redis支持5种核心的数据类型,分别是字符串、哈希、列表、集合、有序集合;
2、Redis还支持3 种特殊数据结构 :HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。
应用场景:缓存、排行榜、计数器/限速器(统计在线人数/浏览量/播放量)、下好友关系(点赞/共同好友)、简单的消息队列(订阅发布)、Session服务器
1.缓存:减轻MySQL的查询压力,提升系统性能;
2.排行榜:利用Redis的SortSet (有序集合)实现;
3.计算器限速器:利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等。这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力;
4.好友关系:利用集合的一-些命令,比如求交集、并集、差集等。可以方便解决一些共同好友、共同爱好之类的功能;
5.消息队列:除了Redis自身的发布/订阅模式,我们也可以利用List来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以用List来完成异步解耦;
6.Session 共享: Session是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用Redis保存Session后,无论用户落在那台机器上都能够获取到对应的Session信息。
问题描述:客户端查询根本不存在的数据,使得请求直达存储层,导致其负载过大,甚至宕机。出现这种情况的原因,可能是业务层误将缓存和库中的数据删除了,也可能是有人恶意攻击,专门访问库中不存在的数据。
解决方案:
问题描述:一份热点数据,它的访问量非常大。在其缓存失效的瞬间,大量请求直达存储层,导致服务崩溃。
解决方案:
问题描述:
在某一时刻,缓存层无法继续提供服务,导致所有的请求直达存储层,造成数据库宕机。可能是缓存中有大量数据同时过期,也可能是Redis节点发生故障,导致大量请求无法得到处理。
举个例子 :秒杀开始 12 个小时之前,我们统一存放了一批商品到 Redis 中,设置的缓存过期时间也是 12 个小时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩一样可怕。
解决方案:
想要保证缓存与数据库的双写一致,一共有4种方式,即4种同步策略:
从这4种同步策略中,我们需要作出比较的是:
从上面的比较来看,一般情况下,删除缓存是更优的方案。
首先,我们将先删除缓存与先更新数据库,在出现失败时进行一个对比:
如上图,是先删除缓存再更新数据库,在出现失败时可能出现的问题:
最终,缓存和数据库的数据是一致的,但仍然是旧的数据。而我们的期望是二者数据一致,并且是新的数据。
如上图,是先更新数据库再删除缓存,在出现失败时可能出现的问题:
1、进程A更新数据库成功;
2、进程A删除缓存失败;
3、进程B读取缓存成功,由于缓存删除失败,所以进程B读取到的是旧的数据。
最终,缓存和数据库的数据是不一致的。
经过上面的比较,我们发现在出现失败的时候,是无法明确分辨出先删缓存和先更新数据库哪个方式更好,以为它们都存在问题。后面我们会进一步对这两种方式进行比较,但是在这里我们先探讨一下,上述场景出现的问题,应该如何解决呢?
实际上,无论上面我们采用哪种方式去同步缓存与数据库,在第二步出现失败的时候,都建议采用重试机制解决,因为最终我们是要解决掉这个错误的。而为了避免重试机制影响主要业务的执行,一般建议重试机制采用异步的方式执行,如下图:
这里我们按照先更新数据库,再删除缓存的方式,来说明重试机制的主要步骤:
下面我们再将先删缓存与先更新数据库,在没有出现失败时进行对比:
如上图,是先删除缓存再更新数据库,在没有出现失败时可能出现的问题:
可见,进程A的两步操作均成功,但由于存在并发,在这两步之间,进程B访问了缓存。最终结果是,缓存中存储了旧的数据,而数据库中存储了新的数据,二者数据不一致。
如上图,是先更新数据库再删除缓存,再没有出现失败时可能出现的问题:
可见,最终缓存与数据库的数据是一致的,并且都是最新的数据。但进程B在这个过程里读到了旧的数据,可能还有其他进程也像进程B一样,在这两步之间读到了缓存中旧的数据,但因为这两步的执行速度会比较快,所以影响不大。对于这两步之后,其他进程再读取缓存数据的时候,就不会出现类似于进程B的问题了。
经过对比你会发现,先更新数据库、再删除缓存是影响更小的方案。如果第二步出现失败的情况,则可以采用重试机制解决问题。
持久化:就是把内存的数据写到磁盘中去,防止服务宕机了(重启机器、机器故障、系统故障等情况)内存数据的丢失。
Redis支持三种持久化操作:快照(snapshotting,RDB)、追加文件(append-only file, AOF)、RDB-AOF混合持久化。
RDB(Redis Database):是Redis默认采用的持久化方式,它以快照的形式将进程数据持久化到硬盘中。RDB会创建一个经过压缩的二进制文件,文件以“.rdb”结尾,内部存储了各个数据库的键值对数据等信息。
RDB持久化的触发方式:
SAVE命令执行期间,Redis服务器将阻塞,直到“.rdb”文件创建完毕为止。
而BGSAVE命令是异步版本的SAVE命令,它会使用Redis服务器进程的子进程,创建“.rdb”文件。BGSAVE命令在创建子进程时会存在短暂的阻塞,之后服务器便可以继续处理其他客户端的请求。
BGSAVE命令是针对SAVE阻塞问题做的优化,Redis内部所有涉及RDB的操作都采用BGSAVE的方式,而SAVE命令已经废弃!
BGSAVE命令的执行流程,如下图:
RDB持久化的优缺点:
AOF(Append Only File):解决了数据持久化的实时性,是目前Redis持久化的主流方式。AOF以独立日志的方式,记录了每次写入命令,重启时再重新执行AOF文件中的命令来恢复数据。
工作流程:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load)
默认情况: Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:appendonly yes
AOF持久化的文件同步机制:为了提高程序的写入性能,现代操作系统会把针对硬盘的多次写操作优化为一次写操作。【调用write写入命令时–>存入内存缓冲区–>到达指定时间周期执行Flush操作–> 写入硬盘中】
具体操作:Redis向用户提供了appendfsync选项,来控制系统冲洗AOF的频率
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显式地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
AOF持久化的优缺点:
AOF以文本协议格式写入命令 :
*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
1、文本协议具有很好的兼容性;2、直接采用文本协议格式,可以避免二次处理的开销;3、文本协议具有可读性,方便直接修改和处理。
Redis从4.0开始引入RDB-AOF混合持久化模式,这种模式是基于AOF持久化构建而来的。用户可以通过配置文件中的“aof-use-rdb-preamble yes”
配置项开启AOF混合持久化,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头,用户可以同时获得RDB持久化和AOF持久化的优点,服务器既可以通过AOF文件包含的RDB数据来实现快速的数据恢复操作,又可以通过AOF文件包含的AOF数据来将丢失数据的时间窗口限制在1s之内。
Redis是单线程的,主要是指Redis的网络IO和键值对读写是由一个线程来完成的。而Redis的其他功能,如持久化、异步删除、集群数据同步等,则是依赖其他线程来执行的。
Redis支持如下两种过期策略:
1、从过期字典中随机选择20个key;
2、删除这20个key中已过期的key;
3、如果已过期key的比例超过25%,则重复步骤1。
当写入数据将导致超出maxmemory限制时,Redis会采用maxmemory-policy所指定的策略进行数据淘汰,该策略一共包含如下8种选项:
其中,volatile前缀代表从设置了过期时间的键中淘汰数据,allkeys前缀代表从所有的键中淘汰数据。关于后缀,ttl代表选择过期时间最小的键,random代表随机选择键,需要我们额外关注的是lru和lfu后缀,它们分别代表采用lru算法和lfu算法来淘汰数据。
LRU(Least Recently Used)是按照最近最少使用原则来筛选数据,即最不常用的数据会被筛选出来!
标准LRU:把所有的数据组成一个链表,表头和表尾分别表示MRU和LRU端,即最常使用端和最少使用端。刚被访问的数据会被移动到MRU端,而新增的数据也是刚被访问的数据,也会被移动到MRU端。当链表的空间被占满时,它会删除LRU端的数据。近似LRU:Redis会记录每个数据的最近一次访问的时间戳(LRU)。Redis执行写入操作时,若发现内存超出maxmemory,就会执行一次近似LRU淘汰算法。近似LRU会随机采样N个key,然后淘汰掉最旧的key,若淘汰后内存依然超出限制,则继续采样淘汰。可以通maxmemory_samples配置项,设置近似LRU每次采样的数据个数,该配置项的默认值为5。
LRU算法的不足之处在于,若一个key很少被访问,只是刚刚偶尔被访问了一次,则它就被认为是热点数据,短时间内不会被淘汰。
实现Redis的高可用,主要有哨兵和集群两种方式。
持久化:持久化是最简单的高可用方法(有时甚至不被归为高可用的手段),主要作用是数据备份,即将数据存储在硬盘,保证数据不会因进程退出而丢失。
复制:复制是高可用Redis的基础,哨兵和集群都是在复制基础上实现高可用的。复制主要实现了数据的多机备份,以及对于读操作的负载均衡和简单的故障恢复。缺陷:故障恢复无法自动化;写操作无法负载均衡;存储能力受到单机的限制。
哨兵:在复制的基础上,哨兵实现了自动化的故障恢复。缺陷:写操作无法负载均衡;存储能力受到单机的限制。
集群:通过集群,Redis解决了写操作无法负载均衡,以及存储能力受到单机限制的问题,实现了较为完善的高可用方案。
概念:哨兵(sentinel)是一个分布式系统,用于对主从结构中的每台服务器进行监控,当出现故障时通过投票机制选择新的master并将所有slave连接到新的master。
机制:Sentinel(哨兵)它包含若干个哨兵节点和数据节点。每个哨兵节点会对数据节点(master、slave)和其余的哨兵节点进行监控,当发现节点不可达时,会对节点做下线标识。如果被标识的是主节点(master),它就会与其他的哨兵节点进行协商,当多数哨兵节点都认为主节点(master)不可达时,它们便会选举出一个哨兵节点来完成自动故障转移的工作,同时还会将这个变化实时地通知给应用方。整个过程是自动的,不需要人工介入,有效地解决了Redis的高可用问题!
一组哨兵可以监控一个主节点,也可以同时监控多个主节点,两种情况的拓扑结构如下图:
选择新的主节点多标准是:跟master断开连接的时长(>10 * down-after-milliseconds 就不适合)、slave优先级、复制offset(哪个slave复制了越多的数据,offset越靠后, 优先级就越高)、run id(选择id值较小的slave)
Redis集群采用虚拟槽分区来实现数据分片,它把所有的键根据哈希函数映射到0-16383整数槽内,计算公式为slot=CRC16(key)&16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。
元数据是指:节点负责哪些数据,是否出现故障等状态信息;常见的元数据维护方式分为:集中式和P2P方式
Gossip协议的工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播。
Redis 集群节点间采取gossip协议进行通信,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更之后不断地将元数据发送给其他节点让其他节点进行数据变更。节点互相之间不断通信,保持整个集群所有节点的数据是完整的。主要交换故障信息、 节点的增加和移除、hashslot信息等。
优点:元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;
缺点:元数据更新有延时,可能导致集群的一些操作会有一些滞后 。
内存碎片:不可用的空闲内存,在操作系统中对于这个内容有很好的解释
操作系统为你分配了 32 字节的连续内存空间,而你存储数据实际只需要使用 24 字节内存空间,那这多余出来的 8 字节内存空间如果后续没办法再被分配存储其他数据的话,就可以被称为内存碎片。
Redis 使用 zmalloc 方法(Redis 自己实现的内存分配方法)进行内存分配的时候,除了要分配 size 大小的内存之外,还会多分配 PREFIX_SIZE 大小的内存。
直接通过 config set 命令将 activedefrag 配置项设置为 yes 即可。
config set activedefrag yes
引出问题:锁在程序中的作用就是同步工具,保证共享资源在同-时刻只能被一个线程访问,Java中的锁我们都很熟悉了,像synchronized、Lock都是我们经常使用的,但是Java的锁只能保证单机的时候有效,分布式集群环境就无能为力了,这个时候我们就需要用到分布式锁。
分布式锁:就是分布式项目开发中用到的锁,可以用来控制分布式系统之间同步访问共享资源。
思路: 在整个系统提供-个全局、唯一的获取锁的"东西",然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。至于这个“东西",可以是Redis、Zookeeper, 也可以是数据库。
特性:
基于关系型数据库实现分布式锁,是依赖数据库的唯一性来实现资源锁定,比如主键和唯一索引等 。
缺点:
分布式锁的三个核心要素:加锁、解锁、锁超时
使用setnx来加锁,key是锁的唯一标识, 按业务来决定命名,value这里 设置为test。
setx key test
当得到的锁的线程执行完任务,需要释放锁,使用del指令del key
锁超时知道的是:如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程北向进来。expire key 30
优点:
缺点:
优点:
缺点:
Redis内容还有待完善,但是已经实现了从0到1的过程,后续会根据真实面试情况持续更新相关内容,实现从1到100的飞跃,我会继续更新面试板块内容,还请小伙伴们持续关注!
欢迎各位小伙伴们阅读以下内容,定能收获满满!
参考文档: