JAVA面试汇总第四章 Spring及数据库相关
Spring 核心功能演示 + 面试题Spring Framework 简称 Spring,是 Java 开发中最常用的框架,地位仅次于 Java API,就连近几年比较流行的微服务框架SpringBoot,也是基于 Spring 实现的,SpringBoot 的诞生是为了让开发者更方便地使用 Spring,因此 Spring 在 Java体系中的地位可谓首屈一指。当然,如果想要把 Spring
Spring 核心功能演示 + 面试题
Spring Framework 简称 Spring,是 Java 开发中最常用的框架,地位仅次于 Java API,就连近几年比较流行的微服务框架
SpringBoot,也是基于 Spring 实现的,SpringBoot 的诞生是为了让开发者更方便地使用 Spring,因此 Spring 在 Java
体系中的地位可谓首屈一指。
当然,如果想要把 Spring 所有功能都讲的一清二楚,远远不是一两篇文章能够做到的,但幸运的是,Spring
的基础资料可以很轻易的搜索到,那么我们本讲主要的目的就是把 Spring 中的核心知识点和常见面试题分享给大家,希望对大家能有所帮助。
Spring 介绍
Spring 是一个开源框架,为了解决企业应用程序开发复杂性而创建的,Spring 的概念诞生于 2002 年,于 2003 年正式发布第一个版本
Spring Framework 0.9。下面一起来看 Spring 各个版本的更新特性和它的发展变化吧。
Spring 1.x
此版本主要是为了解决企业应用程序开发复杂性而创建的,当时 J2EE 应用的经典架构是分层架构:表现层、业务层、持久层,最流行的组合就是
SSH(Struts、Spring、Hibernate)。
Spring 1.x 仅支持基于 XML 的配置,确保用户代码不依赖
Spring,它主要包含了以下功能模块:aop、beans、ejb、jdbc、jndi、orm、transation、validation、web 等。
Spring 2.x
Spring 2.x 的改动并不是很大,主要是在 Spring 1.x 的基础上增加了几个新模块,如
ehcache、jms、jmx、scripting、stereotype 等。
Spring 3.x
Spring 3.x 开始不止支持 XML 的配置,还扩展了基于 Java 类的配置,还增加了
Expression、Instructment、Tomcat、oxm 等组件,同时将原来的 Web 细分为:Portlet、Servlet。
Spring 4.x
Spring 4.x 扩充了 Groovy、Messaging、WebMvc、Tiles2、WebSocket 等功能组件,同时 Spring 还适配了
Java 版本,全面支持 Java 8.0、Lambda 表达式等。随着 RESTful 架构风格被越来越多的用户所采用,Spring 4.x 也提供了
RestController 等注解新特性。
Spring 5.x
Spring 5.x 紧跟 Java 相关技术的更新迭代,不断适配 Java 的新版本,同时不断重构优化自身核心框架代码,支持函数式、响应式编程模型等。
Spring 核心
Spring 核心包括以下三个方面:
- 控制反转(Ioc)
- 依赖注入(DI)
- 面向切面编程(AOP)
下面分别来看它的这些特性。
控制反转(IoC)
控制反转(Inversion of
Control,IoC),顾名思义所谓的控制反转就是把创建对象的权利交给框架去控制,而不需要人为地去创建,这样就实现了可插拔式的接口编程,有效地降低代码的耦合度,降低了扩展和维护的成本。
比如,你去某地旅游不再用自己亲自为订购 A 酒店还是 B
酒店而发愁了,只需要把住店的需求告诉给某个托管平台,这个托管平台就会帮你订购一个既便宜又舒适的酒店,而这个帮你订购酒店的行为就可以称之为控制反转。
依赖注入(DI)
依赖注入(Dependency
Injection,DI),是组件之间依赖关系由容器在运行期决定,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
IoC 和 DI 的关系
IoC 是 Spring 中一个极为重要的概念,而 DI 则是实现 IoC 的方法和手段。
接下来,我们来看依赖注入的常见实现方式有哪些?
依赖注入的常见实现方式如下:
- setter 注入
- 构造方法注入
- 注解注入
1)setter 注入
Java 代码:
public class UserController {
// 注入 UserService 对象
private UserService userService;
public void setUserService(UserService userService){
this.userService = userService;
}
}
XML 配置:
Bean 标签的常用属性说明:
- id:为实例化对象起名称,根据 id 值可以得到我们配置的实例化对象,id 属性的名称原则上可以任意命名,但是能包含任何特殊符号;
- class:创建对象所在类的全路径;
- name:功能和 id 属性一样,但是现在一般不用;与 id 的区别在于:name 属性值里可以包含特殊符号,但是 id 不可以;
- scope:一般最常用的有两个值: Singleton:单例模式,整个应用程序,只创建 bean 的一个实例;Prototype:原型模式,每次注入都会创建一个新的 bean 实例,Spring 默认的是单例模式。
2)构造方法注入
Java 代码:
public class UserController {
private UserService userService;
public UserController(UserService userService){
this.userService = userService;
}
}
XML 配置:
3)注解注入
@Controller
public class UserController {
// 使用注解自动注入
@Autowired()
private UserService userService;
// do something
}
// 创建依赖对象
@Service
public class UserService {
// do something
}
创建依赖对象的常见注解:@Component、@Controller、@Service、@Repository。
总结 :可以看出注解的方式要比传统的 XML(setter 和构造器注入)实现注入更为方便,同时注解方式也是官方力推的依赖注入最佳使用方式。
面向切面编程(AOP)
面向切面编程(Aspect Oriented
Programming,AOP),它就好比将系统按照功能分类,每一个类别就是一个“切面”,我们再针对不同的切面制定相应的规则,类似开发模式被称为面向切面编程。
AOP 使用场景
- 日志系统
- 安全统一效验
AOP 优点
- 集中处理某一类问题,方便维护
- 逻辑更加清晰
- 降低模块间的耦合度
AOP 相关概念
- Join point:连接点,程序执行期间的某一个点,例如执行方法或处理异常时候的点,在 Spring AOP 中,连接点总是表示方法的执行。
- Advice:通知,通知分为方法执行前通知,方法执行后通知、环绕通知等。许多 AOP 框架(包括 Spring)都将通知建模为拦截器,在连接点周围维护一系列拦截器(形成拦截器链),对连接点的方法进行增强。
- Pointcut:切点,匹配连接点(Join point)的表达式,是 AOP 的核心,并且 Spring 默认使用 AspectJ 作为切入点表达式语言。
- Aspect:切面,是一个跨越多个类的模块化的关注点,它是通知(Advice)和切点(Pointcut)合起来的抽象,它定义了一个切点(Pointcut)用来匹配连接点(Join point),也就是需要对需要拦截的那些方法进行定义。
- Target object:目标对象,被一个或者多个切面(Aspect)通知的对象,也就是需要被 AOP 进行拦截对方法进行增强(使用通知)的对象,也称为被通知的对象。由于在 AOP 里面使用运行时代理,因而目标对象一直是被代理的对象。
- AOP proxy:AOP 代理,为了实现切面(Aspect)功能使用 AOP 框架创建一个对象,在 Spring 框架里面一个 AOP 代理指的是 JDK 自身的动态代理或 CGLIB 实现的动态代理。
- Weaving:把切面加入到对象,并创建出代理对象的过程。
- Advisor:一个 Advisor 相当于一个小型的切面,不同的是它只有一个通知(Advice),Advisor 在事务管理里面会经常遇到。
AOP 代码实现
AOP 的示例我们就以开车为例,开车的完成流程是这样的:巡视车体及周围情况 → 发动 → 开车 → 熄火 → 锁车。
当然我们的主要目的是“开车”,但在开车之前和开完车之后,我们要做一些其他的工作,这些“其他”的工作,可以理解为 AOP 编程。
1)创建类和方法
package com.learning.aop;
import org.springframework.stereotype.Component;
@Component("person")
public class Person {
public void drive() {
System.out.println("开车");
}
}
2)创建 AOP 拦截
package com.learning.aop;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class CarAop {
@Before("execution(* com.learning.aop.Person.drive())")
public void before() {
System.out.println("巡视车体及周围情况");
System.out.println("发动");
}
@After("execution(* com.learning.aop.Person.drive())")
public void after() {
System.out.println("熄火");
System.out.println("锁车");
}
}
3)XML 配置注入扫描包路径
<?xml version="1.0" encoding="UTF-8"?>
<context:component-scan base-package=“com.learning”/>
aop:aspectj-autoproxy/
4)创建测试类
package com.learning.aop;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class PersonTest {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("applicationContext.xml");
Person landlord = context.getBean("person", Person.class);
landlord.drive();
}
}
运行测试代码,执行结果如下:
巡视车体及周围情况
发动
开车
熄火
锁车
AspectJ 注解说明:
- @Before — 前置通知,在连接点方法前调用;
- @Around — 环绕通知,它将覆盖原有方法,但是允许你通过反射调用原有方法;
- @After — 后置通知,在连接点方法后调用;
- @AfterReturning — 返回通知,在连接点方法执行并正常返回后调用,要求连接点方法在执行过程中没有发生异常;
- @AfterThrowing — 异常通知,当连接点方法异常时调用。
相关面试题
1.@Value 注解的作用是什么?
答:基于 @Value 的注解可以读取 properties 配置文件,使用如下:
@Value(“#{configProperties[‘jdbc.username’]}”)
private String userName;
以上为读取 configProperties 下的 jdbc.username 配置。
2.Spring 通知类型有哪些?
答:Spring 通知类型总共有 5 种:前置通知、环绕通知、后置通知、异常通知、最终通知。
- 前置通知(Before advice):在目标方法执行之前执行的通知。在某连接点( join point )之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)。
- 环绕通知(Around Advice):在目标方法执行之前和之后都可以执行额外代码的通知,也可以选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。
- 后置通知(After (finally) advice):目标方法执行之后(某连接点退出的时候)执行的通知(不论是正常返回还是异常退出)。
- 异常后通知(After throwing advice):在方法抛出异常退出时执行的通知。
- 最终通知(After returning advice):在某连接点(join point)正常完成后执行的通知,例如,一个方法没有抛出任何异常,正常返回。
3.怎么理解 Spring 中的 IOC 容器?
答:Spring IOC
就是把创建对象的权利交给框架去控制,而不需要人为的去创建,这样就实现了可插拔式的接口编程,有效地降低代码的耦合度,降低了扩展和维护的成本。
比如,去某地旅游不再用自己亲自为订购 A 酒店还是 B
酒店而发愁了,只需要把住店的需求告诉给某个托管平台,这个托管平台就会帮你订购一个既便宜又舒适的酒店,而这个帮你订购酒店的行为就可以称之为控制反转。
4.怎么理解 Spring 中的依赖注入?
答:依赖注入是指组件之间的依赖关系由容器在运行期决定,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
5.IoC 和 DI 有什么关系?
答:IoC 是 Spring 中一个极为重要的概念,提供了对象管理的功能,从而省去了人为创建麻烦,而 DI 正是实现 IoC 的方法和手段。
6.@Component 和 @Bean 有什么区别?
答:它们的作用对象不同:@Component 作用于类,而 @Bean 注解作用于方法。
@Component 通常是通过类路径扫描来自动侦测和装配对象到 Spring 容器中,比如 @ComponentScan 注解就是定义扫描路径中的类装配到
Spring 的 Bean 容器中;@Bean 注解是告诉 Spring 这是某个类的实例,当我需要用它的时把它给我,@Bean 注解比
@Component 注解自定义性更强,很多地方我们只能通过 @Bean 注解来注册 Bean,比如当我们引用第三方库中的类需要装配到
Spring容器时,则只能通过 @Bean 来实现,比如以下示例,只能通过 @Bean 注解来实现:
public class WireThirdLibClass {
@Bean
public ThirdLibClass getThirdLibClass() {
return new ThirdLibClass();
}
}
7.Spring 中 bean 的作用域有几种类型?
答:Spring 中 bean 的作用域有四种类型,如下列表:
- 单例(Singleton):整个应用程序,只创建 bean 的一个实例;
- 原型(Prototype):每次注入都会创建一个新的 bean 实例;
- 会话(Session):每个会话创建一个 bean 实例,只在 Web 系统中有效;
- 请求(Request):每个请求创建一个 bean 实例,只在 Web 系统中有效。
Spring 默认的是单例模式。
8.什么是 Spring 的内部 bean?
答:当一个 bean 仅被用作另一个 bean 的属性时,它能被声明为一个内部 bean,为了定义 inner Bean,在 Spring 的基于 XML
的配置元数据中,可以在 <property/>
或 <constructor-arg/>
元素内使用 <bean/>
元素,内部 bean
通常是匿名的,它们的 Scope 一般是 prototype。
9.Spring 注入方式有哪些?
答:Spring 的注入方式包含以下五种:
- setter 注入
- 构造方法注入
- 注解注入
- 静态工厂注入
- 实例工厂注入
其中最常用的是前三种,官方推荐使用的是注解注入,相对使用更简单,维护成本更低,更直观。
10.在 Spring 中如何操作数据库?
答:在 Spring 中操作数据库,可以使用 Spring 提供的 JdbcTemplate 对象,JdbcTemplate
类提供了很多便利的方法,比如把数据库数据转变成基本数据类型或对象,执行自定义的 SQL 语句,提供了自定义的数据错误处理等,JdbcTemplate
使用示例如下:
@Autowired
private JdbcTemplate jdbcTemplate;
// 新增
@GetMapping(“save”)
public String save(){
String sql = “INSERT INTO USER (USER_NAME,PASS_WORD) VALUES (‘laowang’,‘123’)”;
int rows = jdbcTemplate.update(sql);
return “执行成功,影响” + rows + “行”;
}
// 删除
@GetMapping(“del”)
public String del(int id){
int rows= jdbcTemplate.update(“DELETE FROM USER WHERE ID = ?”,id);
return “执行成功,影响” + rows + “行”;
}
// 查询
@GetMapping(“getMapById”)
public Map getMapById(Integer id){
String sql = “SELECT * FROM USER WHERE ID = ?”;
Map map= jdbcTemplate.queryForMap(sql,id);
return map;
}
11.Spring 的 JdbcTemplate 对象和 JDBC 有什么区别?
答:Spring 的 JdbcTemplate 是对 JDBC API 的封装,提供更多的功能和更便利的操作,比如 JdbcTemplate 拥有:
- JdbcTemplate 是线程安全的;
- 实例化操作比较简单,仅需要传递 DataSource;
- 自动完成资源的创建和释放工作;
- 创建一次 JdbcTemplate,到处可用,避免重复开发。
12.Spring 有几种实现事务的方式?
答:Spring 实现事务有两种方式:编程式事务和声明式事务。
编程式事务,使用 TransactionTemplate 或 PlatformTransactionManager 实现,示例代码如下:
private final TransactionTemplate transactionTemplate;
public void add(User user) throws Exception{
// Spring编码式事务,回调机制
transactionTemplate.execute(new TransactionCallback() {
@Override
public Object doInTransaction(TransactionStatus status) {
try {
userMapper.insertSelective(user);
} catch (Exception e) {
// 异常,设置为回滚
status.setRollbackOnly();
throw e;
}
return null;
}
});
}
如果有异常,调用 status.setRollbackOnly() 回滚事务,否则正常执行 doInTransaction() 方法,正常提交事务。
如果事务控制的方法不需要返回值,就可以使用 TransactionCallbackWithoutResult(TransactionCallback
接口的抽象实现类)示例代码如下:
public void add(User user) throws Exception {
// Spring编码式事务,回调机制
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
userMapper.insertSelective(user);
} catch (Exception e) {
// 异常,设置为回滚
status.setRollbackOnly();
throw e;
}
}
});
}
声明式事务,底层是建立在 Spring AOP
的基础上,在方式执行前后进行拦截,并在目标方法开始执行前创建新事务或加入一个已存在事务,最后在目标方法执行完后根据情况提交或者回滚事务。
声明式事务的优点:不需要编程,减少了代码的耦合,在配置文件中配置并在目标方法上添加 @Transactional 注解来实现,示例代码如下:
@Transactional
public void save() {
User user = new User(“laowang”);
userMapper.insertSelective(user);
if (true) {
throw new RuntimeException(“异常”);
}
}
抛出异常,事务会自动回滚,如果方法正常执行,则会自动提交事务。
13.Spring 事务隔离级别有哪些?
答:Spring 的注入方式包含以下五种:
- ISOLATION_DEFAULT:用底层数据库的设置隔离级别,数据库设置的是什么我就用什么;
- ISOLATIONREADUNCOMMITTED:未提交读,最低隔离级别、事务未提交前,就可被其他事务读取(会出现幻读、脏读、不可重复读);
- ISOLATIONREADCOMMITTED:提交读,一个事务提交后才能被其他事务读取到(会造成幻读、不可重复读),SQL server 的默认级别;
- ISOLATIONREPEATABLEREAD:可重复读,保证多次读取同一个数据时,其值都和事务开始时候的内容是一致,禁止读取到别的事务未提交的数据(会造成幻读),MySQL 的默认级别;
- ISOLATION_SERIALIZABLE:序列化,代价最高最可靠的隔离级别,该隔离级别能防止脏读、不可重复读、幻读。
默认值为 ISOLATION_DEFAULT 遵循数据库的事务隔离级别设置。
14.Spring 声明式事务无效可能的原因有哪些?
答:可能的原因如下:
- MySQL 使用的是 MyISAM 引擎,而 MyISAM 是不支持事务的;
- @Transactional 使用在非 public 方法上,@Transactional 注解只能支持 public 级别,其他类型声明的事务不会生效;
- @Transactional 在同一个类中无事务方法 A() 内部调用有事务方法 B(),那么此时 B() 事物不会生效。Spring 中的 AOP 的底层实现原理是什么?
答:Spring AOP 的底层实现原理就是动态代理。Spring AOP 的动态代理有两种实现方式,对于接口使用的是 JDK
自带的动态代理来实现的,而对比非接口使用的是 CGLib 来实现的,关于动态代理的详细内容,可参考前面【反射和动态代理】的那篇文章。
15.Spring 中的 Bean 是线程安全的吗?
答:Spring 中的 Bean 默认是单例模式,Spring 框架并没有对单例 Bean 进行多线程的封装处理,因此默认的情况 Bean
并非是安全的,最简单保证 Bean 安全的举措就是设置 Bean 的作用域为 Prototype(原型)模式,这样每次请求都会新建一个 Bean。
16.说一下 Spring 中 Bean 的生命周期?
答:Spring 中 Bean 的生命周期如下:
- ① 实例化 Bean:对于 BeanFactory 容器,当客户向容器请求一个尚未初始化的 Bean 时,或初始化 Bean 的时候需要注入另一个尚未初始化的依赖时,容器就会调用 createBean 进行实例化。对于 ApplicationContext 容器,当容器启动结束后,通过获取 BeanDefinition 对象中的信息,实例化所有的 Bean;
- ② 设置对象属性(依赖注入):实例化后的对象被封装在 BeanWrapper 对象中,紧接着 Spring 根据 BeanDefinition 中的信息以及通过 BeanWrapper 提供的设置属性的接口完成依赖注入;
- ③ 处理 Aware 接口:Spring 会检测该对象是否实现了 xxxAware 接口,并将相关的 xxxAware 实例注入给 Bean:
- 如果这个 Bean 已经实现了 BeanNameAware 接口,会调用它实现的 setBeanName(String BeanId) 方法,此处传递的就是 Spring 配置文件中 Bean 的 id 值;
- 如果这个 Bean 已经实现了 BeanFactoryAware 接口,会调用它实现的 setBeanFactory() 方法,传递的是 Spring 工厂自身;
- 如果这个 Bean 已经实现了 ApplicationContextAware 接口,会调用 setApplicationContext(ApplicationContext) 方法,传入 Spring 上下文;
- ④ BeanPostProcessor:如果想对 Bean 进行一些自定义的处理,那么可以让 Bean 实现了 BeanPostProcessor 接口,那将会调用 postProcessBeforeInitialization(Object obj, String s) 方法;
- ⑤ InitializingBean 与 init-method:如果 Bean 在 Spring 配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法;
- ⑥ 如果这个 Bean 实现了 BeanPostProcessor 接口,将会调用 postProcessAfterInitialization(Object obj, String s) 方法;由于这个方法是在 Bean 初始化结束时调用的,因而可以被应用于内存或缓存技术;
以上几个步骤完成后,Bean 就已经被正确创建了,之后就可以使用这个 Bean 了。
- ⑦ DisposableBean:当 Bean 不再需要时,会经过清理阶段,如果 Bean 实现了 DisposableBean 这个接口,会调用其实现的 destroy() 方法;
- ⑧ destroy-method:最后,如果这个 Bean 的 Spring 配置中配置了 destroy-method 属性,会自动调用其配置的销毁方法。
17.Spring 有哪些优点?
答:Spring 优点如下:
- 开源免费的热门框架,稳定性高、解决问题成本低;
- 方便集成各种优秀的框架;
- 降低了代码耦合性,通过 Spring 提供的 IoC 容器,我们可以将对象之间的依赖关系交由 Spring 进行控制,避免硬编码所造成的过度程序耦合;
- 方便程序测试,在 Spring 里,测试变得非常简单,例如:Spring 对 Junit 的支持,可以通过注解方便的测试 Spring 程序;
- 降低 Java EE API 的使用难度,Spring 对很多难用的 Java EE API(如 JDBC、JavaMail、远程调用等)提供了一层封装,通过 Spring 的简易封装,让这些 Java EE API 的使用难度大为降低。
18.Spring 和 Struts 的区别?
答:Spring 和 Struts 区别如下:
Spring 特性如下:
- 具备 IOC/DI、AOP 等通用能力,提高研发效率
- 除了支持 Web 层建设以外,还提供了 J2EE 整体服务
- 方便与其他不同技术结合使用,如 Hibernate、MyBatis 等
- Spring 拦截机制是方法级别
Struts 特性如下:
- 是一个基于 MVC 模式的一个 Web 层的处理
- Struts 拦截机制是类级别
19.Spring、SpringBoot、SpringCloud 的区别是什么?
答:它们的区别如下:
- Spring Framework 简称 Spring,是整个 Spring 生态的基础。
- Spring Boot 是一个快速开发框架,让开发者可以迅速搭建一套基于 Spring 的应用程序,并且将常用的 Spring 模块以及第三方模块,如 MyBatis、Hibernate 等都做了很好的集成,只需要简单的配置即可使用,不需要任何的 XML 配置文件,真正做到了开箱即用,同时默认支持 JSON 格式的数据,使用 Spring Boot 进行前后端分离开发也非常便捷。
- Spring Cloud 是一套整合了分布式应用常用模块的框架,使得开发者可以快速实现微服务应用。作为目前非常热门的技术,有关微服务的话题总是在各种场景下被大家讨论,企业的招聘信息中也越来越多地出现对于微服务架构能力的要求。
20.Spring 中都是用了哪些设计模式?
答:Spring 中使用的设计模式如下:
- 工厂模式:通过 BeanFactory、ApplicationContext 来创建 bean 都是属于工厂模式;
- 单例、原型模式:创建 bean 对象设置作用域时,就可以声明 Singleton(单例模式)、Prototype(原型模式);
- 察者模式:Spring 可以定义一下监听,如 ApplicationListener 当某个动作触发时就会发出通知;
- 责任链模式:AOP 拦截器的执行;
- 策略模式:在创建代理类时,如果代理的是接口使用的是 JDK 自身的动态代理,如果不是接口使用的是 CGLIB 实现动态代理。
总结
通过本节内容我们充分的了解了 Spring 的核心:IoC、DI、AOP,也是用代码演示了 Spring 核心功能的示例,其中可以发现的是 Spring
正在从之前的 XML 配置编程变为 Java 注解编程,注解编程让 Spring 更加轻量化简单化了,这一点在我们后面介绍 SpringBoot
的时候,会让你更加感同身受。对于开发者来说,只有真正掌握了 Spring,才能称得上是一名合格的 Java
工程师。当然,学习的目的是为了更好的应用,因此现在就一起动手实践起来吧。
Spring MVC 核心组件 + 面试题
Spring MVC 介绍
Spring MVC(Spring Web MVC)是 Spring Framework 提供的 Web 组件,它的实现基于 MVC
的设计模式:Controller(控制层)、Model(模型层)、View(视图层),提供了前端路由映射、视图解析等功能,让 Java Web
开发变得更加简单,也属于 Java 开发中必须要掌握的热门框架。
执行流程
Spring MVC 的执行流程如下:
1. 客户端发送请求至前端控制器(DispatcherServlet)
2. 前端控制器根据请求路径,进入对应的处理器
3. 处理器调用相应的业务方法
4. 处理器获取到相应的业务数据
5. 处理器把组装好的数据交还给前端控制器
6. 前端控制器将获取的 ModelAndView 对象传给视图解析器(ViewResolver)
7. 前端控制器获取到解析好的页面数据
8. 前端控制器将解析好的页面返回给客户端
流程如下图所示:
核心组件
Spring MVC 的核心组件如下列表所示:
1. **DispatcherServlet** :核心处理器(也叫前端控制器),负责调度其他组件的执行,可降低不同组件之间的耦合性,是整个 Spring MVC 的核心模块。
2. **Handler** :处理器,完成具体业务逻辑,相当于 Servlet 或 Action。
3. **HandlerMapping** :DispatcherServlet 是通过 HandlerMapping 将请求映射到不同的 Handler。
4. **HandlerInterceptor** :处理器拦截器,是一个接口,如果我们需要做一些拦截处理,可以来实现这个接口。
5. **HandlerExecutionChain** :处理器执行链,包括两部分内容,即 Handler 和 HandlerInterceptor(系统会有一个默认的 HandlerInterceptor,如果需要额外拦截处理,可以添加拦截器设置)。
6. **HandlerAdapter** :处理器适配器,Handler 执行业务方法之前,需要进行一系列的操作包括表单数据的验证、数据类型的转换、将表单数据封装到 POJO 等,这一系列的操作,都是由 HandlerAdapter 来完成,DispatcherServlet 通过 HandlerAdapter 执行不同的 Handler。
7. **ModelAndView** :装载了模型数据和视图信息,作为 Handler 的处理结果,返回给 DispatcherServlet。
8. **ViewResolver** :视图解析器,DispatcherServlet 通过它将逻辑视图解析成物理视图,最终将渲染结果响应给客户端。
自动类型转换
自动类型转换指的是,Spring MVC 可以将表单中的字段,自动映射到实体类的对应属性上,请参考以下示例。
1. JSP 页面代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<body>
<form action="add">
名称:<input type="input" name="name"><br>
年龄:<input type="input" name="age"><br>
<input type="submit" value=" 提交 ">
</form>
</body>
</html>
2. 编写实体类
public class PersonDTO {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
3. 编写控制器
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PersonController {
@RequestMapping(value = "/add", produces = "text/plain;charset=utf-8")
public String add(PersonVO person) {
return person.getName() + ":" + person.getAge();
}
}
4. 执行结果
执行结果如下图所示:
中文乱码处理
业务的操作过程中可能会出现中文乱码的情况,以下是处理中文乱码的解决方案。
第一步,在 web.xml 添加编码过滤器,配置如下:
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
第二步,设置 RequestMapping 的 produces 属性,指定返回值类型和编码,如下所示:
@RequestMapping(value = "/add", produces = "text/plain;charset=utf-8")
拦截器
在 Spring MVC 中可以通过配置和实现 HandlerInterceptor 接口,来实现自己的拦截器。
1. 配置全局拦截器
在 Spring MVC 的配置文件中,添加如下配置:
<mvc:interceptors>
<bean class="com.learning.core.MyInteceptor"></bean>
</mvc:interceptors>
2. 添加拦截器实现代码
拦截器的实现代码如下:
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 拦截器
**/
public class MyInteceptor implements HandlerInterceptor {
// 在业务处理器处理请求之前被调用
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
System.out.println("preHandle");
return true;
}
// 在业务处理器处理请求完成之后,生成视图之前执行
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle");
}
// 在 DispatcherServlet 完全处理完请求之后被调用
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion");
}
}
参数验证
1. pom.xml 添加验证依赖包
配置如下:
<!-- Hibernate 参数验证包 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.17.Final</version>
</dependency>
2. 开启注解验证
在 Spring MVC 的配置文件中,添加如下配置信息:
<mvc:annotation-driven />
3. 编写控制器
代码如下:
import com.google.gson.JsonObject;
import com.learning.pojo.PersonDTO;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class PersonController {
@RequestMapping(value = "/check", produces = "text/plain;charset=utf-8")
public String check(@Validated PersonDTO person, BindingResult bindResult) {
// 需要 import com.google.gson.Gson
JsonObject result = new JsonObject();
StringBuilder errmsg = new StringBuilder();
if (bindResult.hasErrors()) {
List<ObjectError> errors = bindResult.getAllErrors();
for (ObjectError error : errors) {
errmsg.append(error.getDefaultMessage());
}
result.addProperty("status", -1);
} else {
result.addProperty("status", 1);
}
result.addProperty("errmsg", errmsg.toString());
return result.toString();
}
}
4. 编写实体类
代码如下:
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
public class PersonDTO {
@NotNull(message = "姓名不能为空")
private String name;
@Min(value = 18,message = "年龄不能低于18岁")
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
更多验证注解,如下所示:
注解 | 运行时检查 |
---|---|
@AssertFalse | 被注解的元素必须为 false |
@AssertTrue | 被注解的元素必须为 true |
@DecimalMax(value) | 被注解的元素必须为一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(Value) | 被注解的元素必须为一个数字,其值必须大于等于指定的最小值 |
@Digits(integer=, fraction=) | 被注解的元素必须为一个数字,其值必须在可接受的范围内 |
@Future | 被注解的元素必须是日期,检查给定的日期是否比现在晚 |
@Max(value) | 被注解的元素必须为一个数字,其值必须小于等于指定的最大值 |
@Min(value) | 被注解的元素必须为一个数字,其值必须大于等于指定的最小值 |
@NotNull | 被注解的元素必须不为 null |
@Null | 被注解的元素必须为 null |
@Past(java.util.Date/Calendar) | 被注解的元素必须过去的日期,检查标注对象中的值表示的日期比当前早 |
@Pattern(regex=, flag=) | 被注解的元素必须符合正则表达式,检查该字符串是否能够在 match 指定的情况下被 regex |
定义的正则表达式匹配 | |
@Size(min=, max=) | 被注解的元素必须在制定的范围(数据类型:String、Collection、Map、Array) |
@Valid | 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个 map,则对其中的值部分进行校验 |
@CreditCardNumber | 对信用卡号进行一个大致的验证 |
被注释的元素必须是电子邮箱地址 | |
@Length(min=, max=) | 被注解的对象必须是字符串的大小必须在制定的范围内 |
@NotBlank | 被注解的对象必须为字符串,不能为空,检查时会将空格忽略 |
@NotEmpty | 被注释的对象必须不为空(数据:String、Collection、Map、Array) |
@Range(min=, max=) | |
被注释的元素必须在合适的范围内(数据:BigDecimal、BigInteger、String、byte、short、int、long 和原始类型的包装类) | |
@URL(protocol=, host=, port=, regexp=, flags=) | 被注解的对象必须是字符串,检查是否是一个有效的 |
URL,如果提供了 protocol、host 等,则该 URL 还需满足提供的条件 |
5. 执行结果
执行结果,如下图所示:
访问 Spring MVC 官方说明文档:http://1t.click/H7a
相关面试题
1. 简述一下 Spring MVC 的执行流程?
答:前端控制器(DispatcherServlet) 接收请求,通过映射从 IoC 容器中获取对应的 Controller 对象和 Method
方法,在方法中进行业务逻辑处理组装数据,组装完数据把数据发给视图解析器,视图解析器根据数据和页面信息生成最终的页面,然后再返回给客户端。
2. POJO 和 JavaBean 有什么区别?
答:POJO 和 JavaBean 的区别如下:
- POJO(Plain Ordinary Java Object)普通 Java 类,具有 getter/setter 方法的普通类都就可以称作 POJO,它是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO。
- JavaBean 是 Java 语言中的一种可重用组件,JavaBean 的构造函数和行为必须符合特定的约定:这个类必须有一个公共的缺省构造函数;这个类的属性使用 getter/setter 来访问,其他方法遵从标准命名规范;这个类应是可序列化的。
简而言之,当一个 POJO 可序列化,有一个无参的构造函数,它就是一个 JavaBean。
3. 如何实现跨域访问?
答:常见的跨域的实现方式有两种:使用 JSONP 或者在服务器端设置运行跨域。服务器运行跨域的代码如下:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MyConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的请求规则
registry.addMapping("/api/**");
}
};
}
}
4. 以下代码描述正确的是?
@RequestMapping(value="/list",params={"age=10"}
public String list(){
// do something
}
A:age 参数不传递的时候,默认值是 10
B:age 参数可以为空
C:age 参数不能为空
D:以上都不对
答:C
题目解析:params={“age=10”} 表示必须包含 age 参数,且值必须等于 10。
5. @RequestMapping 注解的常用属性有哪些?
答:@RequestMapping 常用属性如下:
- value:指定 URL 请求的实际地址,用法:@RequestMapping(value=“/index”);
- method:指定请求的 method 类型,如 GET/POST/PUT/DELETE 等,用法:@RequestMapping(value=“/list”,method=RequestMethod.POST);
- params:指定请求参数中必须包含的参数名称,如果不存在该名称,则无法调用此方法,用法:@RequestMapping(value=“/list”,params={“name”,“age”})。
6. 访问以下接口不传递任何参数的情况下,执行的结果是?
@RequestMapping(value="/list")
@ResponseBody
public String list(int id){
return "id="+id;
}
A:id=0
B:id=
C:页面报错 500
D:id=null
答:C
题目解析:页面报错会提示:可选的参数“id”不能转为 null,因为基本类型不能赋值 null,所以会报错。
7.访问页面时显示 403 代表的含义是?
A:服务器繁忙
B:找不到该页面
C:禁止访问
D:服务器跳转中
答:C
题目解析:常用 HTTP 状态码及对应的含义:
- 400:错误请求,服务器不理解请求的语法
- 401:未授权,请求要求身份验证
- 403:禁止访问,服务器拒绝请求
- 500:服务器内部错误,服务器遇到错误,无法完成请求
- 502:错误网关,服务器作为网关或代理,从上游服务器收到无效响应
- 504:网关超时,服务器作为网关或代理,但是没有及时从上游服务器收到请求
8.forward 和 redirect 有什么区别?
答:forward 和 redirect 区别如下:
- forward 表示请求转发,请求转发是服务器的行为;redirect 表示重定向,重定向是客户端行为;
- forward 是服务器请求资源,服务器直接访问把请求的资源转发给浏览器,浏览器根本不知道服务器的内容是从哪来的,因此它的地址栏还是原来的地址;redirect 是服务端发送一个状态码告诉浏览器重新请求新的地址,因此地址栏显示的是新的 URL;
- forward 转发页面和转发到的页面可以共享 request 里面的数据;redirect 不能共享数据;
- 从效率来说,forward 比 redirect 效率更高。
9. 访问以下接口不传递任何参数的情况下,执行的结果是?
@RequestMapping(value="/list")
@ResponseBody
public String list(Integer id){
return "id="+id;
}
A:id=0
B:id=
C:页面报错 500
D:id=null
答:D
题目解析:包装类可以赋值 null,不会报错。
10. Spring MVC 中如何在后端代码中实现页面跳转?
答:在后端代码中可以使用 forward:/index.jsp 或 redirect:/index.jsp 完成页面跳转,前者 URL
地址不会发生改变,或者 URL 地址会发生改变,完整跳转代码如下:
@RequestMapping("/redirect")
public String redirectTest(){
return "redirect:/index.jsp";
}
11. Spring MVC 的常用注解有哪些?
答:Spring MVC 的常用注解如下:
- @Controller:用于标记某个类为控制器;
- @ResponseBody :标识返回的数据不是 html 标签的页面,而是某种格式的数据,如 JSON、XML 等;
- @RestController:相当于 @Controller 加 @ResponseBody 的组合效果;
- @Component:标识为 Spring 的组件;
- @Configuration:用于定义配置类;
- @RequestMapping:用于映射请求地址的注解;
- @Autowired:自动装配对象;
- @RequestHeader:可以把 Request 请求的 header 值绑定到方法的参数上。
12. 拦截器的使用场景有哪些?
答:拦截器的典型使用场景如下:
- 日志记录:可用于记录请求日志,便于信息监控和信息统计;
- 权限检查:可用于用户登录状态的检查;
- 统一安全处理:可用于统一的安全效验或参数的加密 / 解密等。
13. Spring MVC 如何排除拦截目录?
答:在 Spring MVC 的配置文件中,添加 ,用于排除拦截目录,完整配置的示例代码如下:
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**" />
<!-- 排除拦截地址 -->
<mvc:exclude-mapping path="/api/**" />
<bean class="com.learning.core.MyInteceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
14.@Validated 和 @Valid 有什么区别 ?
答:@Validated 和 @Valid 都用于参数的效验,不同的是:
- @Valid 是 Hibernate 提供的效验机制,Java 的 JSR 303 声明了 @Valid 这个类接口,而 Hibernate-validator 对其进行了实现;@Validated 是 Spring 提供的效验机制,@Validation 是对 @Valid 进行了二次封装,提供了分组功能,可以在参数验证时,根据不同的分组采用不同的验证机制;
- @Valid 可用在成员对象的属性字段验证上,而 @Validated 不能用在成员对象的属性字段验证上,也就是说 @Validated 无法提供嵌套验证。
15.Spring MVC 有几种获取 request 的方式?
答:Spring MVC 获取 request 有以下三种方式:
① 从请求参数中获取
示例代码:
@RequestMapping("/index")
@ResponseBody
public void index(HttpServletRequest request){
// do something
}
该方法实现的原理是 Controller 开始处理请求时,Spring 会将 request 对象赋值到方法参数中。
② 通过 RequestContextHolder上下文获取 request 对象
示例代码:
@RequestMapping("/index")
@ResponseBody
public void index(){
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
// do something
}
③ 通过自动注入的方式
@Controller
public class HomeController{
@Autowired
private HttpServletRequest request; // 自动注入 request 对象
// do something
}
总结
本文我们了解了 Spring MVC 运行的 8 个步骤和它的 8 大核心组件,也尝试了 Spring MVC
方面的类型转换,可将表单自动转换为实体对象,也使用 Hibernate 的验证功能优雅地实现了参数的验证,还可以通过配置和实现
HandlerInterceptor 接口来自定义拦截器,相信有了这些知识,可以帮助我们更高效地开发 Web 和接口项目。
Spring Boot 的创建方式 + 面试题
为什么要用 Spring Boot?
Spring Boot 来自于 Spring 大家族,是 Spring 官方团队(Pivotal 团队)提供的全新框架,它的诞生解决了 Spring
框架使用较为繁琐的问题。Spring Boot 的核心思想是约定优于配置,让开发人员不需要配置任何 XML 文件,就可以像 Maven 整合 Jar
包一样,整合并使用所有框架。
Spring Boot 特性
- 秒级构建一个项目;
- 便捷的对外输出格式,如 REST API、WebSocket、Web 等;
- 简洁的安全集成策略;
- 内嵌容器运行,如 Tomcat、Jetty;
- 强大的开发包,支持热启动;
- 自动管理依赖;
- 自带应用监控。
Spring Boot 2 对系统环境的要求
- Java 8+
- Gradle 4+ or Maven 3.2+
- Tomcat 8+
Spring Boot 使用
在开始之前,我们先来创建一个Spring Boot 项目。
Spring Boot 有两种快速创建的方式:Spring 官网在线网站创建和 IntelliJ IDEA 的 Spring Initializr
创建,下面分别来看。
创建 Spring Boot 项目
1)在线网站创建
在浏览器输入 https://start.spring.io,页面打开如下图所示:
填写相应的项目信息,选择对应的 Spring Boot 和 Java 版本点击 “Generate the project”按钮下载项目压缩文件,解压后用
IDEA 打开即可。
其中 Group 和 Artifact 是 Maven 项目用来确认依赖项目的标识,比如:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.1.6.RELEASE</version>
</dependency>
Group 对应的是配置文件的 groupId,相当于项目的包名;而 Artifact 对应的是配置文件的 artifactId,相当于项目名。
2)使用 IntelliJ IDEA 创建
① 新建项目 → 选择 Spring Initialzr,如下图所示:
② 点击 Next 按钮,填写对应的项目信息(和在线网站创建的字段基本相同),如下图所示:
③ 点击 Next 按钮,选择相应的依赖信息,如下图所示:
④ 点击 Next 按钮,选择项目保存的路径,点击 Finish 创建项目完成,如下图所示:
创建一个 Web 应用
1)pom.xml 中添加 Web 模块的依赖,如下所示:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2)创建后台代码
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping("/index")
public String index(String name) {
return "Hello, " + name;
}
}
3)启动并访问项目
项目的启动类是标识了 @Spring BootApplication 的类,代码如下所示:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringbootlearningApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootlearningApplication.class, args);
}
}
启动并访问 http://localhost:8080/index?name=laowang 效果如下:
到目前为止 Spring Boot 的项目就创建并正常运行了。
设置配置文件
Spring Boot 的配置文件,是 resources 目录下 application.properties 文件,如下图所示:
可以在配置文件中设置很多关于 Spring 框架的配置,格式如下配置所示:
# 项目运行端口
server.port=8086
# 请求编码格式
server.tomcat.uri-encoding=UTF-8
Spring Boot 的其他功能开发和 Spring 相同(Spring Boot 2 是基于 Spring Framework 5
构建的),本文就不过多的介绍了,[感兴趣的朋友可以点击这里查看](https://docs.spring.io/spring-
boot/docs/current/reference/html/)
Spring Boot 发布
Spring Boot 项目的发布方式有两种:
- 内置容器运行
- 外置容器(Tomcat)运行
内置容器运行
1)打包应用
使用窗口命令,在 pom.xml 同级目录下:
mvn clean package -Dmaven.test.skip=true
Dmaven.test.skip=true 表示不执行测试用例,也不编译测试用例类。
2)启动应用
后台启动 Java 程序, 命令如下:
nohup java -jar springbootlearning-0.0.1-SNAPSHOT.jar &
停止程序
首先查询 Java 程序的 pid
ps -ef|grep java
再停止程序
kill -9 pid
操作如下图所示:
扩展内容
指定程序运行日志文件
nohup java -jar springbootlearning-0.0.1-SNAPSHOT.jar 1>>logs 2>>errlog &
其中:
- 1:表示普通日志
- 2:表示错误日志
外置容器(Tomcat)运行
1)排除内置 Tomcat
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
将 scope 属性设置为 provided,表示打包不会包含此依赖。
2)配置启动类
在项目的启动类中继承 Spring BootServletInitializer 并重写 configure() 方法:
@SpringBootApplication
public class PackageApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(PackageApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(PackageApplication.class, args);
}
}
3)打包应用
使用窗口命令,在 pom.xml 同级目录下:
mvn clean package -Dmaven.test.skip=true
4)部署应用
打包完成会在 target 目录下生成:项目名 + 版本号.war 文件,复制到 Tomcat 的 webapps 目录下,运行 Tomcat 即可。
相关面试题
1.Spring Boot 2.0 支持最低的 JDK 版本是?
A:JDK 6
B:JDK 7
C:JDK 8
D:JDK 9
答:C
2.Spring、Spring Boot、Spring Cloud 是什么关系?
答:它们都是来自于 Spring 大家庭,Spring Boot 是在 Spring 框架的基础上开发而来,让更加方便使用 Spring;Spring
Cloud 是依赖于 Spring Boot 而构建的一套微服务治理框架。
3.Spring Boot 项目有哪些优势?
答:Spring Boot 项目优势如下:
- 开发变得简单,提供了丰富的解决方案,快速集成各种解决方案提升开发效率;
- 配置变得简单,提供了丰富的 Starters,集成主流开源产品往往只需要简单的配置即可;
- 部署变得简单,其本身内嵌启动容器,仅仅需要一个命令即可启动项目,结合 Jenkins、Docker 自动化运维非常容易实现;
- 监控变得简单,自带监控组件,使用 Actuator 轻松监控服务各项状态。
4.如何将 Spring Boot 项目打包成 war 包?
答:在 pom.xml 里设置 <packaging>war</packaging>
。
5.在 Maven 项目中如何修改打包名称?
答:在 pom.xml 文件的 build 节点中,添加 finalName 节点并设置为要的名称即可,配置如下:
<build>
<finalName>warName</finalName>
</build>
6.Ant、Maven、Gradle 有什么区别?
答:Ant、Maven、Gradle 是 Java 领域中主要有三大构建工具,它们的区别如下:
- Ant(AnotherNeatTool)诞生于 2000 年,是由 Java 编写,采用 XML 作为构建脚本,这样就允许你在任何环境下运行构建。Ant 是 Java 领域最早的构建工具,不过因为操作复杂,慢慢的已经被淘汰了;
- Maven 诞生于 2004 年,目的是解决程序员使用 Ant 所带来的一些问题,它的好处在于可以将项目过程规范化、自动化、高效化以及强大的可扩展性;
- Gradle 诞生于 2009 年,是一个基于 Apache Ant 和 Apache Maven 概念的项目自动化建构工具。它使用一种基于 Groovy 的特定领域语言来声明项目设置,而不是传统的 XML。结合了前两者的优点,在此基础之上做了很多改进,它具有 Ant 的强大和灵活,又有 Maven 的生命周期管理且易于使用。
Spring Boot 官方支持 Maven 和 Gradle 作为项目构建工具。Gradle 虽然有更好的理念,但是相比 Maven
来讲其行业使用率偏低,并且 Spring Boot 官方默认使用 Maven。
7.Maven 如何设置发布的包名?
答:在 build 节点下设置 finalName 就是发布的包名,如下代码所示:
<build>
<finalName>biapi</finalName>
</build>
8.Spring Boot 热部署有几种方式?
答:Spring Boot 热部署主要有两种方式:Spring Loaded、Spring-boot-devtools。
方式 1:Spring Loaded
在 pom.xml 文件中添加如下依赖:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId>
<version>1.2.6.RELEASE</version>
</dependency>
</dependencies>
<configuration>
<mainClass>此处为入口类</mainClass>
</configuration>
</plugin>
方式 2:Spring-boot-devtools
在 pom.xml 文件中添加如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>
9.Spring Boot 2.0 可以在 Tomcat 7 运行吗?为什么?
答:Spring Boot 2.0 无法在 Tomcat 7 上运行。因为 Spring Boot 2.0 使用的是 Spring Framework
5,Spring Framework 5 使用的是 Servlet 3.1,而 Tomcat 7 最高支持到 Servlet 3.0,所以 Spring
Boot 2.0 无法在 Tomcat 7 上运行。
10.如何使用 Jetty 代替 Tomcat?
答:在 spring-boot-starter-web 移除现有的依赖项,添加 Jetty 依赖,配置如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
11.Spring Boot 不支持以下哪个内嵌容器?
A:Tomcat
B:Jetty
C:Undertow
D:Nginx
答:D
题目解析:Jetty 容器支持如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
Undertow 容器支持如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
12.Spring Boot 中配置文件有几种格式?
答:Spring Boot 中有 .properties 和 .yml 两种配置文件格式,它们主要的区别是书写格式不同。
.properties 配置文件格式如下:
app.user.name = hellojava
.yml 配置文件格式如下:
app:
user:
name: hellojava
13.项目中有两个配置 application.properties 和 application.yml,以下说法正确的是?
A:application.properties 的内容会被忽略,只会识别 application.yml 的内容。
B:两个配置文件同时有效,有相同配置时,以 application.properties 文件为主。
C:application.yml 的内容会被忽略,只会识别 application.properties 的内容。
D:两个配置文件同时有效,有相同配置时,以 application.yml 文件为主。
答:B
14.RequestMapping 和 GetMapping 有什么不同?
答:RequestMapping 和 GetMapping 区别如下:
- RequestMapping 可以支持 GET、POST、PUT 请求;
- GetMapping 是一个组合注解,相当于 @RequestMapping(method = RequestMethod.GET)。
15.以下关于 @RestController 和 @Controller 说法正确的?
A:@Controller 返回 JSON 数据
B:@RestController 返回 JSON 数据
C:@APIController 返回 JSON 数据
D:以上都对
答:B
16.Spring Cache 常用的缓存注解有哪些?
答:Spring Cache 常用注解如下:
- @Cacheable:用来声明方法是可缓存,将结果存储到缓存中以便后续使用相同参数调用时不需执行实际的方法,直接从缓存中取值;
- @CachePut:使用它标注的方法在执行前,不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中;
- CacheEvict:是用来标注在需要清除缓存元素的方法或类上的,当标记在一个类上时表示其中所有方法的执行都会触发缓存的清除操作。
17.Spring Boot Admin 和 Spring Boot Actuator 的关系是什么?
答:Spring Boot Admin 使用了 Spring Boot Actuator 接口进行 UI
美化封装的监控工具,它以图形化的方式查询单个应用的详细状态,也可以使用 Spring Boot Admin 来监控整个集群的状态。
18.如何理解 Spring Boot 中的 Stater?
答:Stater 可以理解为启动器,它是方便开发者快速集成其他框架到 Spring 中的一种技术。比如,spring-boot-starter-data-
jpa 就是把 JPA 快速集成到 Spring 中。
19.常见的 starter 有哪些?
答:常见的 starter 如下:
- spring-boot-starter-web:Web 开发支持
- spring-boot-starter-data-jpa:JPA 操作数据库支持
- spring-boot-starter-data-redis:Redis 操作支持
- spring-boot-starter-data-solr:Solr 权限支持
- mybatis-spring-boot-starter:MyBatis 框架支持
20.Spring Boot Starter JDBC 和 Spring JDBC 有什么关系?
答:spring-boot-starter-jdbc 是 Spring Boot 针对 JDBC 的使用提供了对应的 Starter 包,在 Spring
JDBC 上做了进一步的封装,方便在 Spring Boot 生态中更好的使用 JDBC。
21.Spring Boot 有哪几种读取配置的方式?
答:Spring Boot 可以通过 @Value、@Environment、@ConfigurationProperties 这三种方式来读取。
例如,配置文件内容如下:
app.name=中文
① Value 方式
@Value("${app.name}")
private String appName;
② Environment 方式
public class HelloController {
@Autowired
private Environment environment;
@RequestMapping("/index")
public String index(String hiName) {
// 读取配置文件
String appName = environment.getProperty("app.name");
return "Hello, " + hiName + " |@" + appName;
}
}
③ ConfigurationProperties 方式
@ConfigurationProperties(prefix = "app")
public class HelloController {
// 读取配置文件,必须有 setter 方法
private String name;
public void setName(String name) {
this.name = name;
}
@RequestMapping("/index")
public String index(String hiName) {
System.out.println("appname:" + name);
return "Hello, " + hiName + " |@" + appName;
}
}
22.使用 @Value 读取中文乱码是什么原因?如何处理?
答:这是因为配置文件的编码格式导致的,需要把编码格式设置为 UTF-8,如下图所示:
设置完成之后,重新启动 IDEA 就可以正常显示中文了。
总结
通过本文我们学习了 Spring Boot 的两种创建方式:在线网站创建和 IntelliJ IDEA 方式创建。知道了 Spring Boot
发布的两种方式:内置容器和外置 Tomcat,知道了 Spring Boot 项目特性,以及配置文件 .properties 和 .yml
的差异,掌握了读取配置文件的三种方式:@Value、@Environment、@ConfigurationProperties。
MyBatis 核心组件 + 面试题
MyBatis 介绍
MyBatis 是一款优秀的 ORM(Object Relational
Mapping,对象关系映射)框架,它可以通过对象和数据库之间的映射,将程序中的对象自动存储到数据库中。它是 Apache
提供的一个开源项目,之前的名字叫做 iBatis,2010 年迁移到了 Google Code,并且将名字改为我们现在所熟知的 MyBatis,又于
2013 年 11 月迁移到了 Github。
MyBatis 提供了普通 SQL 查询、事务、存储过程等功能,它的优缺点如下。
优点 :
- 相比于 JDBC 需要编写的代码更少
- 使用灵活,支持动态 SQL
- 提供映射标签,支持对象与数据库的字段关系映射
缺点 :
- SQL 语句依赖于数据库,数据库移植性差
- SQL 语句编写工作量大,尤其在表、字段比较多的情况下
总体来说,MyBatis 是一个非常优秀和灵活的数据持久化框架,适用于需求多变的互联网项目,也是当前主流的 ORM 框架。
MyBatis 重要组件
MyBatis 中的重要组件如下:
- Mapper 配置:用于组织具体的查询业务和映射数据库的字段关系,可以使用 XML 格式或 Java 注解格式来实现;
- Mapper 接口:数据操作接口也就是通常说的 DAO 接口,要和 Mapper 配置文件中的方法一一对应;
- Executor:MyBatis 中所有的 Mapper 语句的执行都是通过 Executor 执行的;
- SqlSession:类似于 JDBC 中的 Connection,可以用 SqlSession 实例来直接执行被映射的 SQL 语句;
- SqlSessionFactory:SqlSessionFactory 是创建 SqlSession 的工厂,可以通过 SqlSession openSession() 方法创建 SqlSession 对象。
MyBatis 执行流程
MyBatis 完整执行流程如下图所示:
MyBatis 执行流程说明:
1. 首先加载 Mapper 配置的 SQL 映射文件,或者是注解的相关 SQL 内容。
2. 创建会话工厂,MyBatis 通过读取配置文件的信息来构造出会话工厂(SqlSessionFactory)。
3. 创建会话,根据会话工厂,MyBatis 就可以通过它来创建会话对象(SqlSession),会话对象是一个接口,该接口中包含了对数据库操作的增、删、改、查方法。
4. 创建执行器,因为会话对象本身不能直接操作数据库,所以它使用了一个叫做数据库执行器(Executor)的接口来帮它执行操作。
5. 封装 SQL 对象,在这一步,执行器将待处理的 SQL 信息封装到一个对象中(MappedStatement),该对象包括 SQL 语句、输入参数映射信息(Java 简单类型、HashMap 或 POJO)和输出结果映射信息(Java 简单类型、HashMap 或 POJO)。
6. 操作数据库,拥有了执行器和 SQL 信息封装对象就使用它们访问数据库了,最后再返回操作结果,结束流程。
MyBatis XML 版
MyBatis 使用分为两个版本:XML 版和 Java 注解版。接下来我们使用 Spring Boot 结合 MyBatis 的 XML
版,来实现对数据库的基本操作,步骤如下。
1)创建数据表
drop table if exists `t_user`;
create table `t_user` (
`id` bigint(20) not null auto_increment comment '主键id',
`username` varchar(32) default null comment '用户名',
`password` varchar(32) default null comment '密码',
`nick_name` varchar(32) default null,
primary key (`id`)
) engine=innodb auto_increment=1 default charset=utf8;
2)添加依赖
在项目添加对 MyBatis 和 MySQL 支持的依赖包,在 pom.xml 文件中添加如下代码:
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
mybatis-spring-boot-starter 是 MyBatis 官方帮助我们快速集成 Spring Boot 提供的一个组件包,mybatis-
spring-boot-starter 2.1.0 对应 MyBatis 的版本是 3.5.2。
3)增加配置文件
在 application.yml 文件中添加以下内容:
spring:
datasource:
url: jdbc:mysql://localhost:3306/learndb?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
type-aliases-package: com.interview.mybatislearning.model
其中:
- mybatis.config-location:配置 MyBatis 基础属性;
- mybatis.mapper-locations:配置 Mapper 对应的 XML 文件路径;
- mybatis.type-aliases-package:配置项目中实体类包路径。
注:如果配置文件使用的是 application.properties,配置内容是相同的,只是内容格式不同。
4)创建实体类
public class UserEntity implements Serializable {
private static final long serialVersionUID = -5980266333958177104L;
private Integer id;
private String userName;
private String passWord;
private String nickName;
public UserEntity(String userName, String passWord, String nickName) {
this.userName = userName;
this.passWord = passWord;
this.nickName = nickName;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassWord() {
return passWord;
}
public void setPassWord(String passWord) {
this.passWord = passWord;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
}
5)创建 XML 文件
mybatis-config.xml (基础配置文件):
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<typeAlias alias="Integer" type="java.lang.Integer"/>
<typeAlias alias="Long" type="java.lang.Long"/>
<typeAlias alias="HashMap" type="java.util.HashMap"/>
<typeAlias alias="LinkedHashMap" type="java.util.LinkedHashMap"/>
<typeAlias alias="ArrayList" type="java.util.ArrayList"/>
<typeAlias alias="LinkedList" type="java.util.LinkedList"/>
</typeAliases>
</configuration>
mybatis-config.xml 主要是为常用的数据类型设置别名,用于减少类完全限定名的长度,比如:resultType="Integer"
完整示例代码如下:
<select id="getAllCount" resultType="Integer">
select
count(*)
from t_user
</select>
UserMapper.xml (业务配置文件):
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.interview.mybatislearning.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.interview.mybatislearning.model.UserEntity" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="username" property="userName" jdbcType="VARCHAR" />
<result column="password" property="passWord" jdbcType="VARCHAR" />
<result column="nick_name" property="nickName" jdbcType="VARCHAR" />
</resultMap>
<sql id="Base_Column_List" >
id, username, password, nick_name
</sql>
<sql id="Base_Where_List">
<if test="userName != null and userName != ''">
and userName = #{userName}
</if>
</sql>
<select id="getAll" resultMap="BaseResultMap" >
SELECT
<include refid="Base_Column_List" />
FROM t_user
</select>
<select id="getOne" parameterType="Long" resultMap="BaseResultMap" >
SELECT
<include refid="Base_Column_List" />
FROM t_user
WHERE id = #{id}
</select>
<insert id="insert" parameterType="com.interview.mybatislearning.model.UserEntity" >
INSERT INTO
t_user
(username,password,nick_name)
VALUES
(#{userName}, #{passWord}, #{nickName})
</insert>
<update id="update" parameterType="com.interview.mybatislearning.model.UserEntity" >
UPDATE
t_user
SET
<if test="userName != null">username = #{userName},</if>
<if test="passWord != null">password = #{passWord},</if>
nick_name = #{nickName}
WHERE
id = #{id}
</update>
<delete id="delete" parameterType="Long" >
DELETE FROM
t_user
WHERE
id =#{id}
</delete>
</mapper>
以上配置我们增加了增删改查等基础方法。
6)增加 Mapper 文件
此步骤我们需要创建一个与 XML 对应的业务 Mapper 接口,代码如下:
public interface UserMapper {
List<UserEntity> getAll();
UserEntity getOne(Long id);
void insert(UserEntity user);
void update(UserEntity user);
void delete(Long id);
}
7)添加 Mapper 包扫描
在启动类中添加 @MapperScan,设置 Spring Boot 启动的时候会自动加载包路径下的 Mapper。
@SpringBootApplication
@MapperScan("com.interview.mybatislearning.mapper")
public class MyBatisLearningApplication {
public static void main(String[] args) {
SpringApplication.run(MyBatisLearningApplication.class, args);
}
}
8)编写测试代码
经过以上步骤之后,整个 MyBatis 的集成就算完成了。接下来我们写一个单元测试,验证一下。
@RunWith(SpringRunner.class)
@SpringBootTest
public class MybatislearningApplicationTests {
@Resource
private UserMapper userMapper;
@Test
public void testInsert() {
userMapper.insert(new UserEntity("laowang", "123456", "老王"));
Assert.assertEquals(1, userMapper.getAll().size());
}
}
总结
通过本文我们知道 MyBatis 是一个优秀和灵活的数据持久化框架,MyBatis 包含 Mapper 配置、Mapper
接口、Executor、SqlSession、SqlSessionFactory 等几个重要的组件,知道了 MyBatis 基本流程:MyBatis
首先加载 Mapper 配置和 SQL 映射文件,通过创建会话工厂得到 SqlSession 对象,再执行 SQL 语句并返回操作信息。我们也使用 XML
的方式,实现了 MyBatis 对数据库的基础操作。
对数据库的基本操作步骤 + 面试题
MyBatis 最初的设计是基于 XML 配置文件的,但随着 Java 的发展(Java 1.5 开始引入注解)和 MyBatis 自身的迭代升级,终于在
MyBatis 3 之后就开始支持基于注解的开发了。
下面我们使用 Spring Boot + MyBatis 注解的方式,来实现对数据库的基本操作,具体实现步骤如下。
MyBatis 注解版
1)创建数据表
drop table if exists `t_user`;
create table `t_user` (
`id` bigint(20) not null auto_increment comment '主键id',
`username` varchar(32) default null comment '用户名',
`password` varchar(32) default null comment '密码',
`nick_name` varchar(32) default null,
primary key (`id`)
) engine=innodb auto_increment=1 default charset=utf8;
2)添加依赖
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
3)增加配置文件
在 application.yml 文件中添加以下内容:
spring:
datasource:
url: jdbc:mysql://localhost:3306/learndb?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
type-aliases-package: com.interview.model
4)创建实体类
public class UserEntity implements Serializable {
private static final long serialVersionUID = -5980266333958177105L;
private Integer id;
private String userName;
private String passWord;
private String nickName;
public UserEntity(String userName, String passWord, String nickName) {
this.userName = userName;
this.passWord = passWord;
this.nickName = nickName;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassWord() {
return passWord;
}
public void setPassWord(String passWord) {
this.passWord = passWord;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
}
5)增加 Mapper 文件
public interface UserMapper {
@Select("select * from t_user")
@Results({
@Result(property = "nickName", column = "nick_name")
})
List<UserEntity> getAll();
@Select("select * from t_user where id = #{id}")
@Results({
@Result(property = "nickName", column = "nick_name")
})
UserEntity getOne(Long id);
@Insert("insert into t_user(username,password,nick_name) values(#{userName}, #{passWord}, #{nickName})")
void insert(UserEntity user);
@Update("update t_user set username=#{userName},nick_name=#{nickName} where id =#{id}")
void update(UserEntity user);
@Update({"<script> ",
"update t_user ",
"<set>",
" <if test='userName != null'>userName=#{userName},</if>",
" <if test='nickName != null'>nick_name=#{nickName},</if>",
" </set> ",
"where id=#{id} ",
"</script>"})
void updateUserEntity(UserEntity user);
@Delete("delete from t_user where id =#{id}")
void delete(Long id);
}
使用 @Select、@Insert、@Update、@Delete、@Results、@Result 等注解来替代 XML 配置文件。
6)添加 Mapper 包扫描
在启动类中添加 @MapperScan,设置 Spring Boot 启动的时候会自动加载包路径下的 Mapper。
@SpringBootApplication
@MapperScan("com.interview.mapper")
public class MybatisApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisApplication.class, args);
}
}
7)编写测试代码
@RunWith(SpringRunner.class)
@SpringBootTest
public class MybatisApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
public void testInsert() {
userMapper.insert(new UserEntity("laowang", "123456", "老王"));
Assert.assertEquals(1, userMapper.getAll().size());
}
}
相关面试题
1.MyBatis 有哪些优缺点?
答:MyBatis 优缺点如下:
优点:
- 相比于 JDBC 需要编写的代码更少
- 使用灵活,支持动态 SQL
- 提供映射标签,支持对象与数据库的字段关系映射
缺点:
- SQL 语句依赖于数据库,数据库移植性差
- SQL 语句编写工作量大,尤其在表、字段比较多的情况下
总体来说,MyBatis 是一个非常不错的持久层解决方案,它专注于 SQL 本身,非常灵活,适用于需求变化较多的互联网项目,也是当前国内主流的 ORM
框架。
2.以下不属于 MyBatis 优点的是?
A:可以灵活的编辑 SQL 语句
B:很好的支持不同数据库之间的迁移
C:能够很好的和 Spring 框架集成
D:提供映射标签支持对象和数据库的字段映射
答:B
题目解析:因为 MyBatis 需要自己编写 SQL 语句,但每个数据库的 SQL 语句有略有差异,所以 MyBatis
不能很好的支持不同数据库之间的迁移。
3.MyBatis 和 Hibernate 有哪些不同?
答:MyBatis 和 Hibernate 都是非常优秀的 ORM 框架,它们的区别如下:
- 灵活性:MyBatis 更加灵活,自己可以写 SQL 语句,使用起来比较方便;
- 可移植性:MyBatis 有很多自己写的 SQL,因为每个数据库的 SQL 可以不相同,所以可移植性比较差;
- 开发效率:Hibernate 对 SQL 语句做了封装,让开发者可以直接使用,因此开发效率更高;
- 学习和使用门槛:MyBatis 入门比较简单,使用门槛也更低。
4.“#”和“$
”有什么区别?
答:“#”是预编译处理,“$”是字符替换。 在使用“#”时,MyBatis 会将 SQL 中的参数替换成“?”,配合 PreparedStatement 的
set 方法赋值,这样可以有效的防止 SQL 注入,保证程序的运行安全。
5.在 MyBatis 中怎么解决实体类属性名和表字段名不一致的问题?
答:通常的解决方案有以下两种方式。
① 在 SQL 语句中重命名为实体类的属性名,可参考以下配置:
<select id="selectorder" parametertype="int" resultetype="com.interview.order">
select order_id id, order_no orderno form order where order_id=#{id};
</select>
② 通过 <resultMap>
映射对应关系,可参考以下配置:
<resultMap id="BaseResultMap" type="com.interview.mybatislearning.model.UserEntity" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="username" property="userName" jdbcType="VARCHAR" />
<result column="password" property="passWord" jdbcType="VARCHAR" />
<result column="nick_name" property="nickName" jdbcType="VARCHAR" />
</resultMap>
<select id="getAll" resultMap="BaseResultMap">
select * from t_user
</select>
6.在 MyBatis 中如何实现 like 查询?
答:可以在 Java 代码中添加 SQL 通配符来实现 like 查询,这样也可以有效的防治 SQL 注入,具体实现如下:
Java 代码:
String name = "%wang%":
List<User> list = mapper.likeName(name);
Mapper 配置:
<select id="likeName">
select * form t_user where name like #{name};
</select>
7.MyBatis 有几种分页方式?
答:MyBatis 的分页方式有以下两种:
- 逻辑分页,使用 MyBatis 自带的 RowBounds 进行分页,它是一次性查询很多数据,然后在数据中再进行检索;
- 物理分页,自己手写 SQL 分页或使用分页插件 PageHelper,去数据库查询指定条数的分页数据形式。
8.RowBounds 是一次性查询全部结果吗?为什么?
答:RowBounds 表面是在“所有”数据中检索数据,其实并非是一次性查询出所有数据。因为 MyBatis 是对 JDBC 的封装,在 JDBC
驱动中有一个 Fetch Size 的配置,它规定了每次最多从数据库查询多少条数据,假如你要查询更多数据,它会在执行 next()
的时候,去查询更多的数据。 就好比你去自动取款机取 10000 元,但取款机每次最多能取 2500 元,要取 4 次才能把钱取完。对于 JDBC
来说也是一样,这样做的好处是可以有效的防止内存溢出。
9.为什么阿里巴巴不允许使用 HashMap 或 Hashtable 作为查询结果集直接输出?
答:因为使用 HashMap 或 Hashtable 作为查询结果集直接输出,会导致值类型不可控,给调用人员造成困扰,给系统带来更多不稳定的因素。
10.什么是动态 SQL?
答:动态 SQL 是指可以根据不同的参数信息来动态拼接的不确定的 SQL 叫做动态 SQL,MyBatis 动态 SQL
的主要元素有:if、choose/when/otherwise、trim、where、set、foreach 等。 以 if 标签的使用为例:
<select id="findUser" parameterType="com.interview.entity.User" resultType="com.interview.entity.User">
select * from t_user where
<if test="id!=null">
id = #{id}
</if>
<if test="username!=null">
and username = #{username}
</if>
<if test="password!=null">
and password = #{password}
</if>
</select>
11.为什么不建议在程序中滥用事务?
答:因为事务的滥用会影响数据的 QPS(每秒查询率),另外使用事务的地方还要考虑各方面回滚的方案,如缓存回滚、搜索引擎回滚、消息补偿、统计修正等。
12.如何开启 MyBatis 的延迟加载?
答:只需要在 mybatis-config.xml 设置 <setting name="lazyLoadingEnabled" value="true"/>
即可打开延迟缓存功能,完整配置文件如下:
<configuration>
<settings>
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
</configuration>
13.什么是 MyBatis 的一级缓存和二级缓存?
答:MyBatis 缓存如下:
- 一级缓存是 SqlSession 级别的,是 MyBatis 自带的缓存功能,并且无法关闭,因此当有两个 SqlSession 访问相同的 SQL 时,一级缓存也不会生效,需要查询两次数据库;
- 二级缓存是 Mapper 级别的,只要是同一个 Mapper,无论使用多少个 SqlSession 来操作,数据都是共享的,多个不同的 SqlSession 可以共用二级缓存,MyBatis 二级缓存默认是关闭的,需要使用时可手动开启,二级缓存也可以使用第三方的缓存,比如,使用 Ehcache 作为二级缓存。
手动开启二级缓存,配置如下:
<configuration>
<settings>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
</settings>
</configuration>
14.如何设置 Ehcache 为 MyBatis 的二级缓存?
答:可直接在 XML 中配置开启 EhcacheCache,代码如下:
<mapper namespace="com.interview.repository.ClassesReposirory">
<!-- 开启二级缓存 -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache" >
<!-- 缓存创建以后,最后一次访问缓存的时间至失效的时间间隔 -->
<property name="timeToIdleSeconds" value="3600"/>
<!-- 缓存自创建时间起至失效的时间间隔-->
<property name="timeToLiveSeconds" value="3600"/>
<!-- 缓存回收策略,LRU 移除近期最少使用的对象 -->
<property name="memoryStoreEvictionPolicy" value="LRU"/>
</cache>
<select id="findById" parameterType="java.lang.Long" resultType="com.interview.entity.Classes">
select * from classes where id = #{id}
</select>
</mapper>
15.MyBatis 有哪些拦截器?如何实现拦截功能?
答:MyBatis 提供的连接器有以下 4 种。
- Executor:拦截内部执行器,它负责调用 StatementHandler 操作数据库,并把结果集通过 ResultSetHandler 进行自动映射,另外它还处理了二级缓存的操作。
- StatementHandler:拦截 SQL 语法构建的处理,它是 MyBatis 直接和数据库执行 SQL 脚本的对象,另外它也实现了 MyBatis 的一级缓存。
- ParameterHandler:拦截参数的处理。
- ResultSetHandler:拦截结果集的处理。
拦截功能具体实现如下:
@Intercepts({@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class TestInterceptor implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget(); //被代理对象
Method method = invocation.getMethod(); //代理方法
Object[] args = invocation.getArgs(); //方法参数
// 方法拦截前执行代码块
Object result = invocation.proceed();
// 方法拦截后执行代码块
return result;
}
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}
总结
通过本文可以看出 MyBatis 注解版和 XML 版的主要区别是 Mapper 中的代码,注解版把之前在 XML 的 SQL 实现,全部都提到
Mapper 中了,这样就省去了配置 XML 的麻烦。
消息队列面试题汇总
1.消息队列的应用场景有哪些?
答:消息队列的应用场景如下。
- 应用解耦,比如,用户下单后,订单系统需要通知库存系统,假如库存系统无法访问,则订单减库存将失败,从而导致订单失败。订单系统与库存系统耦合,这个时候如果使用消息队列,可以返回给用户成功,先把消息持久化,等库存系统恢复后,就可以正常消费减去库存了。
- 削峰填谷,比如,秒杀活动,一般会因为流量过大,从而导致流量暴增,应用挂掉,这个时候加上消息队列,服务器接收到用户的请求后,首先写入消息队列,假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面。
- 日志系统,比如,客户端负责将日志采集,然后定时写入消息队列,消息队列再统一将日志数据存储和转发。
2.RabbitMQ 有哪些优点?
答:RabbitMQ 的优点如下:
- 可靠性,RabbitMQ 的持久化支持,保证了消息的稳定性;
- 高并发,RabbitMQ 使用了 Erlang 开发语言,Erlang 是为电话交换机开发的语言,天生自带高并发光环和高可用特性;
- 集群部署简单,正是因为 Erlang 使得 RabbitMQ 集群部署变的非常简单;
- 社区活跃度高,因为 RabbitMQ 应用比较广泛,所以社区的活跃度也很高;
- 解决问题成本低,因为资料比较多,所以解决问题的成本也很低;
- 支持多种语言,主流的编程语言都支持,如 Java、.NET、PHP、Python、JavaScript、Ruby、Go 等;
- 插件多方便使用,如网页控制台消息管理插件、消息延迟插件等。
3.RabbitMQ 有哪些重要的角色?
答:RabbitMQ 包含以下三个重要的角色:
- 生产者:消息的创建者,负责创建和推送数据到消息服务器;
- 消费者:消息的接收方,用于处理数据和确认消息;
- 代理:就是 RabbitMQ 本身,用于扮演“快递”的角色,本身不生产消息,只是扮演“快递”的角色。
4.RabbitMQ 有哪些重要的组件?它们有什么作用?
答:RabbitMQ
包含的重要组件有:ConnectionFactory(连接管理器)、Channel(信道)、Exchange(交换器)、Queue(队列)、RoutingKey(路由键)、BindingKey(绑定键)
等重要的组件,它们的作用如下:
- ConnectionFactory(连接管理器):应用程序与 RabbitMQ 之间建立连接的管理器,程序代码中使用;
- Channel(信道):消息推送使用的通道;
- Exchange(交换器):用于接受、分配消息;
- Queue(队列):用于存储生产者的消息;
- RoutingKey(路由键):用于把生成者的数据分配到交换器上;
- BindingKey(绑定键):用于把交换器的消息绑定到队列上。
运行流程,如下图所示:
5.什么是消息持久化?
答:消息持久化是把消息保存到物理介质上,以防止消息的丢失。
6.RabbitMQ 要实现消息持久化,需要满足哪些条件?
答:RabbitMQ 要实现消息持久化,必须满足以下 4 个条件:
- 投递消息的时候 durable 设置为 true,消息持久化,代码:channel.queueDeclare(x, true, false, false, null),参数 2 设置为 true 持久化;
- 设置投递模式 deliveryMode 设置为 2(持久),代码:channel.basicPublish(x, x, MessageProperties.PERSISTENT TEXT PLAIN,x),参数 3 设置为存储纯文本到磁盘;
- 消息已经到达持久化交换器上;
- 消息已经到达持久化的队列。
7.消息持久化有哪些缺点?如何缓解?
答:消息持久化的缺点是很消耗性能,因为要写入硬盘要比写入内存性能较低很多,从而降低了服务器的吞吐量。可使用固态硬盘来提高读写速度,以达到缓解消息持久化的缺点。
8.如何使用 Java 代码连接 RabbitMQ?
答:使用 Java 代码连接 RabbitMQ 有以下两种方式:
方式一:
public static Connection GetRabbitConnection() {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername(Config.UserName);
factory.setPassword(Config.Password);
factory.setVirtualHost(Config.VHost);
factory.setHost(Config.Host);
factory.setPort(Config.Port);
Connection conn = null;
try {
conn = factory.newConnection();
} catch (Exception e) {
e.printStackTrace();
}
return conn;
}
方式二:
public static Connection GetRabbitConnection2() {
ConnectionFactory factory = new ConnectionFactory();
// 连接格式:amqp://userName:password@hostName:portNumber/virtualHost
String uri = String.format("amqp://%s:%s@%s:%d%s", Config.UserName, Config.Password, Config.Host, Config.Port,
Config.VHost);
Connection conn = null;
try {
factory.setUri(uri);
factory.setVirtualHost(Config.VHost);
conn = factory.newConnection();
} catch (Exception e) {
e.printStackTrace();
}
return conn;
}
9.使用 Java 代码编写一个 RabbitMQ 消费和生产的示例?
答:代码如下:
public static void main(String[] args) {
publisher(); // 生产消息
consumer(); // 消费消息
}
/**
* 推送消息
*/
public static void publisher() {
// 创建一个连接
Connection conn = ConnectionFactoryUtil.GetRabbitConnection();
if (conn != null) {
try {
// 创建通道
Channel channel = conn.createChannel();
// 声明队列【参数说明:参数一:队列名称,参数二:是否持久化;参数三:是否独占模式;参数四:消费者断开连接时是否删除队列;参数五:消息其他参数】
channel.queueDeclare(Config.QueueName, false, false, false, null);
String content = String.format("当前时间:%s", new Date().getTime());
// 发送内容【参数说明:参数一:交换机名称;参数二:队列名称,参数三:消息的其他属性-routing headers,此属性为MessageProperties.PERSISTENT_TEXT_PLAIN用于设置纯文本消息存储到硬盘;参数四:消息主体】
channel.basicPublish("", Config.QueueName, null, content.getBytes("UTF-8"));
System.out.println("已发送消息:" + content);
// 关闭连接
channel.close();
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 消费消息
*/
public static void consumer() {
// 创建一个连接
Connection conn = ConnectionFactoryUtil.GetRabbitConnection();
if (conn != null) {
try {
// 创建通道
Channel channel = conn.createChannel();
// 声明队列【参数说明:参数一:队列名称,参数二:是否持久化;参数三:是否独占模式;参数四:消费者断开连接时是否删除队列;参数五:消息其他参数】
channel.queueDeclare(Config.QueueName, false, false, false, null);
// 创建订阅器,并接受消息
channel.basicConsume(Config.QueueName, false, "", new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
String routingKey = envelope.getRoutingKey(); // 队列名称
String contentType = properties.getContentType(); // 内容类型
String content = new String(body, "utf-8"); // 消息正文
System.out.println("消息正文:" + content);
channel.basicAck(envelope.getDeliveryTag(), false); // 手动确认消息【参数说明:参数一:该消息的index;参数二:是否批量应答,true批量确认小于index的消息】
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}
10.RabbitMQ 交换器类型有哪些?
答:RabbitMQ 消费类型也就是交换器(Exchange)类型有以下四种:
- direct:轮询方式
- headers:轮询方式,允许使用 header 而非路由键匹配消息,性能差,几乎不用
- fanout:广播方式,发送给所有订阅者
- topic:匹配模式,允许使用正则表达式匹配消息
RabbitMQ 默认的是 direct 方式。
11.RabbitMQ 如何确保每个消息能被消费?
答:RabbitMQ 使用 ack 消息确认的方式保证每个消息都能被消费,开发者可根据自己的实际业务,选择 channel.basicAck()
方法手动确认消息被消费。
12.RabbitMQ 接收到消息之后必须消费吗?
答:RabbitMQ 接收到消息之后可以不消费,在消息确认消费之前,可以做以下两件事:
- 拒绝消息消费,使用 channel.basicReject(消息编号, true) 方法,消息会被分配给其他订阅者;
- 设置为死信队列,死信队列是用于专门存放被拒绝的消息队列。
13.topic 模式下发布了一个路由键为“com.mq.rabbit.error”的消息,请问以下不能接收到消息的是?
A:cn.mq.rabbit.*
B:#.error
C:cn.mq.*
D:cn.mq.#
答:C
题目解析:“*”用于匹配一个分段(用“.”分割)的内容,“#”用于匹配 0 和多个字符。
14.以下可以获取历史消息的是?
A:topic 交换器
B:fanout 交换器
C:direct 交换器
D:以上都不是
答:C
题目解析:fanout 和 topic 都是广播形式的,因此无法获取历史消息,而 direct 可以。
15.RabbitMQ 包含事务功能吗?如何使用?
答:RabbitMQ 包含事务功能,主要是对信道(Channel)的设置,主要方法有以下三个:
- channel.txSelect() 声明启动事务模式;
- channel.txComment() 提交事务;
- channel.txRollback() 回滚事务。
16.RabbitMQ 的事务在什么情况下是无效的?
答:RabbitMQ 的事务在 autoAck=true 也就是自动消费确认的时候,事务是无效的。因为如果是自动消费确认,RabbitMQ
会直接把消息从队列中移除,即使后面事务回滚也不能起到任何作用。
17.Kafka 可以脱离 ZooKeeper 单独使用吗?
答:Kafka 不能脱离 ZooKeeper 单独使用,因为 Kafka 使用 ZooKeeper 管理和协调 Kafka 的节点服务器。
18.Kafka 有几种数据保留的策略?
答:Kafka 有两种数据保存策略:按照过期时间保留和按照存储的消息大小保留。
19.Kafka 同时设置了 7 天和 10G 清除数据,到第五天的时候消息达到了 10G,这个时候 Kafka 将如何处理?
答:这个时候 Kafka 会执行数据清除工作,时间和大小不论哪个满足条件,都会清空数据。
20.什么情况会导致 Kafka 运行变慢?
答:以下情况可导致 Kafka 运行变慢:
- CPU 性能瓶颈
- 磁盘读写瓶颈
- 网络瓶颈
21.使用 Kafka 集群需要注意什么?
答:Kafka 集群使用需要注意以下事项:
- 集群的数量不是越多越好,最好不要超过 7 个,因为节点越多,消息复制需要的时间就越长,整个群组的吞吐量就越低;
- 集群数量最好是单数,因为超过一半故障集群就不能用了,设置为单数容错率更高。
MySQL 面试题汇总
1.说一下 MySQL 执行一条查询语句的内部执行过程?
答:MySQL 执行一条查询的流程如下:
- 客户端先通过连接器连接到 MySQL 服务器;
- 连接器权限验证通过之后,先查询是否有查询缓存,如果有缓存(之前执行过此语句)则直接返回缓存数据,如果没有缓存则进入分析器;
- 分析器会对查询语句进行语法分析和词法分析,判断 SQL 语法是否正确,如果查询语法错误会直接返回给客户端错误信息,如果语法正确则进入优化器;
- 优化器是对查询语句进行优化处理,例如一个表里面有多个索引,优化器会判别哪个索引性能更好;
- 优化器执行完就进入执行器,执行器则开始执行语句进行查询比对了,直到查询到满足条件的所有数据,然后进行返回。
2.MySQL 查询缓存有什么优缺点?
答:MySQL 查询缓存功能是在连接器之后发生的,它的优点是效率高,如果已经有缓存则会直接返回结果。
查询缓存的缺点是失效太频繁导致缓存命中率比较低,任何更新表操作都会清空查询缓存,因此导致查询缓存非常容易失效。
3.MySQL 的常用引擎都有哪些?
答:MySQL 的常用引擎有 InnoDB、MyISAM、Memory 等,从 MySQL 5.5.5 版本开始 InnoDB 就成为了默认的存储引擎。
4.常用的存储引擎 InnoDB 和 MyISAM 有什么区别?
答:InnoDB 和 MyISAM 最大的区别是 InnoDB 支持事务,而 MyISAM 不支持事务,它们其他主要区别如下:
- InnoDB 支持崩溃后安全恢复,MyISAM 不支持崩溃后安全恢复;
- InnoDB 支持行级锁,MyISAM 不支持行级锁,只支持到表锁;
- InnoDB 支持外键,MyISAM 不支持外键;
- MyISAM 性能比 InnoDB 高;
- MyISAM 支持 FULLTEXT 类型的全文索引,InnoDB 不支持 FULLTEXT 类型的全文索引,但是 InnoDB 可以使用 sphinx 插件支持全文索引,并且效果更好;
- InnoDB 主键查询性能高于 MyISAM。
5.什么叫回表查询?
答:普通索引查询到主键索引后,回到主键索引树搜索的过程,我们称为回表查询。
6.如果把一个 InnoDB 表的主键删掉,是不是就没有主键,就没办法进行回表查询了?
答:不是,如果把主键删掉了,那么 InnoDB 会自己生成一个长度为 6 字节的 rowid 作为主键。
7.一张自增表中有三条数据,删除两条数据之后重启数据库,再新增一条数据,此时这条数据的 ID 是几?
答:如果这张表的引擎是 MyISAM,那么 ID=4,如果是 InnoDB 那么 ID=2(MySQL 8 之前的版本)。
8.什么是独立表空间和共享表空间?它们的区别是什么?
答:共享表空间指的是数据库的所有表数据,索引文件全部放在一个文件中,默认这个共享表空间的文件路径在 data 目录下。
独立表空间:每一个表都将会生成以独立的文件方式来进行存储。
共享表空间和独立表空间最大的区别是如果把表放再共享表空间,即使表删除了空间也不会删除,因此表依然很大,而独立表空间如果删除表就会清除空间。
9.清空表的所有数据性能最好的语句是?
A:delete from t
B:delete t
C:drop table t
D:truncate table t
答:D
题目解析:truncate 清除表数据不会写日志,delete 要写日志,因此 truncate 的效率要高于 delete。
10.唯一索引和普通索引哪个性能更好?
答:唯一索引和普通索引的性能对比分为以下两种情况:
- 对于查询来说两者都是从索引树进行查询,性能几乎没有任何区别;
- 对于更新操作来说,因为唯一索引需要先将数据读取到内存,然后需要判断是否有冲突,因此比唯一索引要多了判断操作,从而性能就比普通索引性能要低。
11.left join 和 right join 的区别是什么?
答:left join 和 right join 的区别如下:
- left join(左联结),返回左表全部记录和右表联结字段相等的记录;
- right join(右联结),返回右表全部记录和左表联结字段相等的记录。
12.什么是最左匹配原则?它的生效原则有哪些?
答:最左匹配原则也叫最左前缀原则,是 MySQL
中的一个重要原则,指的是索引以最左边为起点任何连续的索引都能匹配上,当遇到范围查询(>、<、between、like)就会停止匹配。
生效原则来看以下示例,比如表中有一个联合索引字段 index(a,b,c):
- where a=1 只使用了索引 a;
- where a=1 and b=2 只使用了索引 a,b;
- where a=1 and b=2 and c=3 使用a,b,c;
- where b=1 or where c=1 不使用索引;
- where a=1 and c=3 只使用了索引 a;
- where a=3 and b like ‘xx%’ and c=3 只使用了索引 a,b。
13.以下 or 查询有什么问题吗?该如何优化?
select * from t where num=10 or num=20;
答:如果使用 or 查询会使 MySQL 放弃索引而全表扫描,可以改为:
select * from t where num=10
union
select * from t where num=20;
14.事务是什么?它有什么特性?
答:事务是一系列的数据库操作,是数据库应用的基本单位。
在 MySQL 中只有 InnoDB 引擎支持事务,它的四个特性如下:
- 原子性(Atomic),要么全部执行,要么全部不执行;
- 一致性(Consistency),事务的执行使得数据库从一种正确状态转化为另一种正确状态;
- 隔离性(Isolation),在事务正确提交之前,不允许把该事务对数据的任何改变提供给其他事务;
- 持久性(Durability),事务提交后,其结果永久保存在数据库中。
15.MySQL 中有几种事务隔离级别?分别是什么?
答:MySQL 中有四种事务隔离级别,分别是:
- read uncommited,未提交读,读到未提交数据;
- read committed,读已提交,也叫不可重复读,两次读取到的数据不一致;
- repetable read,可重复读;
- serializable,串行化,读写数据都会锁住整张表,数据操作不会出错,但并发性能极低,开发中很少用到。
MySQL 默认使用 repetable read 的事务隔离级别。
16.如何设置 MySQL 的事务隔离级别?
答:MySQL 事务隔离级别 mysql.cnf 文件里设置的(默认目录 /etc/my.cnf),在文件的文末添加配置:
transaction-isolation = REPEATABLE-READ
可用的配置值:READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ、SERIALIZABLE。
17.MySQL 出现了中文乱码该如何解决?
答:解决 MySQL 中文乱码的问题,可以设置全局编码或设置某个数据库或表的编码为 utf8。 设置全局编码:
set character_set_client='utf8';
set character_set_connection='utf8';
set character_set_results='utf8';
设置数据库的编码:
alter database db character set utf8;
设置表的编码:
alter table t character set utf8;
18.InnoDB 为什么要使用 B+ 树,而不是 B 树、Hash、红黑树或二叉树?
答:因为 B 树、Hash、红黑树或二叉树存在以下问题。
- B 树:不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出),指针少的情况下要保存大量数据,只能增加树的高度,导致 IO 操作变多,查询性能变低。
- Hash:虽然可以快速定位,但是没有顺序,IO 复杂度高。
- 二叉树:树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且 IO 代价高。
- 红黑树:树的高度随着数据量增加而增加,IO 代价高。
19.MySQL 是如何处理死锁?
答:MySQL 对待死锁常见的两种策略:
- 通过 innodb_lock_wait_timeout 来设置超时时间,一直等待直到超时;
- 发起死锁检测,发现死锁之后,主动回滚死锁中的某一个事务,让其他事务继续执行。
20.什么是全局锁?它的应用场景有哪些?
答:全局锁就是对整个数据库实例加锁,它的典型使用场景就是做全量逻辑备份,这个时候整个库会处于完全的只读状态。
21.使用全局锁会导致什么问题?
答:使用全局锁会使整个系统不能执行更新操作,所有的更新业务会出于等待状态;如果你是在从库进行备份,则会导致主从同步严重延迟。
22.InnoDB 存储引擎有几种锁算法?
答:InnoDB 的锁算法包括以下三种:
- Record Lock — 单个行记录上的锁;
- Gap Lock — 间隙锁,锁定一个范围,不包括记录本身;
- Next-Key Lock — 锁定一个范围,包括记录本身。
23.InnoDB 如何实现行锁?
答:只有通过索引条件检索数据,InnoDB 才使用行级锁,否则 InnoDB 将使用表锁。使用 for update 来实现行锁,具体脚本如下:
select * from t where id=1 for update
其中 id 字段必须有索引。
24.MySQL 性能指标都有哪些?如何得到这些指标?
答:MySQL 最重要的性能指标有以下两个:
- QPS(Queries Per Second),每秒查询数,一台数据库每秒能够处理的查询次数;
- TPS(Transactions Per Second),每秒处理事务数。
这些性能指标可以通过 show status 来查询当前数据库状态的结果信息中估算出来,show status 会有 300
多条状态信息记录,其中以下这些信息 QPS 和 TPS 有关系:
- Uptime,服务器已经运行的时间,单位秒;
- Questions,已经发送给数据库查询数;
- Com_select,查询次数,实际查询次数;
- Com_insert,插入次数;
- Com_delete,删除次数;
- Com_update,更新次数;
- Com_commit,事务次数;
- Com_rollback,回滚次数。
25.MySQL 中的重要日志分为哪几个?
① 错误日志 :用来记录 MySQL 服务器运行过程中的错误信息,比如,无法加载 MySQL
数据库的数据文件,或权限不正确等都会被记录在此,还有复制环境下,从服务器进程的信息也会被记录进错误日志。默认情况下,错误日志是开启的,且无法被禁止。默认情况下,错误日志是存储在数据库的数据文件目录中,名称为
hostname.err,其中 hostname 为服务器主机名。在 MySQL 5.5.7
之前,数据库管理员可以删除很长时间之前的错误日志,以节省服务器上的硬盘空间, MySQL 5.5.7
之后,服务器将关闭此项功能,只能使用重命名原来的错误日志文件,手动冲洗日志创建一个新的,命令为:
mv hostname.err hostname.err.old
mysqladmin flush-logs
② 查询日志 :查询日志在 MySQL 中被称为 general log(通用日志),查询日志里的内容不要被“查询日志”误导,认为里面只存储
select 语句,其实不然,查询日志里面记录了数据库执行的所有命令,不管语句是否正确,都会被记录,具体原因如下:
- insert 查询为了避免数据冲突,如果此前插入过数据,则当前插入的数据如果跟主键或唯一键的数据重复那肯定会报错;
- update 时也会查询因为更新的时候很可能会更新某一块数据;
- delete 查询,只删除符合条件的数据;
因此都会产生日志,在并发操作非常多的场景下,查询信息会非常多,那么如果都记录下来会导致 IO 非常大,影响 MySQL
性能。因此如果不是在调试环境下,是不建议开启查询日志功能的。
查询日志的开启有助于帮助我们分析哪些语句执行密集,执行密集的 select
语句对应的数据是否能够被缓存,同时也可以帮助我们分析问题,因此,可以根据自己的实际情况来决定是否开启查询日志。
查询日志模式是关闭的,可以通过以下命令开启查询日志:
set global general_log=1
set global log_output=‘table’;
general_log=1 为开启查询日志,0 为关闭查询日志,这个设置命令即时生效,不用重启 MySQL 服务器。
③ 慢日志 :慢查询会导致 CPU、IOPS、内存消耗过高,当数据库遇到性能瓶颈时,大部分时间都是由于慢查询导致的。开启慢查询日志,可以让
MySQL
记录下查询超过指定时间的语句,之后运维人员通过定位分析,能够很好的优化数据库性能。默认情况下,慢查询日志是不开启的,只有手动开启了,慢查询才会被记录到慢查询日志中。使用如下命令记录当前数据库的慢查询语句:
set global slow_query_log=‘ON’;
使用 set global slow_query_log=‘ON’ 开启慢查询日志,只是对当前数据库有效,如果 MySQL
数据库重启后就会失效。因此如果要永久生效,就要修改配置文件 my.cnf,设置 slow_query_log=1 并重启 MySQL 服务器。
④ redo log(重做日志) :为了最大程度的避免数据写入时,因为 IO 瓶颈造成的性能问题,MySQL
采用了这样一种缓存机制,先将数据写入内存中,再批量把内存中的数据统一刷回磁盘。为了避免将数据刷回磁盘过程中,因为掉电或系统故障带来的数据丢失问题,InnoDB
采用 redo log 来解决此问题。
⑤ undo log(回滚日志) :用于存储日志被修改前的值,从而保证如果修改出现异常,可以使用 undo log 日志来实现回滚操作。
undo log 和 redo log 记录物理日志不一样,它是逻辑日志,可以认为当 delete 一条记录时,undo log 中会记录一条对应的
insert 记录,反之亦然,当 update 一条记录时,它记录一条对应相反的 update 记录,当执行 rollback 时,就可以从 undo
log 中的逻辑记录读取到相应的内容并进行回滚。undo log 默认存放在共享表空间中,在 ySQL 5.6 中,undo log
的存放位置还可以通过变量 innodb_undo_directory 来自定义存放目录,默认值为“.”表示 datadir 目录。
⑥ bin log(二进制日志) :是一个二进制文件,主要记录所有数据库表结构变更,比如,CREATE、ALTER TABLE
等,以及表数据修改,比如,INSERT、UPDATE、DELETE 的所有操作,bin log 中记录了对 MySQL
数据库执行更改的所有操作,并且记录了语句发生时间、执行时长、操作数据等其他额外信息,但是它不记录 SELECT、SHOW 等那些不修改数据的 SQL 语句。
binlog 的作用如下:
- 恢复(recovery):某些数据的恢复需要二进制日志。比如,在一个数据库全备文件恢复后,用户可以通过二进制日志进行 point-in-time 的恢复;
- 复制(replication):其原理与恢复类似,通过复制和执行二进制日志使一台远程的 MySQL 数据库(一般称为 slave 或者 standby)与一台 MySQL 数据库(一般称为 master 或者 primary)进行实时同步;
- 审计(audit):用户可以通过二进制日志中的信息来进行审计,判断是否有对数据库进行注入攻击。
除了上面介绍的几个作用外,binlog 对于事务存储引擎的崩溃恢复也有非常重要的作用,在开启 binlog 的情况下,为了保证 binlog 与 redo
的一致性,MySQL 将采用事务的两阶段提交协议。当 MySQL 系统发生崩溃时,事务在存储引擎内部的状态可能为 prepared(准备状态)和
commit(提交状态)两种,对于 prepared 状态的事务,是进行提交操作还是进行回滚操作,这时需要参考 binlog,如果事务在 binlog
中存在,那么将其提交;如果不在 binlog 中存在,那么将其回滚,这样就保证了数据在主库和从库之间的一致性。
binlog 默认是关闭状态,可以在 MySQL 配置文件(my.cnf)中通过配置参数 log-bin = [base-name] 开启记录 binlog
日志,如果不指定 base-name,则默认二进制日志文件名为主机名,并以自增的数字作为后缀,比如:mysql-
bin.000001,所在目录为数据库所在目录(datadir)。
通过以下命令来查询 binlog 是否开启:
show variables like ‘log_%’;
binlog 格式分为 STATEMENT、ROW 和 MIXED 三种。
- STATEMENT 格式的 binlog 记录的是数据库上执行的原生 SQL 语句。这种格式的优点是简单,简单地记录和执行这些语句,能够让主备保持同步,在主服务器上执行的 SQL 语句,在从服务器上执行同样的语句。另一个好处是二进制日志里的时间更加紧凑,因此相对而言,基于语句的复制模式不会使用太多带宽,同时也节约磁盘空间,并且通过 mysqlbinlog 工具容易读懂其中的内容。缺点就是同一条 SQL 在主库和从库上执行的时间可能稍微或很大不相同,因此在传输的二进制日志中,除了查询语句,还包括了一些元数据信息,如当前的时间戳。即便如此,还存在着一些无法被正确复制的 SQL,比如,使用 INSERT INTO TB1 VALUE(CUURENT_DATE()) 这一条使用函数的语句插入的数据复制到当前从服务器上来就会发生变化,存储过程和触发器在使用基于语句的复制模式时也可能存在问题;另外一个问题就是基于语句的复制必须是串行化的,比如,InnoDB 的 next-key 锁等,并不是所有的存储引擎都支持基于语句的复制。
- ROW 格式是从 MySQL 5.1 开始支持基于行的复制,也就是基于数据的复制,基于行的更改。这种方式会将实际数据记录在二进制日志中,它有其自身的一些优点和缺点,最大的好处是可以正确地复制每一行数据,一些语句可以被更加有效地复制,另外就是几乎没有基于行的复制模式无法处理的场景,对于所有的 SQL 构造、触发器、存储过程等都能正确执行;它的缺点就是二进制日志可能会很大,而且不直观,因此,你不能使用 mysqlbinlog 来查看二进制日志,也无法通过看二进制日志判断当前执行到那一条 SQL 语句。现在对于 ROW 格式的二进制日志基本是标配了,主要是因为它的优势远远大于缺点,并且由于 ROW 格式记录行数据,因此可以基于这种模式做一些 DBA 工具,比如数据恢复,不同数据库之间数据同步等。
- MIXED 也是 MySQL 默认使用的二进制日志记录方式,但 MIXED 格式默认采用基于语句的复制,一旦发现基于语句的无法精确的复制时,就会采用基于行的复制。比如用到 UUID()、USER()、CURRENT_USER()、ROW_COUNT() 等无法确定的函数。
26.redo log 和 binlog 有什么区别?
redo log(重做日志)和 binlog(归档日志)都是 MySQL 的重要的日志,它们的区别如下:
- redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;
- binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”;
- redo log 是 InnoDB 引擎特有的,binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用;
- redo log 是循环写的,空间固定会用完,binlog 是可以追加写入的,“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
最开始 MySQL 里并没有 InnoDB 引擎,MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe
的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe
能力的,因此 InnoDB 使用另外一套日志系统,也就是 redo log 来实现 crash-safe 能力。
27.慢查询日志的获取方式有哪些?
答:慢查询日志的常见获取方式如下。
- 使用 MySQL 自带功能,开启慢查询日志,在 MySQL 的安装目录下找到 my.cnf 文件设置
slow-query-log=On
开启慢查询,慢查询默认时长为 10s,默认存储文件名为 host_name-slow.log。 - 使用三方开源方案 zabbix,zabbix 是一个基于 Web 界面的提供分布式系统监视以及网络监视功能的企业级的开源解决方案,能监视各种网络参数,保证服务器系统的安全运营;并提供灵活的通知机制以让系统管理员快速定位/解决存在的各种问题。
28.如何定位慢查询?
答:使用 MySQL 中的 explain 分析执行语句,比如:
explain select * from t where id=5;
如下图所示:
其中:
- id — 选择标识符,id 越大优先级越高,越先被执行
- select_type — 表示查询的类型。
- table — 输出结果集的表
- partitions — 匹配的分区
- type — 表示表的连接类型
- possible_keys — 表示查询时,可能使用的索引
- key — 表示实际使用的索引
- key_len — 索引字段的长度
- ref— 列与索引的比较
- rows — 大概估算的行数
- filtered — 按表条件过滤的行百分比
- Extra — 执行情况的描述和说明
其中最重要的就是 type 字段,type 值类型如下:
- all — 扫描全表数据
- index — 遍历索引
- range — 索引范围查找
- index_subquery — 在子查询中使用 ref
- unique_subquery — 在子查询中使用 eq_ref
- ref_or_null — 对 null 进行索引的优化的 ref
- fulltext — 使用全文索引
- ref — 使用非唯一索引查找数据
- eq_ref — 在 join 查询中使用主键或唯一索引关联
- const — 将一个主键放置到 where 后面作为条件查询, MySQL 优化器就能把这次查询优化转化为一个常量,如何转化以及何时转化,这个取决于优化器,这个比 eq_ref 效率高一点
29.MySQL 中常见的读写分离方案有哪些?
答:MySQL 中常见的读写分离方案通常为以下两种:
- 使用 MySQL 官方提供的数据库代理产品 MySql ProxySQL 搭建自动分配的数据库读写分离环境;
- 在程序层面配置多数据源使用代码实现读写分离。
30.怎样保证主备数据库无延迟?
答:通常保证主备数据库无延迟有以下三种方法。
- 每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0。如果还不等于 0 ,那就必须等到这个参数变为 0 才能执行查询请求,seconds_behind_master 参数是用来衡量主备延迟时间的长短。
- 对比位点确保主备无延迟。Master_Log_File 和 Read_Master_Log_Pos,表示的是读到的主库的最新位点,Relay_Master_Log_File 和 Exec_Master_Log_Pos,表示的是备库执行的最新位点。
- 对比 GTID 集合确保主备无延迟。Auto_Position=1 ,表示这对主备关系使用了 GTID 协议。Retrieved_Gtid_Set,是备库收到的所有日志的 GTID 集合;Executed_Gtid_Set,是备库所有已经执行完成的 GTID 集合。
31.什么是 MySQL 多实例,如何配置 MySQL 多实例?
答:MySQL 多实例就是在同一台服务器上启用多个 MySQL
服务,它们监听不同的端口,运行多个服务进程,它们相互独立,互不影响的对外提供服务,便于节约服务器资源与后期架构扩展。 多实例的配置方法有两种:
- 一个实例一个配置文件,不同端口;
- 同一配置文件(my.cnf)下配置不同实例,基于 MySQL 的 d_multi 工具。
32.表的优化策略有哪些?
「参考答案」常见的大表优化策略如下。
- 读写分离,主库负责写,从库负责读。
- 垂直分区,根据数据属性单独拆表甚至单独拆库。
- 水平分区,保持表结构不变,根据策略存储数据分片,这样每一片数据被分散到不同的表或者库中。水平拆分只是解决了单一表数据过大的问题,表数据还在同一台机器上,对于并发能力没有什么意义,因此水平拆分最好分库。另外分片事务难以解决,跨节点 join 性能较差。
33.数据库分片方案有哪些?
「参考答案」数据库分片方案有哪些? 答:数据库创建的分片方案有两种方式:客户端代理方式和中间件代理方式。
- 客户端代理 — 分片逻辑在应用端,封装在 jar 包中,通过修改或者封装 JDBC 层来实现,比如 Sharding-JDBC、阿里 TDDL 等。
- 中间件代理 — 在应用层和数据层中间加了一个代理层。分片逻辑统一维护在中间件服务中,比如 MyCat、网易的 DDB 都是中间件代理的典型代表。
34.查询语句的优化方案有哪些?
「参考答案」常见优化方案如下:
- 不做列运算,把计算都放入各个业务系统实现;
- 查询语句尽可能简单,大语句拆小语句,减少锁时间;
- 不使用 select * 查询;
- or 查询改写成 in 查询;
- 不用函数和触发器;
- 避免 %xx 查询;
- 少用 join 查询;
- 使用同类型比较,比如 ‘123’ 和 ‘123’、123 和 123;
- 尽量避免在 where 子句中使用 != 或者 <> 操作符,查询引用会放弃索引而进行全表扫描;
- 列表数据使用分页查询,每页数据量不要太大。
35.MySQL 毫无规律的异常重启,可能产生的原因是什么?该如何解决?
「参考答案」可能是积累的长连接导致内存占用太多,被系统强行杀掉导致的异常重启,因为在 MySQL
中长连接在执行过程中使用的临时内存对象,只有在连接断开的时候才会释放,这就会导致内存不断飙升,解决方案如下:
- 定期断开空闲的长连接;
- 如果是用的是 MySQL 5.7 以上的版本,可以定期执行 mysql_reset_connection 重新初始化连接资源,这个过程会释放之前使用的内存资源,恢复到连接刚初始化的状态。
Redis 面试题汇总
1.Redis 使用场景有哪些?
答:Redis 使用场景如下:
- 记录帖子点赞数、点击数、评论数
- 缓存近期热帖
- 缓存文章详情信息
- 记录用户会话信息
2.Redis 有哪些功能?
答:Redis 功能如下:
- 数据缓存功能
- 分布式锁的功能
- 支持数据持久化
- 支持事务
- 支持消息队列
3.Redis 支持哪些数据类型?
答:Redis 支持的数据类型如下:
- String 字符串
- List 列表
- Set 无序集合
- ZSet 有序集合
- Hash 哈希类型
4.Redis 相比 Memcached 有哪些优势?
答:Redis 相比 Memcached 优势如下:
- Memcached 所有的值均是简单的字符串,Redis 支持更为丰富的数据类型
- Redis 的速度比 Memcached 要快
- Redis 可以持久化
- Redis 可以设置过期时间
- Redis 支持主从同步
5.Redis 支持哪些淘汰策略?
答:Redis 淘汰策略如下:
- noeviction:禁止淘汰数据;
- allkeys-lru:尝试回收最少使用的键,使得新添加的数据有空间存放;
- volatile-lru:尝试回收最少使用的键,但仅限于在过期集合的键,使得新添加的数据有空间存放;
- allkeys-random:回收随机的键使得新添加的数据有空间存放;
- volatile-random:回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键;
- volatile-ttl:回收在过期集合的键,并且优先回收存活时间较短的键,使得新添加的数据有空间存放。
6.官方为什么不支持 Windows 版本?
答:Redis 官方是不支持 Windows 版的,因为目前 Linux 版本已经相当稳定,如果开发 Windows 版本,反而会带来兼容性等问题。
7.为什么 Redis 是单线程的?
答:因为 Redis 的瓶颈最有可能是机器内存或者网络带宽,而非单线程,既然单线程不是 Redis 的性能瓶颈,并且单线程又比较容易实现,所以 Redis
就选择使用单线程来实现。
单线程并不代表运行速度就慢,比如,Nginx 和 NodeJs 都是单线程高性能的代表。
8.为什么 Redis 需要把所有数据放到内存中?
答:Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘,这样 Redis 就拥有了快速查询和数据持久化等特征。
9.在 Redis 中 key 的最大容量是多少?
答:最大容量 512 MB,官方说明如下图所示:
10.Jedis 和 Redisson 有什么区别?
答:Jedis 和 Redisson 的区别如下:
- Jedis 是 Redis 的 Java 实现客户端,其 API 提供了比较全面的 Redis 命令的支持;
- Redisson 实现了分布式和可扩展的 Java 数据结构,和 Jedis 相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等 Redis 特性。Redisson 的宗旨是促进使用者对 Redis 的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
11.Redis 如何设置过期时间?如何设置永久有效?
答:Redis 通过 expire() 方法设置过期时间,语法:redis.expire(key, expiration)。当 expire
的过期时间设置为 -1 时,表示永不过期。
12.如何保证 Redis 的数据一致性?
答:可使用以下方法来保证 Redis 的数据一致性:
- 合理设置缓存的过期时间;
- 新增、更改、删除数据库操作时同步更新 Redis,可以使用事物机制来保证数据的一致性。
13.Redis 的数据结构是什么?它有什么优缺点?
答:Redis
的数据结构是跳跃表,跳跃表是一种基于链表的扩展,跳跃表还是一个链表,是一个有序的链表,在遍历的时候基于比较,但普通链表只能遍历,跳跃表加入了一个层的概念,层级越高元素越少,每次先从高层查找,直到找到合适的位置,从图中可以看到高层的节点远远少于底层的节点数,从而实现了跳跃式查找。
跳跃表优点:
- 实现比红黑树简单
- 比红黑树更容易扩展
- 红黑树插入删除时为了平衡高度需要旋转附近节点,高并发需要锁,跳跃表不需要考虑
跳跃表缺点:
- 比红黑树占用更多的内存,每个节点的大小取决于该节点层数
- 空间局部性差导致缓存命中率低,比红黑树略慢
14.Redis 为什么用跳跃表来存储?
答:第一是因为红黑树存储比较复杂,调整涉及到多个节点的并发修改;第二是越接近根节点的地方越容易产生竞争,即使是不同叶子节点的操作由于平衡操作也可能逐级向上涉及到接近根的节点,而跳跃表可以用
CAS(Compare And Swap)来并发操作节点,比较容易实现,且更加局部化。
15.什么是缓存穿透?如何解决?
答:缓存穿透是指查询一个一定不存在的数据,由于缓存中没有,因而每次需要从数据库中查询,但数据库也没有相应的数据,所以不会写入缓存,这就将导致每次请求都会去数据库查询,这种行为就叫缓存穿透。
解决方案是不管查询数据库是否有数据,都缓存起来,只不过把没有数据的缓存结果的过期时间设置为比较短的一个值,比如 3 分钟。
16.什么是缓存雪崩,该如何解决?
答:指缓存由于某些原因,比如,宕机或者缓存大量过期等,从而导致大量请求到达后端数据库,进而导致数据库崩溃的情况。
解决缓存雪崩的方案如下:
- 分析业务功能,尽量让缓存的失效时间点均匀分布;
- 使用 Redis 主备,保证缓存系统的高可用。
17.什么是缓存预热?有几种实现方式?
答:缓存预热是指系统上线后,将相关的缓存数据直接加载到缓存系统,这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。
缓存预热的实现方式,可分为以下两种:
- 数据量不大的时候,工程启动的时候进行加载缓存动作;
- 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新。
18.在 Java 程序中如何使用 Redis?
答:在 Java 程序中可使用 Jedis 来操作 Redis,使用步骤如下:
1)添加 Jedis 引用
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>x.x.x</version>
</dependency>
2)连接并操作 Redis
Jedis jedis = new Jedis("127.0.0.1",6379);
// 存值
jedis.set("hello","world");
// 取值
jedis.get("hello");
// 关闭连接
jedis.close();
19.什么是 Redis 持久化?如何进行 Redis 持久化?
答:Redis 持久化是指将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。 Redis 有以下两种持久化方案:
- RDB(Redis DataBase):是指在制定的时间间隔内将内存中的数据集快照写入磁盘;
- AOF(Append Only File):该机制将以日志的形式记录服务器所处理的每一个写操作,在 Redis 服务器启动之初会读取该文件来重新构建数据库,以保证启动后数据库中的数据是完整的。
Redis 默认支持的持久化方式是 RDB 方式。
20.RDB 和 AOF 有什么区别?
答:RDB 和 AOF 的区别如下:
- RDB 可能会引起一定程度的数据丢失,AOF 不会造成数据丢失
- RDB 的启动效率更高
- AOF 占用的空间比 RDB 大,AOF 的同步速度比 RDB 慢
总体来说如果对数据的完整性要求不高,RDB 是最好的解决方案,反之则选择 AOF。
21.Redis 的监控工具都有哪些?
答:常用的 Redis 监控工具如下:
- Redis-stat:采用 Ruby 开发,基于 Redis 的 info 命令来统计,不影响 Redis 的性能;
- RedisLive:采用 Python 开发的可视化及查询分析工具,它是通过监控脚本来 Redis 提供的 MONITOR 命令从被监控 Redis 实例中获取数据,并存储到 Redis 的监控实例中。
22.如何定位 Redis 的慢查询?
答:使用 slowlog get 来定位慢查询操作,如下所示:
127.0.0.1:6379> slowlog get
- (integer) 0
- (integer) 1565937939
- (integer) 28003
- “lpush”
2) "list"
3) "1"
4) "2"
5) "6"
6) "3"
7) "4"
8) "9"
9) "8"
其中:
-
表示慢查询记录 id
-
表示发起命令的时间戳
-
表示命令耗时,单位为微秒
-
表示该条记录的命令及参数
23.SAVE 和 BGSAVE 有什么区别?
答:SAVE 和 BGSAVE 都是用于 Redis 持久化的,它们的区别如下:
- SAVE 直接调用 rdbSave 函数(用于 Redis 持久化的函数),阻塞 Redis 主进程,直到保存完成为止,在主进程阻塞期间,服务器不能处理客户端的任何请求;
- BGSAVE 则会创建一个子进程,子进程负责调用 rdbSave 函数,并在保存完成之后向主进程发送完成信号,Redis 服务器在 BGSAVE 执行期间仍然可以继续处理客户端的请求。
24.Redis 是如何实现同步的?
答:Redis 可以实现主从同步和从从同步。当第一次同步时,主节点做一次 BGSAVE,并同时将后续修改操作记录到内存中,待完成后将 RDB
文件全量同步到复制节点,复制节点接受完成后将 RDB
镜像加载到内存,加载完成后再通知主节点将期间修改的操作记录,同步到复制节点进行重放,这样就完成了同步过程。
25.Redis 可以切换数据库吗?如何切换?
答:Redis 不像 MySQL 等关系型数据库那样有数据库的概念,不同的数据存在不同的数据库中,Redis
数据库是由一个整数索引标识,而不是一个数据库名称,默认情况下客户端连接到数据库 0,可以在配置文件中控制数据库总数,默认是 16 个。
可以使用 select index 来切换数据库,如下所示:
127.0.0.1:6379> select 0
OK
26.Redis 有哪些集群策略?
答:Redis 集群策略有以下 3 种:
- 主从策略:1 台机器作为写操作,另外 2 台作为读操作,类似于 MySQL 的主从方式;
- 哨兵策略:增加 1 台机器作为哨兵,监控 3 台主从机器,当主节点挂机的时候,机器内部进行选举,从集群中从节点里指定一台机器升级为主节点,从而实现高可用。当主节点恢复的时候,加入到从节点中继续提供服务;
- 集群策略:Redis 3.0 之后增加了集群的概念,可实现多主多从的结构,实现真正的高可用。
27.Redis 集群方案都有哪些?
答:Redis 集群实现方案如下:
- Twemproxy 是 Twitter 开源的 Redis 代理,它的使用和普通 Redis 完全一致,它会以一个代理的身份接收请求,并使用 hash 算法将请求转接到具体 Redis,将结果再返回 Twemproxy;
- Codis 是开源解决方案,也是目前用的最多的集群方案,基本和 Twemproxy 效果一致,但它支持在节点数量改变情况下,旧节点数据可恢复到新 hash 节点;
- Redis Cluster 是 Redis 3.0 自带的集群方案,特点在于他的分布式算法不是一致性 hash,而是 hash 槽的概念,以及自身支持节点设置从节点;
- 业务代码层实现,创建几个独立的 Redis 实例,在代码层对 key 进行 hash 计算,然后去对应的 Redis 实例操作数据。这种方式对 hash 层代码要求比较高,考虑部分包括,节点失效后的替代算法方案,数据震荡后的自动脚本恢复、实例的监控等。
28.Redis 如何做内存优化?
答:把相关的信息整体存储,而不是把每个信息独立存储,这样就可以有效的减少内存使用。
29.分布式锁设计时需要注意哪些事项?
答:通常分布式锁在设计时,需同时满足以下四个约束条件。
- 互斥性:在任意时刻,只有一个客户端能持有锁。
- 安全性:即不会形成死锁,当一个客户端在持有锁的期间崩溃而没有主动解锁的情况下,其持有的锁也能够被正确释放,并保证后续其他客户端能加锁。
- 可用性:就 Redis 而言,当提供锁服务的 Redis master 节点发生宕机等不可恢复性故障时,slave 节点能够升主并继续提供服务,支持客户端加锁和解锁;对基于分布式一致性算法实现的锁服务,如 ETCD 而言,当 leader 节点宕机时,follow 节点能够选举出新的 leader 继续提供锁服务。
- 对称性:对于任意一个锁,其加锁和解锁必须是同一个客户端,即,客户端 A 不能把客户端 B 加的锁给解了。
30.Redis 集群实现的原理是什么?
答:集群的实现原理和集群的实现方式有关,如下所述:
- Redis Sentinal 着眼于高可用,在 Master 宕机时会自动将 Slave 提升为 Master,继续提供服务;
- Redis Cluster 着眼于扩展性,在单个 Redis 内存不足时,使用 Cluster 进行分片存储。
31.Redis 常见的性能问题有哪些?
答:Redis 常见性能问题如下:
- 主服务器写内存快照,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,因此主服务器最好不要写内存快照;
- Redis 主从复制的性能问题,为了主从复制的速度和连接的稳定性,主从库最好在同一个局域网内。
lo",“world”);
// 取值
jedis.get(“hello”);
// 关闭连接
jedis.close();
19.什么是 Redis 持久化?如何进行 Redis 持久化?
答:Redis 持久化是指将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。 Redis 有以下两种持久化方案:
- RDB(Redis DataBase):是指在制定的时间间隔内将内存中的数据集快照写入磁盘;
- AOF(Append Only File):该机制将以日志的形式记录服务器所处理的每一个写操作,在 Redis 服务器启动之初会读取该文件来重新构建数据库,以保证启动后数据库中的数据是完整的。
Redis 默认支持的持久化方式是 RDB 方式。
20.RDB 和 AOF 有什么区别?
答:RDB 和 AOF 的区别如下:
- RDB 可能会引起一定程度的数据丢失,AOF 不会造成数据丢失
- RDB 的启动效率更高
- AOF 占用的空间比 RDB 大,AOF 的同步速度比 RDB 慢
总体来说如果对数据的完整性要求不高,RDB 是最好的解决方案,反之则选择 AOF。
21.Redis 的监控工具都有哪些?
答:常用的 Redis 监控工具如下:
- Redis-stat:采用 Ruby 开发,基于 Redis 的 info 命令来统计,不影响 Redis 的性能;
- RedisLive:采用 Python 开发的可视化及查询分析工具,它是通过监控脚本来 Redis 提供的 MONITOR 命令从被监控 Redis 实例中获取数据,并存储到 Redis 的监控实例中。
22.如何定位 Redis 的慢查询?
答:使用 slowlog get 来定位慢查询操作,如下所示:
127.0.0.1:6379> slowlog get
- (integer) 0
- (integer) 1565937939
- (integer) 28003
- “lpush”
2) "list"
3) "1"
4) "2"
5) "6"
6) "3"
7) "4"
8) "9"
9) "8"
其中:
-
表示慢查询记录 id
-
表示发起命令的时间戳
-
表示命令耗时,单位为微秒
-
表示该条记录的命令及参数
23.SAVE 和 BGSAVE 有什么区别?
答:SAVE 和 BGSAVE 都是用于 Redis 持久化的,它们的区别如下:
- SAVE 直接调用 rdbSave 函数(用于 Redis 持久化的函数),阻塞 Redis 主进程,直到保存完成为止,在主进程阻塞期间,服务器不能处理客户端的任何请求;
- BGSAVE 则会创建一个子进程,子进程负责调用 rdbSave 函数,并在保存完成之后向主进程发送完成信号,Redis 服务器在 BGSAVE 执行期间仍然可以继续处理客户端的请求。
24.Redis 是如何实现同步的?
答:Redis 可以实现主从同步和从从同步。当第一次同步时,主节点做一次 BGSAVE,并同时将后续修改操作记录到内存中,待完成后将 RDB
文件全量同步到复制节点,复制节点接受完成后将 RDB
镜像加载到内存,加载完成后再通知主节点将期间修改的操作记录,同步到复制节点进行重放,这样就完成了同步过程。
25.Redis 可以切换数据库吗?如何切换?
答:Redis 不像 MySQL 等关系型数据库那样有数据库的概念,不同的数据存在不同的数据库中,Redis
数据库是由一个整数索引标识,而不是一个数据库名称,默认情况下客户端连接到数据库 0,可以在配置文件中控制数据库总数,默认是 16 个。
可以使用 select index 来切换数据库,如下所示:
127.0.0.1:6379> select 0
OK
26.Redis 有哪些集群策略?
答:Redis 集群策略有以下 3 种:
- 主从策略:1 台机器作为写操作,另外 2 台作为读操作,类似于 MySQL 的主从方式;
- 哨兵策略:增加 1 台机器作为哨兵,监控 3 台主从机器,当主节点挂机的时候,机器内部进行选举,从集群中从节点里指定一台机器升级为主节点,从而实现高可用。当主节点恢复的时候,加入到从节点中继续提供服务;
- 集群策略:Redis 3.0 之后增加了集群的概念,可实现多主多从的结构,实现真正的高可用。
27.Redis 集群方案都有哪些?
答:Redis 集群实现方案如下:
- Twemproxy 是 Twitter 开源的 Redis 代理,它的使用和普通 Redis 完全一致,它会以一个代理的身份接收请求,并使用 hash 算法将请求转接到具体 Redis,将结果再返回 Twemproxy;
- Codis 是开源解决方案,也是目前用的最多的集群方案,基本和 Twemproxy 效果一致,但它支持在节点数量改变情况下,旧节点数据可恢复到新 hash 节点;
- Redis Cluster 是 Redis 3.0 自带的集群方案,特点在于他的分布式算法不是一致性 hash,而是 hash 槽的概念,以及自身支持节点设置从节点;
- 业务代码层实现,创建几个独立的 Redis 实例,在代码层对 key 进行 hash 计算,然后去对应的 Redis 实例操作数据。这种方式对 hash 层代码要求比较高,考虑部分包括,节点失效后的替代算法方案,数据震荡后的自动脚本恢复、实例的监控等。
28.Redis 如何做内存优化?
答:把相关的信息整体存储,而不是把每个信息独立存储,这样就可以有效的减少内存使用。
29.分布式锁设计时需要注意哪些事项?
答:通常分布式锁在设计时,需同时满足以下四个约束条件。
- 互斥性:在任意时刻,只有一个客户端能持有锁。
- 安全性:即不会形成死锁,当一个客户端在持有锁的期间崩溃而没有主动解锁的情况下,其持有的锁也能够被正确释放,并保证后续其他客户端能加锁。
- 可用性:就 Redis 而言,当提供锁服务的 Redis master 节点发生宕机等不可恢复性故障时,slave 节点能够升主并继续提供服务,支持客户端加锁和解锁;对基于分布式一致性算法实现的锁服务,如 ETCD 而言,当 leader 节点宕机时,follow 节点能够选举出新的 leader 继续提供锁服务。
- 对称性:对于任意一个锁,其加锁和解锁必须是同一个客户端,即,客户端 A 不能把客户端 B 加的锁给解了。
30.Redis 集群实现的原理是什么?
答:集群的实现原理和集群的实现方式有关,如下所述:
- Redis Sentinal 着眼于高可用,在 Master 宕机时会自动将 Slave 提升为 Master,继续提供服务;
- Redis Cluster 着眼于扩展性,在单个 Redis 内存不足时,使用 Cluster 进行分片存储。
31.Redis 常见的性能问题有哪些?
答:Redis 常见性能问题如下:
- 主服务器写内存快照,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,因此主服务器最好不要写内存快照;
- Redis 主从复制的性能问题,为了主从复制的速度和连接的稳定性,主从库最好在同一个局域网内。
更多推荐
所有评论(0)