面试准备
HBase
HBase 基础结构
1. HMaster
- HMaster 是 HBase 主/从集群架构的的中央节点;
- HMaster 将 Region 分配给 RegionServer,协调 RegionServer 的负载并维护集群状态;
- 维护表和 Region 的元数据,不参与数据的输入/输出过程
2. RegionServer
- 维护 HMaster 分配的 Region,并且处理对应 Region 的 I/O 请求
- 负责切分正在运行过程中变的过大的 Region
3. Zookeeper
- Zookeeper 是集群的协调器
- HMaster 在启动的时候把系统表加载在 Zookeeper 上
- 维护 RegionServer 的状态,提供 HBase RegionServer 的状态信息
HBase 写流程
- Client 先访问 Zookeeper,获取表相关的信息,并得到对应的 RegionServer 地址,根据要插入的 Rowkey 获取指定的 RegionServer 的信息(如果为批量提交,会把 Rowkey 根据 HRegionLocation 进行分组);
- Client 对 RegionServer 发起写请求(RegionServer 会进入检查状态,比如当前的 Region 是否处于只读状态,MemoStore 的大小是否超过了 BlockingMemoStoreSize 等等),RegionServer 接受数据写入内存(会依次写入 MemoStore 和 HLog);
- 当 MemStore 大小达到一定的值后,flush 到 StoreFile 并存储到 HDFS 中。如果 StoreFile 达到一定的阈值,会触发 Split 机制,将 Region 一分为二,然后 HMaster 给两个 Region 分配相应的 RegionServer 进行管理,从而分担压力。
- RegionServer:RegionServer上有一个或者多个Region。我们读写的数据就存储在Region上。如果你的HBase是基于HDFS的,那么Region所有数据存取操作都是调用了HDFS的客户端接口来实现的。
- Region:表的一部分数据。HBase是一个会自动分片的数据库。一个Region就相当于关系型数据库中分区表的一个分区,或者MongoDB的一个分片。每一个Region都有起始rowkey和结束rowkey,代表了它所存储的row范围。
- HDFS:Hadoop的一部分。HBase并不直接跟服务器的硬盘交互,而是跟HDFS交互,所以HDFS是真正承载数据的载体。
- HLog:每一个 RegionServer都会有一个HLog示例,并且将操作写在里面。 HLog是WAL(Write-Ahead Log,预写日志)的一个实现实例。WAL是一个保险机制,数据在写到Memstore之前,先被写到WAL了。这样当故障恢复的时候可以从WAL中恢复数据。
HBase 读流程
- Client 先访问 Zookeeper,得到对应的 RegionServer 地址;
- Client 对 RegionServer 发起读请求;
- 当 RegionServer 收到 Client 的读请求的时候,先扫描自己的 Memstore,再扫描 BlockCache(加速读内容缓冲区),如果还没有找到相应的数据,则从 StoreFile 中读取数据,然后将数据返回给 Client。
为什么 Client 只需要访问 Zookeeper?
HMaster 启动的时候会把 Meta 的信息表记录在 Zookeeper 中。这个元数据信息表存储了 HBase 中所有的表,以及 Region 的详细信息。如 Region 开始和结束的 Key,所在的 RegionServer 的地址。HBASE 的 Meta 表相当于一个目录。通过访问 Meta 表可以快速的定位到数据的实际位置,所以读写操作只需要与 Zookeeper 和对应的 RegionServer 进行交互,而 HMaster 只需要负责维护 table 和 Region 的元数据信息,协调各个 RegionServer,也因此减少了 HMaster 的负载。
参考资料
面向对象设计原则
S.O.L.I.D
简写 | 全拼 | 中文翻译 |
---|---|---|
SRP | The Single Responsibility Principle | 单一责任原则 |
OCP | The Open Closed Principle | 开放封闭原则 |
LSP | The Liskov Substitution Principle | 里氏替换原则 |
ISP | The Interface Segregation Principle | 接口分离原则 |
DIP | The Dependency Inversion Principle | 依赖倒置原则 |
1. 单一责任原则
修改一个类的原因应该只有一个。
换句话说就是让一个类只负责一件事,当这个类需要做过多事情的时候,就需要分解这个类。
如果一个类承担的职责过多,就等于把这些职责耦合在了一起,一个职责的变化可能会削弱这个类完成其它职责的能力。
2. 开放封闭原则
类应该对扩展开放,对修改关闭。
扩展就是添加新功能的意思,因此该原则要求在添加新功能时不需要修改代码。
符合开闭原则最典型的设计模式是装饰者模式,它可以动态地将责任附加到对象上,而不用去修改类的代码。
3. 里氏替换原则
子类对象必须能够替换掉所有父类对象。
继承是一种 IS-A 关系,子类需要能够当成父类来使用,并且需要比父类更特殊。
如果不满足这个原则,那么各个子类的行为上就会有很大差异,增加继承体系的复杂度。
4. 接口分离原则
不应该强迫客户依赖于它们不用的方法。
因此使用多个专门的接口比使用单一的总接口要好。
5. 依赖倒置原则
高层模块不应该依赖于低层模块,二者都应该依赖于抽象;
抽象不应该依赖于细节,细节应该依赖于抽象。
高层模块包含一个应用程序中重要的策略选择和业务模块,如果高层模块依赖于低层模块,那么低层模块的改动就会直接影响到高层模块,从而迫使高层模块也需要改动。
依赖于抽象意味着:
- 任何变量都不应该持有一个指向具体类的指针或者引用;
- 任何类都不应该从具体类派生;
- 任何方法都不应该覆写它的任何基类中的已经实现的方法。
Spring MVC 请求流程
- 发起请求到前端控制器 (DispatcherServlet);
- 前端控制器请求 HandlerMapping 查找 Handler (可以根据 xml 配置、注解进行查找);
- 处理器映射器 HandlerMapping 向前端控制器返回 Handler,HandlerMapping 会把请求映射为 HandlerExecutionChain 对象(包含一个 Handler 处理器(页面控制器)对象,多个 HandlerInterceptor 拦截器对象),通过这种策略模式,很容易添加新的映射策略;
- 前端控制器调用处理器适配器去执行 Handler;
- 处理器适配器 HandlerAdapter 将会根据适配的结果去执行 Handler;
- Handler 执行完成给适配器返回 ModelAndView;
- 处理器适配器向前端控制器返回 ModelAndView (ModelAndView 是 Spring MVC 框架的一个底层对象,包括 Model 和 view);
- 前端控制器请求视图解析器去进行视图解析 (根据逻辑视图名解析成真正的视图 (jsp)),通过这种策略很容易更换其他视图技术,只需要更改视图解析器即可;
- 视图解析器向前端控制器返回 View;
- 前端控制器进行视图渲染 (视图渲染将模型数据 (在 ModelAndView 对象中) 填充到 request 域);
- 前端控制器向用户响应结果。
参考资料
Spring IOC
IOC 是 Inversion of Control 的缩写,多数书籍翻译成“控制反转”。
IOC 理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。
软件系统在没有引入 IOC 容器之前,如图 1 所示,对象 A 依赖于对象 B,那么对象 A 在初始化或者运行到某一点的时候,自己必须主动去创建对象 B 或者使用已经创建的对象 B。无论是创建还是使用对象 B,控制权都在自己手上。
软件系统在引入 IOC 容器之后,这种情形就完全改变了,如图所示,由于 IOC 容器的加入,对象 A 与对象 B 之间失去了直接联系,所以,当对象 A 运行到需要对象 B 的时候,IOC 容器会主动创建一个对象 B 注入到对象 A 需要的地方。
通过前后的对比,我们不难看出来:对象 A 获得依赖对象 B 的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。
DI 依赖注入
2004 年,Martin Fowler 探讨了同一个问题,既然 IOC 是控制反转,那么到底是“哪些方面的控制被反转了呢?”,经过详细地分析和论证后,他得出了答案:“获得依赖对象的过程被反转了”。控制被反转之后,获得依赖对象的过程由自身管理变为了由 IOC 容器主动注入。于是,他给“控制反转”取了一个更合适的名字叫做“依赖注入(Dependency Injection)”。他的这个答案,实际上给出了实现 IOC 的方法:注入。所谓依赖注入,就是由 IOC 容器在运行期间,动态地将某种依赖关系注入到对象之中。
IOC 和 DI 的区别
理解以上概念需要搞清以下问题:
- 参与者都有谁?
一般有三个参与者。 1)是某个对象;2)是 IOC/DI 的容器;3)某个对象的外部资源。
其中 1)某个对象指的是任意的,普通的 Java 对象;2)IOC/DI 容器指的是指用于实现 IOC/DI 功能的框架程序;3)对象的完毕资源指的是对象所需要的,但是需要从外部获取的统称为资源;比如一个对象的属性为另外一个对象,或者是对象需要的是一个文件资源等等。 - 依赖: 谁依赖于谁?为什么需要依赖?
对象依赖于 IOC/DI 的容器。 因为对象需要 IOC 来提供对象所需要的外部资源。 - 注入:谁注入谁?到底注入什么?
IOC/DI 容器注入某个对象。 注入某个对象所需要的外部资源。 - 控制反转:谁控制谁?控制了什么?为什么叫反转(有反转就应该有正转)?
是 IOC 容器控制对象,主要是控制了对象实例的创建。
反转是针对正向而言,正向是针对常规下的应用程序而言的。正规应用程序下,如果要在 A 里面使用 C,则会直接创建 C 的对象。,也就是说,是在 A 类中主动去获取所需要的外部资源 C,这种情况被称为正向的。
那么什么是反向呢?就是 A 类不再主动去获取 C,而是被动等待,等待 IoC/DI 的容器获取一个 C 的实例,然后反向的注入到 A 类中。 - 依赖注入和控制反转是同一概念么?
依赖注入和控制反转是对同一件事情的不同描述,从某个方面讲,就是它们描述的角度不同。
依赖注入是从应用程序的角度在描述,可以把依赖注入描述完整点:应用程序依赖容器创建并注入它所需要的外部资源;
而控制反转是从容器的角度在描述。描述完整点:容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源。
Spring AOP
AOP(Aspect Orient Programming),我们一般称为面向方面(切面)编程,作为面向对象的一种补充。它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。
所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。
AOP 常见的使用场景
- Authentication 权限
- Caching 缓存
- Context passing 内容传递
- Error handling 错误处理
- Lazy loading 懒加载
- Debugging 调试
- logging, tracing, profiling and monitoring 记录跟踪 优化 校准
- Performance optimization 性能优化
- Persistence 持久化
- Resource pooling 资源池
- Synchronization 同步
- Transactions 事务
实现原理
Spring AOP 使用的动态代理,所谓的动态代理就是说 AOP 框架不会去修改字节码,而是在内存中临时为方法生成一个 AOP 对象,这个 AOP 对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring AOP 中的动态代理方法主要有两种:
- JDK 动态代理:JDK 动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类。
- CGLIB 动态代理:如果目标类没有实现接口,那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为 final,那么它是无法使用 CGLIB 做动态代理的。
JDK 与 Cglib 代理对比
- JDK 只能针对有接口的类的接口方法进行动态代理。
- Cglib 基于继承实现代理,无法对 static 或者 final 类进行代理。
- Cglib 基于继承实现代理,也无法对 private 或者 static 方法进行代理。
Spring AOP 对两种方法的选择
-
如果目标对象实现了接口,则默认采用 JDK 动态代理。
-
如果目标对象没有实现接口,则默认采用 Cglib 进行动态代理。
-
如果目标对象实现了接口,则强制 Cglib 代理,则采用 Cglib 进行代理。
// 采用注解开启强制代理 @EnableAspectJAutoProxy(proxyTragetClass = true)
参考资料
Spring Bean 的初始化流程
- Spring 对 bean 进行实例化;
- Spring 将值和 bean 的引用注入到 bean 对应的属性中;
- 如果 bean 实现了 BeanNameAware 接口,Spring 将 bean 的 ID 传递给 setBeanName() 方法;
- 如果 bean 实现了 BeanFactoryAware 接口,Spring 将调用 setBeanFactory() 方法,将 BeanFactory 容器实例传入;
- 如果 bean 实现了 ApplicationContextAware 接口,Spring 将调用 setApplicationContext() 方法,将 bean 所在的应用上下文的引用传入进来;
- 如果 bean 实现了 BeanPostProcessor 接口,Spring 将调用它们的 postProcessBeforeInitialization() 方法;
- 如果 bean 实现了 InitializingBean 接口,Spring 将调用它们的 after-PropertiesSet() 方法。类似地,如果 bean 使用 init-method 声明了初始化方法,该方法也会被调用;
- 如果 bean 实现了 BeanPostProcessor 接口,Spring 将调用它们的 post-ProcessAfterInitialization() 方法;
- 此时,bean 已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁;
- 如果 bean 实现了 DisposableBean 接口,Spring 将调用它的 destroy() 接口方法。同样,如果 bean 使用 destroy-method 声明了销毁方法,该方法也会被调用。
/**
* bean的生命周期:
* bean创建---初始化----销毁的过程
* 容器管理bean的生命周期;
* 我们可以自定义初始化和销毁方法;容器在bean进行到当前生命周期的时候来调用我们自定义的初始化和销毁方法
*
* 构造(对象创建)
* 单实例:在容器启动的时候创建对象
* 多实例:在每次获取的时候创建对象
*
* BeanPostProcessor.postProcessBeforeInitialization
* 初始化:
* 对象创建完成,并赋值好,调用初始化方法。。。
* BeanPostProcessor.postProcessAfterInitialization
* 销毁:
* 单实例:容器关闭的时候
* 多实例:容器不会管理这个bean;容器不会调用销毁方法;
*
*
* 遍历得到容器中所有的BeanPostProcessor;挨个执行beforeInitialization,
* 一但返回null,跳出for循环,不会执行后面的BeanPostProcessor.postProcessorsBeforeInitialization
*
* BeanPostProcessor原理
* populateBean(beanName, mbd, instanceWrapper);给bean进行属性赋值
* initializeBean
* {
* applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
* invokeInitMethods(beanName, wrappedBean, mbd);执行自定义初始化
* applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
*}
*
*
*
* 1)、指定初始化和销毁方法;
* 通过@Bean指定init-method和destroy-method;
* 2)、通过让Bean实现InitializingBean(定义初始化逻辑),
* DisposableBean(定义销毁逻辑);
* 3)、可以使用JSR250;
* @PostConstruct:在bean创建完成并且属性赋值完成;来执行初始化方法
* @PreDestroy:在容器销毁bean之前通知我们进行清理工作
* 4)、BeanPostProcessor【interface】:bean的后置处理器;
* 在bean初始化前后进行一些处理工作;
* postProcessBeforeInitialization:在初始化之前工作
* postProcessAfterInitialization:在初始化之后工作
*
* Spring底层对 BeanPostProcessor 的使用;
* bean赋值,注入其他组件,@Autowired,生命周期注解功能,@Async,xxx BeanPostProcessor;
*
* @author lfy
*
*/
Spring 组件注册
Spring 组件注册主要有以下三种方式:
- 包扫描 + 组件标注注解(@Controller/@Service/@Repository/@Component)
缺点: 只能对自定义的类进行注解标注,无法对三方类库中的属性进行组件注册 - @Bean[主要用于三方类库中的组件注册 ]
- @Import[可以快速给容器中导入一个组件,其 Bean 的 id 默认为组件的全类名]
Spring 自动装配
@Autowired
- 默认优先按照类型去容器中找对应的组件:applicationContext.getBean(BookDao.class);找到就赋值
- 如果找到多个相同类型的组件,再将属性的名称作为组件的 id 去容器中查找,如
applicationContext.getBean("bookDao")
- @Qualifier(“bookDao”):使用@Qualifier 指定需要装配的组件的 id,而不是使用属性名
- 自动装配默认一定要将属性赋值好,没有就会报错;可以使用
@Autowired(required=false);
- @Primary:让 Spring 进行自动装配的时候,默认使用首选的 bean;(也可以继续使用@Qualifier 指定需要装配的 bean 的名字)
BookService{
@Autowired
BookDao bookDao;
}
@Autowired 构造器,参数,方法,属性;都是从容器中获取参数组件的值。
- 构造器:如果组件只有一个有参构造方法,则该有参构造器的 @Autowired 可以省略,有参构造器参数的值自动从容器中获取。
- 方法:如果只是 @Bean 注解 + 方法参数,参数的值也会自动从容器中获取。默认不写 @Autowired 效果是一样的;都能自动装配。
- 属性:
@Resource [JSR250]
可以和@Autowired 一样实现自动装配功能;默认是按照组件名称进行装配的;
没有支持@Primary 功能,没有支持 @Autowired(reqiured=false)
@Inject [JSR300]
需要导入 javax.inject 的依赖,和 @Autowired 的功能一样。
<!-- https://mvnrepository.com/artifact/javax.inject/javax.inject -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
没有支持 @Autowired(reqiured=false) 的功能;
自动注入流程
AutowiredAnnotationBeanPostProcessor:解析完成自动装配功能;
容器组件注入
自定义组件想要使用Spring容器底层的一些组件(ApplicationContext,BeanFactory,xxx);
自定义组件实现xxxAware;在创建对象的时候,会调用接口规定的方法注入相关组件;均实现 `` 接口;
把Spring底层一些组件注入到自定义的Bean中;
xxAware 均有对应的 xxxProcessor 进行值的注入;
ApplicationContextAware==》ApplicationContextAwareProcessor;
Java 读取一个文件, 有哪些方法, 考虑性能, 用哪一个类
文件读写主要有以下集中常用的方法:
- 字节读写(InputStream/OutputStream)
- 字符读取(FileReader/FileWriter)
- 行读取(BufferedReader/BufferedWriter)
通过测试 ,可以发现,就写数据而言,BufferedOutputStream
耗时是最短的,而性能FileWriter
最差;读数据方面,BufferedInputStream
性能最好,而FileReader
性能最差劲。
Java OOM
为什么为发生 OOM
原因一般出现为以下两点:
- 分配的少了:比如虚拟机本身可使用的内存(一般通过启动时的 VM 参数指定)太少。
- 应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。
其对应的两个术语为:
- 内存泄露:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。
- 内存溢出:申请的内存超出了 JVM 能提供的内存大小,此时称之为溢出。