文章目录
- 面向对象
-
- 什么是面向对象?
- 封装
- 继承
- 多态
- == 和equals比较
- hashCode与equals
- 重载和重写的区别
- Final
- 类加载器
- spring是什么
- AOP的理解
- 谈谈你对IOC的理解
- 零拷贝
- RocketMQ 架构设计
- RocketMq 事务消息原理
- RockeMq顺序消息消费原理
- 简述RockerMQ持久化机制
- RocketMQ如何保证不丢消息
- 消息发送
- 消息消费
- Mysql
-
- 索引的基本原理
- mysql聚族和非聚族索引的区别
- 业务系统里面的sql耗时,慢查询
- 最左前缀原则是什么
- explain的type类型
-
- explain 的key和key_len,extra
- Innodb是如何实现事务的
- redolog 和 undolog的区别
- zk的初始化选举和崩溃选举过程
-
- 初始化选举
- 崩溃选举
- zookeeper集群中节点之间数据是如何同步的
-
- 初始化:没有历史数据,5个节点为例
- 崩溃选举
- 脑裂
- 布隆过滤器
- zk的数据模型
-
- 分布式缓存寻址算法
- redis的持久化机制
- Redis单线程为什么这么快
- redis高可用方案
-
- 主从
- Redis Cluster
- 微服务拆分
- 微服务拆分坚守哪些指导原则
- 服务拆分的几种方法
-
- 纵向拆分
- 横向拆分
- 基于可扩展拆分
- 基于可靠性拆分
- G1是如何做到对垃圾回收导致系统的停顿是可控的?
- 如何设定G1对应的内存大小
- CMS垃圾回收中的“浮动垃圾”的理解
- TCP和UDP的区别
- TCP协议如何保证可靠性传输
- 你还有什么问题要问的吗? 如何做到高质量回答?
- 软件架构设计原则
-
- 开闭原则
- 依赖倒置原则
- 单一职责原则
- 接口隔离原则
- 迪米特法则
- 里氏替换原则
- 合成复用原则
- 线程池和CPU核心数的关系
- CPU的核心数,CPU的线程数
面向对象
什么是面向对象?
对比面向过程,是两种不同的处理问题角度
面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者(对象),及各自需要做什么。
比如:实现一个模块功能
面向过程会将任务拆解成一系列的步骤(函数),1,书写技术文档 2,排期 3,代码编写 4,测试 5,上线
面向对象会拆除技术架构师 java开发工程师 测试工程师 运维工程师
架构师:调研 技术文档编写
开发工程师:代码编写
测试工程师:测试
运维工程师:上线
面向过程比较直接高效,而面向对象更容易复用、扩展和维护
面向对象
封装
封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项
内部细节对外部调用透明,外部调用无需修改或者关心内部实现
eg: javabean的getset方法对外访问,因为属性的赋值智能由javabean本身决定。而不能由外部胡乱修改。
private String name;
public void setName(String name){
this.name = "tuling_"+name;
}
public String getName(){
retrun this.name;
}
该name有自己的命名规则,明显不能由外部直接赋值
继承
继承基类的方法,并做出自己的改变或扩展
子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需要扩展自己个性化的
多态
基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同
== 和equals比较
==对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象地址。
equeals: object中默认也是采用==比较,通常会重写。
- Object:
public boolean equals(Object obj) {
return (this == obj);
}
- String
String类中被重写的equals()方法其实是比较两个字符串的内容。
public boolean equals(Object anObject) {if (this == anObject) {return true;}if (anObject instanceof String) {String anotherString = (String)anObject;int n = value.length;if (n == anotherString.value.length) {char v1[] = value;char v2[] = anotherString.value;int i = 0;while (n-- != 0) {if (v1[i] != v2[i])return false;i++;}return true;}}return false;}
public static void main(String[] args) {String aa="aa";String a2=new String("aa");String a3=a2;System.out.println(aa==a2); //falseSystem.out.println(aa==a3); //falseSystem.out.println(a2==a3); //true
System.out.println(aa.equals(a2));//trueSystem.out.println(aa.equals(a3));//trueSystem.out.println(a2.equals(a3)); //true
}
hashCode与equals
hashCode()的作用是获取哈希码(散列码),实际返回一个int整数。
作用是确定该对象在哈希表中的索引位置。hashCode()定义在JDK的Object.java中,java中的任何类都包含有hashCode()函数。
散列表存储的键值对(key-value),它的特点是:能根据键快速检索出对应的value,这其中就用到了散列码。
为什么要有hashCode:
以“HashSet如何检查重复”为例子来说明为什么要有hashCode。
对象加入HashSet时,HashSet会先计算出对象的HashCode值来判断对象加入的位置,看该位置时候有值,如果没有,HashSet会假设对象没有重复出现。但是如果发现有值,这时会调用equals()方法来检查两个对象是否真的相同,HashSet就不会让其加入操作成功。
如果不同的话,就会重新散列到其他位置。这样就大大减少了equals的次数,相应就大大提高了执行速度。
- 如果两个对象相等,则hashcode一定也是相等
- 两个对象相等,对两个对象分别调用equals方法都返回true。
- 两个对象有相同的hashcode的值,他们不一定是相等的
- 因此equals方法被覆盖过,则hashCode方法也必须被覆盖。
- hashCode()的默认行为是堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等。
重载和重写的区别
重载:发生在同一个类中,方法名必须相同,参数类型不同,个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写:发生在父子类中,方法名,参数列表必须相同,返回值范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法修饰符为private则子类就不能重写该方法。
Final
最终的
- 修饰类:表示类不可被继承
- 修饰方法:表示方法不可被子类覆盖,但是可以重载。
- 修饰变量:表示变量一旦被赋值就不可以更改它的值。
类加载器
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器
1)启动类加载器(Bootstarp ClassLoader)用来加载java核心类库,无法被java程序直接引用。
2)扩展类加载器(ExtensionsClassLoader)用来加载java的扩展库。Java虚拟机的实现会提供一个扩展库目录。
该类加载器在此目录里面查找并加载类。
3)系统类加载器(System class loader)也叫应用类加载器:它根据Java应用的类路径(ClassPath)来加载Java类。一般来说,Java应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClasssLoader()来获取它。
4)用户自定义类加载器,通过继承java.lang.ClassLoader类的方式实现。
spring是什么
轻量级的开源J2EE框架。它是一个容器框架,用来装javabean(java对象)。
Spring是一个轻量级的控制翻转IOC和面向切面AOP的容器架构
- 从大小与开销两方面而言Spring都是轻量级的。
- 通过控制翻转(IOC)的技术达到松耦合的目的。
- 提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统服务进行内聚性的开发
- 包含并管理应用对象(Bean)的配置和生命周期,这个意义上是一个框架。
- 将简单的组件配置、组合成为复杂的应用、这个意义上是一个框架。
AOP的理解
系统是由许多不同的组件组成的,每一个组件各负责一块特定的功能。除了实现自身的核心功能外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的核心服务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常称为横切关注点,因为他们会跨越系统的多个组件。
当我们需要分散的对象引入公共行为的时候,OOP则显的无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。
日志代码往往水平的散步在所有的对象层次中,而它所散列到的对象的核心功能毫无关系。
在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP:将程序中的交叉业务逻辑(比如安全、日志、事务等),封装成一个切面,然后注入到目标对象(具体业务逻辑)中去。AOP可以对某个对象或某些对象的功能进行增强,比如对象中的方法进行增强,可以执行某个方法之前额外的做一些事情,在某个方法执行之后额外做的一些事情。
谈谈你对IOC的理解
容器概念、控制翻转、依赖注入
IOC容器:实际上就是个map(key,value),里面存的各种对象(在xml里配置的bean节点、@repository@service@controller@compoment),在项目启动的时候会读取配置文件里面的bean节点,根据全限定类目使用反射创建对象放到map里、扫描到有上述注解的类还是通过反射创建对象放到map里。
这个时候map里就有各种对象了,接下来我们在代码里需要用到里面的对象时,再通过DI注入(autowired,resource等注解,xml里bean节点内的ref属性,项目启动的时候会读取xml节点ref属性根据id注入,也会扫描这些注解,根据类型或id注入,id就是对象名)
- 控制反转
没有引入IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。
引入IOC容器之后,对象A与对象B之间失去了直接联系,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。
通过前后对比,不难看出来:对象A获得依赖对象B的过程,由主动行为变成了被动行为,控制权颠倒过来了,这就是“控制反转”这个名词的由来。
全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个ioc,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。
- 依赖注入
获得依赖对象的过程被反转了。控制被反转之后,获得依赖对象的过程由自身管理变成为了由IOC容器主动注入。依赖注入实现了IOC的方法,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中。
零拷贝
零拷贝指的是,应用程序在需要把内核中的一块区域数据转移到另外一块区域去时,不需要经过先复制到用户空间,再转移到目标内核区域去了,而直接实现转移。
RocketMQ 架构设计
RocketMq 事务消息原理
依赖于TransationListener接口
- executeLocalTransaction方法会在发送消息后调用,用于执行本地事务,如果本地事务执行成功,rocket在提交信息
- checkLocalTransation用于对本地事务检查,rocket依赖此方法做补偿
prepare:将消息(消息上带有事务标识)投递到一个名为RMS_SYS_TRANS_HALC_TOPIC的topic中,而不是投递到真正的topic中。
commit/rollback:producer在通过TransactionListeer的executorLocalTransaction方法执行本地事务,当producer的localTranstion处理成功或失败后,producer会向broker发送commit或rollback命令,如果是commit,则broker会将投递到RMQ_SYS_TRANS_HALF_TOPIC中的消息投递到真实的topic中,然后再投递一个标识删除的消息RMQ_SYS_TRANS_HALF_TOPIC中,表示当前事务已经完成。
如果是rollback,则没有投递到真实的topic的过程,只需要投递表示删除的消息到RMQ_SYS_TRANS_OP_HALF_TOPIC。最后,消费者和普通消息一样消费事务消息。
- 第一阶段(prepare)失败:给应用返回消息发送失败
- 事务失败:发送回滚命令给broker,由broker执行消息的回滚
- commit或rollback失败,由broker定时向producer发起事务检查,如果本地事务成功,则提交消息,否则回滚消息式服务。
事务状态的检查有两种情况:
- commit/rollback:broker会执行相应的commit/rollback操作
- 如果是transaction_not_type,则一段事件后会再次检查,当检查的次数超过上限(默认15次)则丢弃消息。
@RocketMQTransactionListener
class TransactionListenerImpl implements RocketMQLocalTransactionListener {@Overridepublic RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {// ... local transaction process, return bollback, commit or unknownSystem.out.println("executeLocalTransaction");return RocketMQLocalTransactionState.UNKNOWN;}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {// ... check transaction status and return bollback, commit or unknownSystem.out.println("checkLocalTransaction");return RocketMQLocalTransactionState.COMMIT;
}
}
RockeMq顺序消息消费原理
默认是不能保证的,需要程序保证发送和消费的是同一个queue,多线程消费也无法保证
发送顺序:发送端自己业务逻辑保证先后,发往一个固定的queue,生产者可以在消费体上设置消息的顺序
发送者实现MessageQueueSeletor接口,选择一个queue进行发送,也可使用rocketmq提供的默认实现。
SelectMessageQueueByHash:按参数的hashcode与可选队列进行求余选择
SelectMessageQueueByRandom:随机选择
mq:queue本身就是顺序追加写,只需保证一个队列同一时间只有一个consumer消费,通过加锁实现,consumer上的顺序消费有一个定时任务,每隔一定时间向broker发送请求延长锁定。
消费端:
pull模式:消费者需要自己维护需要拉取queue,一次拉取的消息都是顺序的,需要消费端自己保证顺序消费。
push模式:消费实例实现自MQPushConsumer接口,提供注册监听的方法消费消息,registerMessageListener、重载方法。
MessageListenerConcurrently:并行消费
MessageListenerOrderly:串行消费,consumer会把消息放入本地队列并加锁,定时任务保证锁的同步。
简述RockerMQ持久化机制
commitLog:日志数据文件,被所有的queue共享,大小为1G,写满之后重新生成,顺序写
consumeQueue:逻辑queue,消息先到达commitLog、然后异步转发到consumeQueue,包含queue在commitLog中的物理位置偏移量Offset,消息实体内容的大小和MessageTag的hash值。大小约为600w个字节,写满之后重新生成,顺序写。
indexFile:通过key或者时间区间来查找CommitLog中的消息,文件名以创建的时间戳命名,固定的单个IndexFile大小为400M,可以保存2000W个索引。
所有队列共用一个日志数据文件,避免了kafka的分区数过多、日志文件过多导致磁盘IO读写压力较大造成的性能瓶颈,rocketmq的queue只存储少量的数据、更加轻量化,对于磁盘的访问是串行化避免磁盘竞争,缺点在于:写入是顺序写,但读是随机的,先读ConsumeQueue,再读CommitLog,会降低消费读的效率。
消息发送到broker后,会被写入commitLog,写之前加锁,保证顺序写入。然后转发到consumeQueue。
消息消费时先从consumeQueue读取消息在CommitLog中的起始物理偏移量Offset,消息大小和消息的tag的HashCode值。在重commitLog读取消息内容。
- 同步刷盘:
消息持久化到磁盘才会给生产者返回ack,可以保证消息可靠,但是会影响性能 - 异步刷盘
消息写入pageCache就返回ack给生产者,刷盘采用异步线程,降低读写延迟提高性能和吞吐。
RocketMQ如何保证不丢消息
生产者:
- 同步阻塞的方式发送消息,加上失败重试机制,可能broker存储失败,可以通过查询确认
- 异步发送需要重写回调方法,检查发送结果
- ack机制,可能存储CommitLog,存储ConsumerQueue失败,此时对消费者不可见
broker:
同步刷盘、集群模式下采用同步复制,会等待slave复制完成才会返回确认。
消费者:
offset手动提交,消息消费保证幂等。
消息发送
生产者向消息队列里写入消息,不同的业务场景需要生产者采用不同的写入策略。比如同步发送,异步发送,Oneway发送,延迟发送,发送事务消息等。默认使用的是DefaultMQProducer类,发送消息要经过五个步骤。
1)设置Producer的GroupName.
2)设置InstanceName,当一个jvm需要启动多个Producer的时候,通过设置不同的InstanceName来区分,不设置的话系统使用默认名称“Default”。
3)设置发送失败重试次数,当网络出现异常的时候,这个次数影响消息的重复投递次数。想保证不丢消息,可以设置重试几次。
4)设置NameServer地址。
5)组装消息并发送。
消息发生返回状态(SendResule#SendStatus)有如下四种
Flush_Dish_timeout
Flush_slave_timeout
Slave_not_available
send_ok
不同状态在不同的刷盘策略和同步策略的配置下含义是不同的。
1,Flush_Dish_Timeout:表示没有在规定时间内完成刷盘(需要Broker的刷盘策略被设置成Sync_flush才会报这个错)。
2,Flush_slave_Timeout:表示在主备方式下,并且Broker被设置成Sync_Master方式,没有在设定时间内完成主从同步。
3,Slave_not_available:这个状态产生的场景和Flush_slave_timeout类似,表示在主备方式下,并且Broker被设置成Sync_master,但是没有找到被设置成slave的broker。
4,Send_ok:表示发送成功,发送成功的具体含义,比如消息是否已经被存储到磁盘?消息是否被同步到Slave上?消息在Slave上是否被写入磁盘?需要结合所配置刷盘策略,主从策略来定。这个状态可以理解为,没有发生上面列出的三个问题状态就是Send_Ok.
写一个高质量的生产者程序,重点在于对发送结果的处理,要充分考虑各种异常,写清对应的处理逻辑。
提升写入性能
发送一条消息处刑曲需要经过三步。
1,客户端发送请求到服务器
2,服务器处理该请求。
3,服务器向客户端返回应答。
一次消息的发送耗时是上述三个步骤的总和。
在一些对速度要求高,但是可靠性要求不高的场景下,比如日志收集类应用,可以采用Oneway方式发送。
Oneway方式发送请求不等待应答,即将数据写入客户端的Socket缓冲区就返回,不等待对方返回结果。
用这种方式发送消息的耗时可以缩短到毫秒级。
另一种提高发送速度的方法是增加Producer的并发量,使用多个Producer同时发送,我们不担心多Producer同时写会降低消息写磁盘的效率,RocketMq引入了一个并发窗口,在窗口内消息可以并发地写入DirectMem中,然后异步地将连续一段无空洞的数据刷入文件系统中。
顺序写CommitLog可以让rocketMq无论在HDD还是SSD磁盘情况下都能保持较高的写入性能。
目前在阿里内部经过调优的服务器上,写入性能达到90w+的tps,我们可以参考这个数据进行系统优化。
消息消费
简单总结消费的几个要点:
1,消息消费方式(pull和push)
2,消息消费的模式(广播模式和集群模式)
3,流量控制(可以结合sentinel来实现,后面单独讲)
4,并发线程数设置
5,消息的过滤(tag,key)taga||tagb||tagc*null
当consumer的处理速度跟不上消息的产生速度,会造成越来越多的消息积压,这个时候可以查看消费逻辑本身有没有优化的空间,除此之外还有三种方法可以提高Consumer的处理能力。
1,提高消费并行度
在同一个ConsumerGroup下(Clustering方式),可以通过增加Consumer实例的数量来提高并行度。
通过加机器,或者在已有的机器启动多个Consumer进程都可以增加Consumer是实例数。
注意:总的consumer数量不要超过topic的read queue数量,超过的consumer实例接收不到消息。
此外,通过提高单个Consumer实例中的并行处理的线程数,可以在同一个Consumer内增加并行度来提高吞吐量(设置方法是修改consumerThreadMin和consumeThreadMax)
2,以批量方式进行消费
某些业务场景下,多条消息同时处理的时间会大大小于逐个处理的时间总和,比如消费消息中涉及update某个数据库,一次update10条的时间会大大小于10次update1条数据的时间。
可以通过批量方式消费来提高消费的吞吐量。实现方法是设置Consumer的consumerMessageBatchMaxSize这个参数,默认是1,如果设置N,在消息多的时候每次收到长度为N的消息链表。
3,检测延时情况,跳过非重要消息
consumer在消费过程中,如果发现由于某种原因发生严重的消息堆积,短时间无法消除堆积,这个时候可以选择丢弃不重要的消息,使Consumer尽快追上Producer的进度。
Mysql
索引的基本原理
索引用来快速地寻找那些具有特定值得记录。如果没有索引,一般来说执行查询时遍历整张表。
索引得原理:就是把无序得数据变成有序得查询。
1,把创建了索引得列得内容进行排序
2,对排序结果生成倒排表
3,在查询得时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体得数据
mysql聚族和非聚族索引的区别
业务系统里面的sql耗时,慢查询
在业务系统中,除了使用主键进行的查询,其他的都会在测试库上测试其耗时,慢查询的统计主要是运维在做,会定期将业务中的慢查询反馈给我们。
慢查询的优化首先要搞明白慢的原因是什么?是查询没有命中索引?是load了不需要的数据列?还是数据量太大?
所以优化是针对这三个方向来的。
首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析和重写。
分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。
如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。
最左前缀原则是什么
当一个SQL想利用索引是,就一定要提供索引所对应的字段中最左边的字段,也就是排在前面的字段,比如针对a,b,c三个字段建立了一个联合索引,那么在写一个sql时就一定要提供a字段的条件,这样才能用到联合索引,这是由于在建立a,b,c三个字段的联合索引时,底层的B+树是按照a,b,c三个字段从左往右去比较大小进行排序,所以如果想利用B+树进行快速查找也得符合这个规则。
explain的type类型
- all 全表扫描
- index 索引全扫描
- range 索引范围扫描,常用语<,<=,>=,between等操作。
- ref 使用非唯一索引或唯一索引前缀扫描,返回单条记录,常出现在关键查询中。
- eq-ref 类似ref,区别在于使用的是唯一索引前缀扫描,返回单条记录,常出现在关联查询中。
- const/system 单条记录,系统会把匹配行中的其他列作为常数处理,如主键或唯一索引查询。
- null mysql不访问任何表或索引,直接返回结果。
explain 的key和key_len,extra
Innodb是如何实现事务的
innodb通过Buffer Pool,LogBuffer,Redo Log,UndoLog来实现事务
1,Innodb在收到一个update语句后,会先根据条件找到数据所在的页,并将该页缓存在Buffer Pool中
2,执行update语句,修改buffer pool中的数据,也就是内存中的数据
3,针对update语句生成一个redolog对象,并存入LogBuffer中
4,针对update语句生成undolog日志,用于事务回滚
5,如果事务提交,那么则把redolog对象进行持久化,后续还有其他机制将buffer pool中所修改的数据页持久化到磁盘中。
6,如果事务回滚,则利用undolog日志进行回滚。
redolog 和 undolog的区别
redolog 重做日志:
确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入到磁盘,在重写mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性
- 为什么需要redolog?
我们都知道,事务的四大特性里面有一个持久性,具体来说就是只要事务提交成功,那么对数据库做的修改就被永久保存下来了,不可能因为任何原因再回到原来状态。
那么mysql是如何保证一致性呢?最简单的做法是在每次事务提交的时候,将该事务涉及修改的数据页全部刷新到磁盘中。但是这么做会有严重的性能问题,主要体现在两个方面。
1,因为innodb是以 页为单位进行磁盘交互的,而一个事务很可能只修改一个数据页里面的几个字节,这个时候将完整的数据页刷到磁盘的话,太浪费资源了
2,一个事务可能涉及修改多个数据页,并且这些数据在物理上并不连续,使用随机IO写入性能太差。
因此mysql设计了redolog ,具体来说就是只记录事务对数据页做了哪些修改,这样就能完美地解决性能问题了。
undolog 回滚日志
保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。
zk的初始化选举和崩溃选举过程
zxid:事务id sid:节点id
先对比zxid,在对比节点id,先投自己,选票内容(zxid,sid),遇强改投。
投票箱:每个节点在本地维护自己和其他节点的投票信息,改投时需要更新信息,并广播节点状态。
- Looking:竞选状态
- Following:随从状态
- Observing:观察状态,同步leader状态,不参与投票
- Leading:领导者状态
初始化选举
- 节点1启动,此时只有一台服务器,它发出去的请求没有任何响应,所以它的选举状态是Looking竞选状态。
- 节点2启动,它与节点1进行通信,互相交换自己的选举结果,由于两者没有历史数据,所以serviceId值较大的节点2胜出,但是由于没有达到半数以上,所以服务器1,2还是继续保持Looking状态。
- 节点3启动,与1,2节点通信交互数据,服务器3成为1,2,3中的leader,此时三台服务器选举了服务器3,所以3成为leader。
- 节点4启动,理论上服务器4应该是1,2,3,4中最大的,但是由于前面已经有半数以上的服务器选举了服务器3,所以它只能切换为follower
- 节点5启动,同4一样
崩溃选举
- 变更状态,leader故障后,follower进入looking状态
- 各节点投票,先投自己(zxid,sid),在广播投票
- 接收到投票,对比zxid和sid,如果本节点小,则将票改为接收到的投票信息,并记录投票信息,重新广播。否则本节点大,不做任何处理
- 统计本地投票信息,超过半数,则切换为leading状态,并广播
zookeeper集群中节点之间数据是如何同步的
1,首先集群启动时,会先进行领导者选举,确定哪个节点时Leader,哪些节点时Follower和Observer
2,然后Leader会和其他节点进行数据同步,采用发送快照和发送Diff日志的方式
3,集群在工作过程中,所有的写请求都会交给Leader节点来进行处理,从节点只能处理读请求。
初始化:没有历史数据,5个节点为例
- 节点1启动,此时只有一台服务器启动,它发出去的请求没有任何响应,所以它的选举状态一直都是Locking状态。
- 节点2启动,它与节点1进行通信,互相交换自己的选举结果,由于两者没有历史数据,所以serverId值较大的服务器2胜出,但是由于没有达到半数以上,所以服务器1,2还是继续保持Looking状态。
- 节点3启动,与1、2节点通信交互数据,服务器3成为服务器1,2,3中的leader,此时有三台服务器选择了3,所以3成为leader。
- 节点4启动,理论上服务器4应该时服务器1,2,3,4中最大的,但是由于前面已经有半数以上的服务器选择了服务器3,所以它只能切换为follower
- 节点5启动,同4一样
崩溃选举
- 变更状态,leader故障后,follower进入looking状态
- 各节点投票,先投自己(zxid,sid),再广播投票
- 接收到投票,对比zxid和Sid,如果本节点小,则将票改为接收投票信息,并记录投票信息,重新广播。否则本节点大,则可不做处理。
- 统计本地投票信息,超过半数,则切换为leading状态并广播
脑裂
一般脑裂都是出现在集群环境中。指的是一个集群环境中出现了多个master节点 ,导致严重的数据问题,数据不一致等等。
出现的原因:可能就是网络环境有问题如断开,假死等等,导致一部分slave节点会重新进入崩溃恢复模式,重新选举新的master节点,然后对外提供事务服务。
例如机房A和机房B通信,一个6个节点,选举机房A的一个节点为master节点。
当机房A和机房B出现网络通信故障,如下图会导致机房重新选举master。在网络恢复后就会出现两个master。
解决办法
1,采用冗余心跳通信
2,采用过半原则
节点之间采用多路心跳来保证单心跳链路的不稳定性。这里着重介绍下过半原则
布隆过滤器
位图:int[10],每个int类型的整数是4*8=32个bit,则int[10]一共有320bit,每个bit非0即1,初始化时都是0
添加数据时,将数据进行hash得到hash值,对应到bit位,将该bit改为1,hash函数可以定义多个,则一个数据添加会将多个(hash函数个数)bit改为1,多个hash函数的目的是减少hash碰撞的概率
查询数据时:hash函数计算得到hash值,对应到bit中,如果有一个为0,则说明数据不在bit中,如果都为1,则该数据可能在bit中。
优点:
- 占用内存小
- 增加和查询元素的时间复杂度为:O(k),k为哈希函数的个数,一般比较小,与数据量大小无关
- 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大的优势
- 数据量很大时,布隆过滤器可以表示全集
- 使用同一组散列函数的布隆过滤器可以进行交、并、查运算
缺点:
- 误判率,即存在假阳性(False Position),不能准确判断元素是否在集合中
- 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素
zk的数据模型
zk的数据模型时一种树形结构,具有一个固定的根节点(/),可以
#Redis
分布式缓存寻址算法
- hash算法:根据key进行hash函数运算、结果对分片取模,确定分片适合固定分片数的场景
扩展分片或者减少分片时,所有数据都需要重新计算分片、存储 - 一致性hash:将整个hash值得区间组织成一个闭合的圆环,计算每台服务器的hash值、映射到圆环中。使用相同的hash算法计算数据的hash值,映射到圆环,顺时针寻找,找到第一个服务器就是数据存储的服务器。
新增或减少节点时只会影响节点到他逆时针最近的一个服务器之间的值,存在hash环倾斜的问题,即服务器分布不均匀,可以通过虚拟节点解决 - hash slot:将数据与服务器隔离开,数据与slot映射,slot与服务器映射,数据进行hash决定存放的slot
新增及删除节点时,将slot进行迁移即可
redis的持久化机制
RDB:redis DataBase 将某一个时刻的内存快照(Snapshot),以二进制的方式写入磁盘。
手动触发:
save命令,使Redis处于阻塞状态,直到RDB持久完成,才会响应其他客户端发来的命令,所以在生产环境一定要慎用。
bgsave命令:fork出一个子进程执行持久化,主进程只在fork过程中有短暂的阻塞,子进程创建之后,主进程就可以响应端请求了。
自动触发:
save m n:在m秒内,如果有n个健发生改变,则自动触发持久化,通过bgsave执行,如果设置多个、只要满足其一就会触发,配置文件有默认配置(可以注释掉)
flushall:用于清空redis所有的数据库,flushdb清空当前redis所在库数据(默认是0号数据库),会清空RDB文件,同时会生成dump.rdb、内容为空。
主从同步:全量同步时会自动触发bgsave命令,生成rdb发送给从节点
优点:
1,整个Redis数据库将只包含dump.rdb,方便持久化。
2,容灾性好,方便备份。
3,性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所有是IO最大化。使用单独子进程进行持久化,主进程不会进行任何IO操作,保证了redis的高性能。
4,相对于数据集大时,比AOF的启动效率更高。
Redis单线程为什么这么快
Redis基于Reactor模式开发了网络事件处理器、文件事件处理器file event handler。它是单线程的,所以Redis才叫做单线程模式,它采用IO多路复用机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理这个事件。可以实现高性能的网络通信模型,又可以跟内部其他单线程模块进行对接,保证了Redis内部的线程模型的简单性。
文件事件处理器的结构包含4个部分:多个Socket、IO多路复用程序、文件事件分派器以及事件处理器(命令请求处理器、命令回复处理器、连接应答处理器等)
多个Socket可能并发的产生不同的事件,IO多路复用程序会监听多个Socket,会将socket放入一个队列中排队,每次从队列中有序、同步取出一个Socket事件分派器,事件分派器把Socket给对应的事件处理器。
然后一个Socket的事件处理完之后,IO多路复用程序才会将队列中的下一个Socket给事件分派器。文件事件分派器会根据每个socket当前产生的事件,来选择对应的事件处理器来处理。
1,Redis启动初始化时,将连接应答处理器跟AE_READABLE事件关联
2,若一个客户端发起连接,会产生一个AE_READBLE事件,然后由连接应答器负责和客户端建立连接,创建客户端对应的socket,同时将这个socket的AE_READBLE事件和命令请求处理器关联,使得客户端可以向主服务器发送命令请求。
3,当客户端向Redis发请求时(不管读还是写请求),客户端socket都会产生一个AE_READABLE事件,触发命令请求处理器。处理器读取客户端的命令内容,然后传给相关程序执行。
4,当Redis服务器准备好给客户端的响应数据后,会将socket的AE_WRITABLE事件和命令回复处理器关联,当客户端准备好读取响应数据时,会在socket产生一个AE_WRITABLE事件,由对应命令回复处理器,即将准备好的响应数据写入socket,供客户端读取。
5,命令回复处理器全部写完到socket后,就会删除该socket的AE_WRITABLE事件和命令回复处理器的映射。
单线程快的原因:
1)纯内存操作
2)核心是基于非阻塞的IO多路复用机制
3)单线程反而避免了多线程的频繁上下文切换带来的性能问题
redis高可用方案
主从
哨兵模式
sentinel,哨兵是redis集群中非常重要的一个组件,主要有以下功能:
- 集群监控:负责监控redis master和slave进程是否正常工作。
- 消息通知:如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
- 故障转移:如果master node挂掉了,会自动转移到slave node上。
- 配置中心:如果故障转移发生了,通知client客户端新的master地址。
哨兵用于实现redis集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
- 故障转移时,判断一个master node是否宕机,需要大部分的哨兵都同意才行,涉及到了分布式选举
- 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的。
- 哨兵通常需要3个实例,来保证自己的健壮性。
- 哨兵+redis主从的部署架构,是不保证数据零丢失的,只能保证保证redis集群的高可用性。
- 对于哨兵+redis主从这种复杂的部署架构。
Redis Cluster
3.0版本开始正式提供。采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询发送的正确的节点上执行。
方案说明:
- 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值)区间数据,默认分配了16384(2的14次方)
- 每份数据分片会存储在多个互为主从的多节点上
- 数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)
- 同一分片多个节点间的数据不保持强一致性
- 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
- 扩容时需要把旧节点的数据迁移一部分到新节点
在redis cluster架构下,每个redis要开放两个端口号,比如一个是6379,另外一个是16379.
16379端口号是用来进行节点间通信的,也就是cluster bus的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus用了另外一种二进制的协议,gossip协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间
优点:
- 无中心架构,支持动态扩容,对业务透明
- 只能使用0号数据库
- 不支持批量操作(pipeline管道操作)
- 分布式逻辑和存储模块耦合等
微服务拆分
微服务拆分坚守哪些指导原则
- 单一服务内部功能高内聚低耦合,也就是说每个服务只完成自己职责内的任务,对于不是自己职责的功能交给其他服务来完成。
- 闭包原则(CCP),微服务的闭包原则就是当我们需要改变一个微服务的时候,所有依赖都在这个微服务的组件内,不需要修改其他微服务。
- 服务自治、接口隔离原则,尽量消除对其他服务的强依赖,这样可以降低沟通成本,提升服务稳定性。服务通过标准的接口隔离,隐藏那个内部实现细节。这使得服务可以独立开发、测试、部署、运行、以服务为单位持续交互。
- 持续演进原则,在服务拆分的初期,你其实很难确定服务究竟要拆成什么样,从微服务这几个字来看,服务的粒度貌似应该足够小,但是服务多了也会带来问题,服务数量快速增长会带来架构复杂度急剧升高,开发、测试、运维等环节很难快速适应,会导致故障率大幅度增加,可用性降低,非必要情况,应逐步拆分,持续演进,避免服务数量的爆炸性增长,这等同于灰度发布的效果,先拿出几个不太重要的功能拆分出一个服务做试验,如果出现故障,则可以减少故障的影响范围。
- 拆分的过程尽量避免影响产品的日常迭代功能,也就是谁要一边做产品功能迭代,一边完成微服务拆分。比如优先剥离比较独立的边界服务(如短信服务等),从非核心的服务出发减少拆分对现有业务的影响。也给团队一个练习、试错的机会。同时当两个服务存在依赖关系时优先拆分被依赖的服务。
- 服务接口的定义要具备可扩展性,服务拆分之后,由于服务是以独立进行的方式部署的,所以服务之间通信就不再是进程内部的方法调用而是跨进程的网络通信。这种通信模型下服务接口的定义要具备可扩展性,否则在服务变更时会造成意向不到的错误。比如微服务的接口因为升级把之前的三分参数改成4个,上线后导致大量报错,推荐做法服务接口的参数类型最好是封装类,这样如果增加参数就不必要变更接口的的参数,而只要在类中添加字段就可以了。
- 阶段性合并,随着你对业务领域理解的逐渐深入或者业务本身逻辑发生了较大的变化,亦或者之前的拆分没有考虑很清楚,导致拆分后的服务边界变得越来越混乱,这时就要重新梳理领域边界,不断纠正拆分的合理性。
- 避免环形依赖与双向依赖,尽量不要有服务之间的环形依赖或者双向依赖,原因是存在这种情况说明我们的功能边界没有划分清楚或者有通用功能没有下沉。
服务拆分的几种方法
纵向拆分
基于业务逻辑拆分
是从业务维度进行拆分。标准是按照业务的关联程度来决定,关联比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合独立拆分为一个微服务。
以社交App为例。你可以认为首页信息流是一个服务,评论是一个服务,消息通知是一个服务,个人主页也是一个服务。
横向拆分
是从公共且独立功能维度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源不与其他业务耦合。
基于可扩展拆分
将系统中的业务模块按照稳定性排序,将已经成熟和改动不大的服务拆分为稳定服务,将经常变化和迭代的服务拆分为变动服务。稳定的服务粒度可以粗一些,即使逻辑上没有强关联的服务,也可以放在同一个子系统中,例如将“日志服务”和“升级服务”放在同一个子系统中;不稳定的服务粒度可以细一些,但也不要太细,始终记住要控制服务的总数量。
基于可靠性拆分
将系统中的业务模块按照优先级排序,将可靠性要求高的核心服务和可靠性要求低的非核心服务拆分开来,然后重点保证核心服务的高可用
避免非核心的业务故障影响核心业务
核心服务高可用方案可以更简单
能够降低高可用成本
基于性能拆分
G1是如何做到对垃圾回收导致系统的停顿是可控的?
其实G1如果要做到这一点,他就必须要追踪每个Region里的回收价值。
回收价值:必须清楚每个Region里的对象有多少垃圾,如果对这个Region 进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾。
总结:G1可以做到让你来设定垃圾回收对系统的影响,他自己通过把内存拆分为大量小Region,以及追踪每个Region,以及追踪每个Region中可以回收的对象大小和预估时间,最后在垃圾回收的时候,尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时间内尽量回收竟可能多的垃圾对象。
如何设定G1对应的内存大小
G1对应的是一大堆的Region内存区域,每个Region的大小是一致的。
问题:到底有多少个Region?每个Region的大小是多少?
其实这个默认情况下是自动计算和设置的,我们可以给整个内存设置一个大小,比如说用“-Xms”和“-Xmx”来设置堆内存的大小。
因为jvm最大可以有2048个region,然后region的大小必须是2的倍数,比如1MB,2MB,4MB之类的
CMS垃圾回收中的“浮动垃圾”的理解
并发清理阶段用户线程还在运行,这段时间就可能产生新的垃圾,新的垃圾在此次GC无法清除,只能等到下次清理。这些垃圾有个专业名词:浮动垃圾。
这个浮动垃圾如何理解?难道不是在本次GC重新标记remark的过程被发现然后清理吗?为何还要等到下次GC才能清理?
remark过程标记活着的对象,从GCRoot的可达性判断对象活着,但无法标记“死亡”的对象。
如果在初始化标记阶段标记为活着,并行运行过程中“死亡”,remarck过程无法纠正,因此变为浮动垃圾,需等待下次gc的到来。
重新标记(Remark)
之前在并发标记时,因为是GC和用户程序是并行执行的,可能导致一部分已经标记为从GC Roots不可达的对象,因为用户程序的(并行)运行又可达了。remark的作用就是将这部分对象又标记为可达对象。
至于浮动垃圾,因为CMS在并发标记时是并发的,GC线程和用户线程并发执行,这个过程当然可能会因为线程交替执行而导致新产生的垃圾(即浮动垃圾)没有被标记到,而重新标记的作用只是修改并发标记所获得的的不可达,所以是没有办法处理“浮动垃圾”的。
TCP和UDP的区别
tcp是面向连接,传输可靠,以字节流的形式传输,传输效率慢。
udp面向无连接,传输不可靠,以数据报文的形式传输,传输效率快。
udp在传输之前不需要建立连接,远地主机在收到UDP报文后,不需要给出任何确认。在某些情况下UDP确是一种最有效的工作方式(一般用于即使通信),比如qq语音,直播
tcp提供面向连接的服务。在传送之前必须先建立连接3次握手,数据传送结束后释放连接。(tcp的可靠性体现在tcp在传输数据之前,会有三次握手来建立连接,而且在数据传送时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源),这一难以避免增加了许多开销,如:确认,流量,计时器以及连接管理。
TCP协议如何保证可靠性传输
应用数据被分割成TCP认为最适合发送的数据块。
tcp给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
校验和:tcp将保持它首部和数据的校验和。这是一个端到端的校验和,目的是检测数据在传输过程中的任何变化。如果收到段的校验和有差错,TCP将丢弃这个报文和不确认收到此报文段和不确认收到此报文段。
流量控制:TCP连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端接收端缓存区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP使用的流量控制协议是可变大小的滑动窗口协议。(TCP利用滑动窗口实现流量控制)
拥塞控制:当网络拥塞时,减少数据的发送。
ARQ协议:也是为了实现可靠性传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。
超时重传:当tcp发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
你还有什么问题要问的吗? 如何做到高质量回答?
问岗位:
这个岗位最大的挑战是什么?
在这个岗位会接触到哪些类型的项目?
请问此岗位所隶属的团队成员有多少人?目前团队的核心工作是哪些?如果我有幸入职,我的工作内容会涉及哪些?
问团队:
这个团队在公司的角色是什么?
团队的基本情况?
可以介绍下跟我的介绍下我的领导吗?
问公司:
员工的晋升机制是什么样的? 我这个岗位的晋升机会如何?
这个岗位所在的团队如何支持公司目标的实现?
有关工作潜力:
这个职位未来一段时间内的职业发展是什么?
软件架构设计原则
开闭原则
开闭原则是指软件实体如类、模块、函数对扩展开放,对修改关闭
强调的是用抽象构建框架,用实现扩展细节。
比如在一个稳定的软件系统中增加新的功能,若对原有的功能进行修改,那么可能带来很多的隐患,甚至降低原系统的稳定性。而如果只是原有功能的基础上进行扩展,将会有效的降低原有功能的影响,且有利于后续的维护。
提高软件系统的可复用性、可维护性。
依赖倒置原则
指的是高层模块不应该直接依赖于底层模块,二者都应该依赖于其抽象。
抽象不依赖于实现,实现依赖于抽象。
具体实现可以通过依赖注入的方式进行使用。
这样可以有效的减少类与类之间的耦合,提高代码的可读性、可维护性,并能够降低修改程序带来的风险。
单一职责原则
尽量避免一种以上可能导致软件中的类、对象、方法进行变更的因素。
可以有效的降低代码的复杂度,提高代码的可读性、可维护性。
接口隔离原则
接口的定义与使用应遵循的原则,尽量使用多个专用的接口,而不是单个大量不相关的方法组成的接口,使实现减少依赖不需要的接口。
符合高内聚、低耦合的设计思想,可以有效的提高代码的可读性,可扩展性、可维护性。
迪米特法则
指一个对象应对其他对象保持最少的了解。
eg:你是一家花店的老板,当你想指导当天的营业额时,无需你自己对当天售出的鲜花价格、数量等进行计算,只需要询问负责收银的员工得到结果,那么就降低了你想得到营业额的复杂度,由对应的人去处理对应的事务。对象之间也是如此,当你想要得到某个结果时,可以使用对应的处理其的对象,而无需自身进行计算,lishi可以有效的降低代码的复杂程度。
可以有效的降低类之间的耦合度
里氏替换原则
所有使用父类的地方都能透明的使用其子类替换,并且不会影响到原有的功能和逻辑。
强调了子类可以对父类的功能进行扩展,但是不能修改原有的功能。
合成复用原则
合成复用原则强调的是尽量使用的对象组合(has-a)或对象聚合(contains-a),而不是使用继承关系来达到复用的目的。
避免滥用继承关系,可以使系统更加的灵活,降低类之间的耦合度,并且在修改其中的类时对其他类的影响会较小。
线程池和CPU核心数的关系
- 一般来说,大家认为线程池的大小经验值应该这样设置:(其中N为CPU processors的个数)
1)如果是CPU密集型应用,则线程池大小设置为N+1(或者N),线程的应用场景:主要是复杂算法
2)如果IO密集型应用,则线程池大小设置为2N+1(或者2N),线程的应用场景:数据库数据的交互,文件上传下载,网络数据传输等等。
+1的原因是:即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能保证CPU的时钟周期不会被浪费。
- 如果一台服务器上只部署这一个应用并且只有这一个线程池,那么这种估算或许合理,具体还需要自行测试验证。
但是,IO优化中,这样的估算方式可能更适合。
最佳线程数目=((线程等待时间+线程CPU时间)/线程CPU时间)*CPU数目。
1)线程等待时间所占比例越高,需要越多线程
2)线程CPU时间所占比例越高,需要越少线程。
比如平均每个线程CPU运行时间为0.5秒,而线程等待时间(非CPU运行时间,比如IO)为1.5s,cpu核心数为8,那么根据上面这个公式估算得到:
((0.5+1.5)/0.5)*8=32。
这个公式进一步转化为:最佳线程数目=(线程等待时间与线程CPU时间之比+1)*CPU数目。
- 1
- 2
- 3
CPU的核心数,CPU的线程数
- cpu的核心数
cpu的核心数是物理上的,也就是硬件上存在着几个核心。
比如,双核就是包括2各相对独立的CPU核心单元组,四核就包含4个相对独立的CPU核心单元组 - cpu的线程数
对于一个CPU,线程数总是大于或等于核心数的
1)一个核心最少对应一个线程,但通过超线程技术,一个核心可以对应两个线程,也就是说它可以同时运行两个线程。
2)CPU之所以要增加线程数,是源于多任务处理的需要。线程数越多,越有利于同时运行多个程序,因为线程数等同于再某个瞬间CPU能同时并行处理的任务数。
eg:
在windows中,在cmd命令中输入“wmic”,然后再出现的新端口中输入“CPU get”即可查看物理CPU数,
其中:
name:表示物理cpu数
numberOfCores:表示CPU核心数
NumberOfLogicalProcessors:表示CPU线程数
1)cpu的线程数概念仅仅只针对Intel的cpu才有用,因为它是通过Intel超线程技术来实现的
2)如果没有超线程技术,一个CPU核心对应一个线程。所以,对于AMD的CPU来说,只有核心数的概念,没有线程数的概念