在40岁老架构师尼恩的(50+)读者社区中,经常有小伙伴,需要面试美团、京东、阿里、 百度、头条等大厂。
下面是一个5年小伙伴成功拿到通过了京东一面面试,并且最终拿到offer,月薪40K。
现在把面试真题和参考答案收入咱们的宝典,大家看看,收个优质Offer需要学点啥?
当然对于中高级开发来说,这些面试题,也有参考意义。
小伙伴说,光代码漂亮不够, 面试,还得会吹。
这里把题目以及小伙伴的吹牛逼的方式方法,经过整理和梳理之后,收入咱们的《尼恩Java面试宝典》 V95版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发、吹牛水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到公号【技术自由圈】取
小伙伴从以下2个维度去 “吹牛”, 吹到 面试官 “口水直流,不能自已”
程序计数器:当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。
Java虚拟栈:存放基本数据类型、对象的引用、方法出口等,线程私有。
Native方法栈:和虚拟栈相似,只不过它服务于Native方法,线程私有。
Java堆:java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,线程共享。
方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),回收目标主要是常量池的回收和类型的卸载,各线程共享
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
尼恩说明:
由于篇幅限制,这里对JVM内存结构、Java内存模式的介绍,没有做展开,
有关JVM内存结构、Java内存模式的详细介绍,可以参考《尼恩Java面试宝典》中JVM面试专题的内存模型部分
小伙伴从以下6个维度去 “吹牛”, 吹到 面试官 “口水直流,不能自已”
垃圾回收的本质
垃圾回收是Java程序执行自动内存管理的过程。当Java程序在JVM上运行时,将在堆上创建对象,这是专用于该程序的内存的一部分。最终,将不再需要某些对象。垃圾收集器找到这些未使用的对象并将其删除以释放内存。
CMS和G1有什么区别
CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
G1(Garbage First)收集器(标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
区别:
在JVM中一次完整的GC流程如下:
JVM中的垃圾回收分为三个阶段:Minor GC、Full GC和G1 GC。
当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。
如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。
Java中判断对象存活的方法有以下两种算法:
Java中常见的三种垃圾收集算法是标记-清除算法(Mark-Sweep)、复制算法(Copying)和分代收集算法(Generational Collection)。
小伙伴说,他是参考《尼恩Java面试宝典》中 Redis 专题中的数据一致性问题及方案去吹的, 面试官非常满意
小伙伴从以下7个维度去 “吹牛”, 吹到 面试官 “口水直流,不能自已”
Redis高可用集群有两种,分别是主从哨兵模式和集群模式
1.主从哨兵模式
其中一台服务器作为master服务器,提供读写服务,配置多台从服务器,从服务器只提供只读服务,同时配置多台sentinel,也即是哨兵,哨兵的作用是可以监控master节点,如果master宕机,可以从从服务器中选举出一台作为master服务器。
哨兵模式,客户端连接哨兵集群,即可获得master服务器的信息。此时客户端并不会做读写分离,也就是所有读写都由master服务器处理,这里相当于从服务器只作为主服务器的数据备份。如果master发生故障,切换到其他从服务器,哨兵会把新的master服务器地址告知客户端。
jedis和RedisTemplate都没有实现读写分离。如果需要可以分别建立master服务器连接池和slave服务器连接池,并严格区分读写操作,路由到需要使用的连接池。需要注意的是,Redis主从复制是异步的,可能存在小概率数据不一致的问题。
2.Cluster集群模式
在主从哨兵模式,所有的写操作都是由master处理,这在性能上可能会出现瓶颈。Redis3.0后推出了集群模式,可以实现水平扩展,配置多台的master服务器处理读写请求。
集群模式下,看似于将一个大的主从架构拆分成多个主从架构的服务器群,具有复制,高可用和分片的特性。不需要哨兵,也可以实现节点故障移除和master选举功能。性能和高可用性均优于哨兵模式,但需要更多的服务器。可以从公司业务的并发量和成本等角度考量选择哪种模式。
Redis Cluster集群模式默认将所有的数据划分为16384个slot槽,每个master节点均匀负责一部的槽位。
通常,会对key值使用crc16算法进行hash得到一个整数值,然后使用这个整数值对16384进行取模,来得到具体的槽位。
Cluster集群模式下,Redis默认从服务是不分担读请求,只作为备注和故障转移。但有读请求到达从服务器,会重定向到主服务器处理。
Redis主从数据同步大致分为两种,全量复制和增量复制。
全量复制
增量复制
一般情况下,主从断开连接后会进行全量复制,但Redis2.8后开始支持部分数据的复制。
master和从服务器第一次连接时会进行全量复制,同时master和所有的slave都会维护一个复制数据的偏移量offset和master的进程id。
如果从服务器断开重连后,会比较偏移量是否太旧或者master进程id是否变更了,如果这样则会进行一次全量复制,否则会进行部分复制,把offset之后的数据同步给从服务器。
1.主从哨兵模式
这种模式下,是有哨兵监控master服务器状态,并实现故障转移。一旦master服务器宕机,则哨兵会从剩下的从服务器中选举一条作为新的master节点。这里有几个概念:
主观下线:
哨兵会定期向主服务器发送心跳包检测是否正常,如果超过配置文件中sentinel down-after-milliseconds mymaster 配置的时间没有收到主服务器的回复,则这个哨兵认为主服务下线。
客观下线:
一个哨兵把master记为主观下线,并不代表master就一定下线了,此时要向其他哨兵确认master是否真的下线,如果超过sentinel monitor mymaster 配置的数量(一般为哨兵数量/2 + 1)哨兵认为master下线,则记为客观下线。
哨兵选举master服务器过程:
2.集群模式
小伙伴说,他是参考《尼恩Java面试宝典》中Mysql 面试专题中的Mysql原理、MyISAM和InnoDB的对比去吹的
小伙伴从以下6个维度去 “吹牛”, 吹到 面试官 “口水直流,不能自已”
索引的底层数据结构有:
时间复杂度对比:
Hash的时间O(1)
Tree的O(logn)
为什么索引采用B+树,而不采用Hash这种结构?
Hash这种结构对与获取单条记录时的查询效率是要比B+树效率要高的,但是对与数据的范围查找效率就很低,特别是对于数据量大的时候(10万 甚至百万级别的数据)。
为什么低?它是根据字段值算出获取一个hash值,然后根据hash值找到在索引表中查找出hash值对应的数据行,它的索引没有按顺序存储
B+树:是一种多路平衡树,一个横向存放多个节点,并且它的非叶子节点都只存放指向子节点的索引指针,不存放数据。所有非叶子节点都存放一个节点在叶子节点上,并且这个叶子节点是一个有序的列表结构。
当我们查一个范围数据时,通过B+树搜索
多路:一课树每个节点最多可以拥有大于2个子节点的树
平衡树: 一颗树左子树和右子树保持相对平衡,不会出现一边层级特别多,一边层级特别少的情况
为什么不用二叉树?
二叉树为非平衡树,如果是有序递变的数据,很有可能就会变成左边树层级过高,或则右边树的层级过高,甚至是退化成链表。
层级过高为什么就不能用了呢?
假设是1,2,3,4,5,6这样的数据,如果我们要找到底部6这个数据,就要进行6次IO(加载磁盘数据进内存,索引是在磁盘文件中以表的形式存在的)才能找到。如果按一次IO10ms计算,60ms也不算什么。但是假设是100万呢?1001000010ms,也就是10000s,换算成分钟也就是167,所以这种结构肯定是不行的。
为什么不用红黑二叉树?
红黑二叉树一种平衡树,虽然它不会出现有一树的层级过高的情况,但是它还是没有从根本上解决树的层级问题,随着数据量增大,树的层级会越来越高。要遍历叶子节点的数据IO消耗还是过高。
为什么没有采用B树,或则说B-树?
B树虽然虽然通过多路平衡树解决了树的高度问题,但是它对与访问数据的查找效率还是低下的,对于数据访问在非叶子节点和叶子节点都有的范围,它不仅非叶子节点的遍历还需要做叶子节点的遍历。
总体来说:选择B+树是因为它具有以下优点:
因此,B+树是一种非常适合用于MySQL索引的底层数据结构,它可以提供高效、稳定、可靠的查询性能。
小伙伴说,他是参考《尼恩Java面试宝典》中Mysql 面试专题中的索引失效面试题去吹的, 面试官非常满意
设计一个分布式自增id生成服务的步骤如下:
下面是一个简单的Java实现:
public class DistributedIdGenerator {
private static final int PARTITION_ID = 1; // 分区ID
private static final int NODE_ID = 1; // 节点ID
private static final int SEQUENCE = 1; // 序列号
private static final long SEQUENCE_ROOT = 1L << 32; // 序列号根节点
private static final long MAX_ID = SEQUENCE_ROOT + 1 - 1L << 32; // 最大ID
private static final long TIMESTAMP = System.currentTimeMillis() / 1000; // 时间戳
private static final long MACHINE_ID = UUID.randomUUID().getMostSignificantBits(); // 机器ID
private static final int SCALE = 10; // 位数
private static final int SHIFT = 22; // 位移
private static final int PARTITION_SIZE = 1 << SCALE; // 分区大小
private static final DistributedIdGenerator instance = new DistributedIdGenerator();
private static int sequence = 0; // 当前节点的序列号
private static long lastId = SEQUENCE_ROOT; // 上一次生成的ID
private static Map<Integer, Long> partitionMap = new HashMap<>(); // 分区ID和最大ID的映射
private DistributedIdGenerator() {
// 初始化分区ID和节点ID
partitionMap.put(PARTITION_ID, SEQUENCE_ROOT);
partitionMap.put(NODE_ID, SEQUENCE_ROOT);
}
public static synchronized long generateId() {
// 获取当前节点的序列号
sequence = (sequence + 1) & SEQUENCE_ROOT;
if (sequence == 0) {
// 如果序列号溢出,则从上一次生成的ID开始
sequence = partitionMap.get(PARTITION_ID);
if (sequence == SEQUENCE_ROOT) {
// 如果分区ID溢出,则从最大ID开始
sequence = partitionMap.get(NODE_ID);
if (sequence == SEQUENCE_ROOT) {
// 如果节点ID溢出,则从最大ID开始
sequence = SEQUENCE_ROOT;
}
}
}
// 获取当前时间的毫秒数
long now = System.currentTimeMillis() / 1000;
// 计算ID
long id = ((now - TIMESTAMP) << SHIFT) | (MACHINE_ID << SCALE) | (sequence << SEQUENCE_SHIFT) | lastId;
// 如果ID溢出,则从最大ID开始
if (id > MAX_ID) {
id = SEQUENCE_ROOT;
}
// 更新上一次生成的ID
lastId = id;
// 更新分区ID和节点ID的映射
partitionMap.put(PARTITION_ID, partitionMap.get(PARTITION_ID) + PARTITION_SIZE);
partitionMap.put(NODE_ID, partitionMap.get(NODE_ID) + PARTITION_SIZE);
// 返回ID
return id;
}
}
这个实现使用了Snowflake算法,通过获取当前时间的毫秒数、机器ID和序列号来生成ID。在生成ID时,首先获取当前节点的序列号,如果序列号溢出,则从上一次生成的ID开始。如果分区ID和节点ID都溢出,则从最大ID开始。最后,计算出ID并更新上一次生成的ID和分区ID和节点ID的映射。
小伙伴从以下4个维度去 “吹牛”, 吹到 面试官 “口水直流,不能自已”
XSS 攻击是指攻击者通过在受信任的网页上注入恶意脚本,使得脚本在用户浏览器中执行,从而窃取用户敏感信息、劫持会话或执行其他恶意行为。XSS 攻击的原理是利用网页中的漏洞,将攻击者的恶意脚本注入到网页中,然后当用户浏览该网页时,浏览器会执行该恶意脚本。
解决 XSS 攻击的方法包括:
CSRF 攻击是指攻击者通过伪造用户的请求,向网站服务器发送恶意请求,从而操纵用户账户或执行其他恶意行为。CSRF 攻击的原理是利用网站应用程序中的漏洞,绕过网站的安全验证机制,以用户的身份执行恶意请求。
解决 CSRF 攻击的方法包括:
DDoS 攻击是指攻击者通过控制大量僵尸主机,向目标主机发送海量流量,从而瘫痪目标主机或使其服务不可用。DDoS 攻击的原理是通过大量流量消耗目标主机的带宽和资源,使其无法正常提供服务。
解决 DDoS 攻击的方法包括:
SQL 注入攻击是指攻击者通过在 Web 应用程序的输入框中注入恶意 SQL 语句,从而获取未授权的访问权限或窃取敏感数据。SQL 注入攻击的原理是利用应用程序中的漏洞,将恶意 SQL 语句插入到 SQL 查询语句中,从而操纵数据库。
小伙伴从以下8个维度去 “吹牛”, 吹到 面试官 “口水直流,不能自已”
小伙伴从以下7个维度去 “吹牛”, 吹到 面试官 “口水直流,不能自已”
在 Redis 集群中,当多个节点同时进行写操作时,可能会导致数据不一致。例如,当节点 A 和节点 B 同时尝试向同一个 key 写入数据时,如果节点 A 先写入成功,而节点 B 的后写入操作失败,则导致该 key 的数据在节点 A 和节点 B 上不一致。为了解决这个问题,可以使用 Redis 集群的主节点来协调多个节点之间的数据写入操作,确保数据的一致性。
解决方法:
在使用 Redis 集群时,可能会出现性能瓶颈。例如,当集群中的某个节点出现网络延迟或负载过高时,可能会导致整个集群的性能下降。此外,由于 Redis 集群需要进行数据同步和协调,因此可能会增加额外的延迟和开销,从而影响集群的性能。
解决方法:
在使用 Redis 集群时,如果某个节点出现故障或宕机,可能会导致整个集群的不可用。
解决方法:
需要使用 Redis 集群的高可用性机制来确保集群的可用性,例如使用多个节点进行数据冗余和备份,以便在节点故障或宕机时能够自动切换到备用节点。
可以通过以下两种方式来实现:
在Redis集群中,每个节点都需要承担一定的负载,如果负载不均衡,可能会导致某些节点的负载过高,影响系统的性能。
解决方法:
可以使用Redis Cluster来解决负载均衡问题。Redis Cluster会根据节点的负载情况,自动地将负载较高的节点上的数据转移到负载较低的节点上,从而实现负载均衡。
在Redis集群中,节点之间的通信需要通过网络进行,如果网络延迟过高,可能会导致数据的延迟,影响系统的性能。
解决方法:
可以使用Redis Cluster来解决网络延迟问题。Redis Cluster会根据节点之间的网络延迟情况,自动地将数据转移到网络延迟较低的节点上,从而降低数据的延迟。
在Redis集群中,如果某个节点发生故障,可能会导致数据的不一致性或者系统的不可用性。
解决方法:
可以使用Redis Cluster来解决节点故障问题。Redis Cluster会自动地将故障节点上的数据转移到其他节点上,保证数据的一致性和系统的可用性。
在Redis集群中,数据的安全性也是一个问题,如果节点的安全性受到攻击,可能会导致数据的泄露或者被篡改。因此,在使用Redis集群时,需要注意数据的安全性,并采取相应的安全措施。
解决方法:
可以使用Redis Cluster来解决安全问题。Redis Cluster会对节点的安全性进行监控,并采取相应的安全措施,例如限制节点的访问权限、加密数据等,从而保证数据的安全性。
小伙伴从以下8个维度去 “吹牛”, 吹到 面试官 “口水直流,不能自已”
CAP理论作为分布式系统的基础理论,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),这三个要素最多只能同时实现两点。
一致性(C:Consistency):
一致性是指数据在多个副本之间能否保持一致的特性。例如一个数据在某个分区节点更新之后,在其他分区节点读出来的数据也是更新之后的数据。
可用性(A:Availability):
可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。这里的重点是"有限时间内"和"返回结果"。
分区容错性(P:Partition tolerance):
分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务。
选择 | 说明 |
---|---|
CA | 放弃分区容错性,加强一致性和可用性,其实就是传统的单机数据库的选择 |
AP | 放弃一致性,分区容错性和可用性,这是很多分布式系统设计时的选择 |
CP | 放弃可用性,追求一致性和分区容错性,网络问题会直接让整个系统不可用 |
BASE 理论, 是对CAP中AP的一个扩展,对于我们的业务系统,我们考虑牺牲一致性来换取系统的可用性和分区容错性。BASE是Basically Available(基本可用),Soft state(软状态),和 Eventually consistent(最终一致性)三个短语的缩写。
Basically Available
基本可用:通过支持局部故障而不是系统全局故障来实现的。如将用户分区在 5 个数据库服务器上,一个用户数据库的故障只影响这台特定主机那 20% 的用户,其他用户不受影响。
Soft State
软状态,状态可以有一段时间不同步
Eventually Consistent
最终一致,最终数据是一致的就可以了,而不是时时保持强一致。
小伙伴从以下4个维度去 “吹牛”, 吹到 面试官 “口水直流,不能自已”
zookeeper实现数据一致性的核心是ZAB协议(Zookeeper原子消息广播协议)。该协议需要做到以下几点:
(1)集群在半数以下节点宕机的情况下,能正常对外提供服务;
(2)客户端的写请求全部转交给leader来处理,leader需确保写变更能实时同步给所有follower及observer;
(3)leader宕机或整个集群重启时,需要确保那些已经在leader服务器上提交的事务最终被所有服务器都提交,确保丢弃那些只在leader服务器上被提出的事务,并保证集群能快速恢复到故障前的状态。
Zab协议有两种模式, 崩溃恢复(选主+数据同步)和消息广播(事务操作)。
任何时候都需要保证只有一个主进程负责进行事务操作,而如果主进程崩溃了,就需要迅速选举出一个新的主进程。主进程的选举机制与事务操作机制是紧密相关的。
下面详细讲解这三个场景的协议规则,从细节去探索ZAB协议的数据一致性原理。
leader选举是zk中最重要的技术之一,也是保证分布式数据一致性的关键所在。当集群中的一台服务器处于如下两种情况之一时,就会进入leader选举阶段——服务器初始化启动、服务器运行期间无法与leader保持连接。
选举阶段,集群间互传的消息称为投票,投票Vote主要包括二个维度的信息:ID、ZXID
节点进入选举阶段后的大体执行逻辑如下:
(1)设置状态为LOOKING,初始化内部投票Vote (id,zxid) 数据至内存,并将其广播到集群其它节点。节点首次投票都是选举自己作为leader,将自身的服务ID、处理的最近一个事务请求的ZXID(ZXID是从内存数据库里取的,即该节点最近一个完成commit的事务id)及当前状态广播出去。然后进入循环等待及处理其它节点的投票信息的流程中。
(2)循环等待流程中,节点每收到一个外部的Vote信息,都需要将其与自己内存Vote数据进行PK,规则为取ZXID大的,若ZXID相等,则取ID大的那个投票。若外部投票胜选,节点需要将该选票覆盖之前的内存Vote数据,并再次广播出去;同时还要统计是否有过半的赞同者与新的内存投票数据一致,无则继续循环等待新的投票,有则需要判断leader是否在赞同者之中,在则退出循环,选举结束,根据选举结果及各自角色切换状态,leader切换成LEADING、follower切换到FOLLOWING、observer切换到OBSERVING状态。
算法细节可参照FastLeaderElection.lookForLeader(),主要有三个线程在工作:选举线程(主动调用lookForLeader方法的线程,通过阻塞队列sendqueue及recvqueue与其它两个线程协作)、WorkerReceiver线程(选票接收器,不断获取其它服务器发来的选举消息,筛选后会保存到recvqueue队列中。zk服务器启动时,开始正常工作,不停止)以及WorkerSender线程(选票发送器,会不断地从sendqueue队列中获取待发送的选票,并广播至集群)。WorkerReceiver线程一直在工作,即使当前节点处于LEADING或者FOLLOWING状态,它起到了一个过滤的作用,当前节点为LOOKING时,才会将外部投票信息转交给选举线程处理;如果当前节点处于非LOOKING状态,收到了处于LOOKING状态的节点投票数据(外部节点重启或网络抖动情况下),说明发起投票的节点数据跟集群不一致,这时,当前节点需要向集群广播出最新的内存Vote(id,zxid),落后节点收到该Vote后,会及时注册到leader上,并完成数据同步,跟上集群节奏,提供正常服务。
选主算法中的zxid是从内存数据库中取的最新事务id,事务操作是分两阶段的(提出阶段和提交阶段),leader生成提议并广播给followers,收到半数以上的ACK后,再广播commit消息,同时将事务操作应用到内存中。
follower收到提议后先将事务写到本地事务日志,然后反馈ACK,等接到leader的commit消息时,才会将事务操作应用到内存中。可见,选主只是选出了内存数据是最新的节点,仅仅靠这个是无法保证已经在leader服务器上提交的事务最终被所有服务器都提交。比如leader发起提议P1,并收到半数以上follower关于P1的ACK后,在广播commit消息之前宕机了,选举产生的新leader之前是follower,未收到关于P1的commit消息,内存中是没有P1的数据。而ZAB协议的设计是需要保证选主后,P1是需要应用到集群中的。这块的逻辑是通过选主后的数据同步来弥补。
选主后,节点需要切换状态,leader切换成LEADING状态后的流程如下:
(1)重新加载本地磁盘上的数据快照至内存,并从日志文件中取出快照之后的所有事务操作,逐条应用至内存,并添加到已提交事务缓存commitedProposals。这样能保证日志文件中的事务操作,必定会应用到leader的内存数据库中。
(2)获取learner发送的FOLLOWERINFO/OBSERVERINFO信息,并与自身commitedProposals比对,确定采用哪种同步方式,不同的learner可能采用不同同步方式(DIFF同步、TRUNC+DIFF同步、SNAP同步)。这里是拿learner内存中的zxid与leader内存中的commitedProposals(min、max)比对,如果zxid介于min与max之间,但又不存在于commitedProposals中时,说明该zxid对应的事务需要TRUNC回滚;如果 zxid 介于min与max之间且存在于commitedProposals中,则leader需要将zxid+1~max 间所有事务同步给learner,这些内存缺失数据,很可能是因为leader切换过程中造成commit消息丢失,learner只完成了事务日志写入,未完成提交事务,未应用到内存。
(3)leader主动向所有learner发送同步数据消息,每个learner有自己的发送队列,互不干扰。同步结束时,leader会向learner发送NEWLEADER指令,同时learner会反馈一个ACK。当leader接收到来自learner的ACK消息后,就认为当前learner已经完成了数据同步,同时进入“过半策略”等待阶段。当leader统计到收到了一半已上的ACK时,会向所有已经完成数据同步的learner发送一个UPTODATE指令,用来通知learner集群已经完成了数据同步,可以对外服务了。
细节可参照Leader.lead() 、Follower.followLeader()及LearnerHandler类。
ZAB协议对于事务操作的处理是一个类似于二阶段提交过程。
针对客户端的事务请求,leader服务器会为其生成对应的事务proposal,并将其发送给集群中所有follower机器,然后收集各自的选票,最后进行事务提交。
流程如下图
ZAB协议的二阶段提交过程中,移除了中断逻辑(事务回滚),所有follower服务器要么正常反馈leader提出的事务proposal,要么就抛弃leader服务器。follower收到proposal后的处理很简单,将该proposal写入到事务日志,然后立马反馈ACK给leader,也就是说如果不是网络、内存或磁盘等问题,follower肯定会写入成功,并正常反馈ACK。leader收到过半follower的ACK后,会广播commit消息给所有follower,并将事务应用到内存;follower收到commit消息后会将事务应用到内存。
ZAB协议中多次用到“过半”设计策略 ,该策略是zk在A(可用性)与C(一致性)间做的取舍,也是zk具有高容错特性的本质。相较分布式事务中的2PC(二阶段提交协议)的“全量通过”,ZAB协议可用性更高(牺牲了部分一致性),能在集群半数以下服务宕机时正常对外提供服务。
小伙伴说,他是参考《尼恩Java面试宝典》中架构设计面试专题 吹的, 面试官非常满意
小伙伴从以下3个维度去 “吹牛”, 吹到 面试官 “口水直流,不能自已”
我们可以在更新数据时不更新缓存,而是删除缓存中的数据,在读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。
缓存读写过程
这个策略就是我们使用缓存最常见的策略,Cache Aside 策略(也叫旁路缓存策略),这个策略数据以数据库中的数据为准,缓存中的数据是按需加载的。它可以分为读策略和写策略,
读策略的步骤是:
从缓存中读取数据;
如果缓存命中,则直接返回数据;
如果缓存不命中,则从数据库中查询数据;
查询到数据后,将数据写入到缓存中,并且返回给用户。
写策略的步骤是:
更新数据库中的记录;
删除缓存记录。
注意
Cache Aside 存在的最大的问题是当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响。如果你的业务对缓存命中率有严格的要求,那么可以考虑两种解决方案:
这个策略的核心原则是用户只与缓存打交道,由缓存和数据库通信,写入或者读取数据。这就好比你在汇报工作的时候只对你的直接上级汇报,再由你的直接上级汇报给他的上级,你是不能越级汇报的。
Write Through 的策略是这样的:先查询要写入的数据在缓存中是否已经存在,如果已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,如果缓存中数据不存在,我们把这种情况叫做“Write Miss(写失效)”。
一般来说,我们可以选择两种“Write Miss”方式:一个是“Write Allocate(按写分配)”,做法是写入缓存相应位置,再由缓存组件同步更新到数据库中;另一个是“No-write allocate(不按写分配)”,做法是不写入缓存中,而是直接更新到数据库中。
在 Write Through 策略中,我们一般选择“No-write allocate”方式,原因是无论采用哪种“Write Miss”方式,我们都需要同步将数据更新到数据库中,而“No-write allocate”方式相比“Write Allocate”还减少了一次缓存的写入,能够提升写入的性能。
Read Through 策略就简单一些,它的步骤是这样的:先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库中同步加载数据。
下面是 Read Through/Write Through 策略的示意图:
Read/Write Through策略示意图
Read Through/Write Through 策略的特点是由缓存节点而非用户来和数据库打交道,在我们开发过程中相比 Cache Aside 策略要少见一些,原因是我们经常使用的分布式缓存组件,无论是 Memcached 还是 Redis 都不提供写入数据库,或者自动加载数据库中的数据的功能。而我们在使用本地缓存的时候可以考虑使用这种策略,比如说在上一节中提到的本地缓存 Guava Cache 中的 Loading Cache 就有 Read Through 策略的影子。
我们看到 Write Through 策略中写数据库是同步的,这对于性能来说会有比较大的影响,因为相比于写缓存,同步写数据库的延迟就要高很多了。那么我们可否异步地更新数据库?这就是我们接下来要提到的“Write Back”策略。
这个策略的核心思想是在写入数据时只写入缓存,并且把缓存块儿标记为“脏”的。而脏块儿只有被再次使用时才会将其中的数据写入到后端存储中。
需要注意的是,在“Write Miss”的情况下,我们采用的是“Write Allocate”的方式,也就是在写入后端存储的同时要写入缓存,这样我们在之后的写请求中都只需要更新缓存即可,而无需更新后端存储了,我将 Write back 策略的示意图放在了下面:
Write Back 写回策略示意图
如果使用 Write Back 策略的话,读的策略也有一些变化了。
我们在读取缓存时如果发现缓存命中则直接返回缓存数据。如果缓存不命中则寻找一个可用的缓存块儿,如果这个缓存块儿是“脏”的,就把缓存块儿中之前的数据写入到后端存储中,并且从后端存储加载数据到缓存块儿,如果不是脏的,则由缓存组件将后端存储中的数据加载到缓存中,最后我们将缓存设置为不是脏的,返回数据就好了。
Write Back 读策略示意图
其实这种策略不能被应用到我们常用的数据库和缓存的场景中,它是计算机体系结构中的设计,比如我们在向磁盘中写数据时采用的就是这种策略。无论是操作系统层面的 Page Cache,还是日志的异步刷盘,亦或是消息队列中消息的异步写入磁盘,大多采用了这种策略。因为这个策略在性能上的优势毋庸置疑,它避免了直接写磁盘造成的随机写问题,毕竟写内存和写磁盘的随机 I/O 的延迟相差了几个数量级呢。
但因为缓存一般使用内存,而内存是非持久化的,所以一旦缓存机器掉电,就会造成原本缓存中的脏块儿数据丢失。所以你会发现系统在掉电之后,之前写入的文件会有部分丢失,就是因为 Page Cache 还没有来得及刷盘造成的。
当然,你依然可以在一些场景下使用这个策略,在使用时,我想给你的落地建议是:你在向低速设备写入数据的时候,可以在内存里先暂存一段时间的数据,甚至做一些统计汇总,然后定时地刷新到低速设备上。比如说,你在统计你的接口响应时间的时候,需要将每次请求的响应时间打印到日志中,然后监控系统收集日志后再做统计。但是如果每次请求都打印日志无疑会增加磁盘 I/O,那么不如把一段时间的响应时间暂存起来,经过简单的统计平均耗时,每个耗时区间的请求数量等等,然后定时地,批量地打印到日志中。
尼恩的系统架构知识图谱(一张价值10w的系统架构知识图谱)
www.processon.com/view/link/60fb9421637689719d246739
尼恩的秒杀系统的架构
www.processon.com/view/link/61148c2b1e08536191d8f92f
在尼恩的(50+)读者社区中,很多、很多小伙伴需要进大厂、拿高薪。
尼恩团队,会持续结合一些大厂的面试真题,给大家梳理一下学习路径,看看大家需要学点啥?
前面用多篇文章,给大家介绍阿里、百度、字节、滴滴的真题:
《太猛了,靠“吹牛”过顺丰一面,月薪30K》
《炸裂了…京东一面索命40问,过了就50W+》
《问麻了…阿里一面索命27问,过了就60W+》
《百度狂问3小时,大厂offer到手,小伙真狠!》
《饿了么太狠:面个高级Java,抖这多硬活、狠活》
《字节狂问一小时,小伙offer到手,太狠了!》
《收个滴滴Offer:从小伙三面经历,看看需要学点啥?》
这些真题,都会收入到 史上最全、持续升级的 PDF电子书 《尼恩Java面试宝典》。
本文收录于 《尼恩Java面试宝典》 V95版。
基本上,把尼恩的 《尼恩Java面试宝典》吃透,大厂offer很容易拿到滴。
另外,下一期的 大厂面经,更加精彩,具体可以参见文末公号。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓