百味皆苦 java后端开发攻城狮

八股文-web

2019-04-08
百味皆苦

servlet

91.什么是 Servlet?

Servlet 是用来处理客户端请求并产生动态网页内容的 Java 类。Servlet 主要是用来处理或者是存储 HTML 表单提交的数据,产生动态内容,在无状态的 HTTP 协议下管理状态信息。
92.说一下 Servlet 的体系结构。

所有的 Servlet 都必须要实现的核心的接口是 javax.servlet.Servlet。每一个 Servlet 都必须要直接或者是间接实现这个接口, 或者是继承 javax.servlet.GenericServlet 或者javax.servlet.http.HTTPServlet。最后,Servlet 使用多线程可以并行的为多个请求服务。
93.Applet 和 Servlet 有什么区别?

Applet 是运行在客户端主机的浏览器上的客户端 Java 程序。而 Servlet 是运行在 web 服务器上的服务端的组件。applet 可以使用用户界面类,而 Servlet 没有用户界面,相反,Servlet是等待客户端的 HTTP 请求,然后为请求产生响应。
94.GenericServlet 和 HttpServlet 有什么区别?

GenericServlet 是一个通用的协议无关的 Servlet,它实现了 Servlet 和 ServletConfig 接口。继承自 GenericServlet 的 Servlet 应该要覆盖 service()方法。最后,为了开发一个能用在网页上服务于使用 HTTP 协议请求的 Servlet,你的 Servlet 必须要继承自 HttpServlet。这里有 Servlet的例子。
95.解释下 Servlet 的生命周期。

Web容器加载Servlet并将其实例化后,Servlet生命周期开始,容器运行其init()方法进行Servlet的初始化;
请求到达时调用Servlet的service()方法,service()方法会根据需要调用与请求对应的doGet或doPost等方法;
当服务器关闭或项目被卸载时服务器会将Servlet实例销毁,此时会调用Servlet的destroy()方法。
init方法和destroy方法只会执行一次,service方法客户端每次请求Servlet都会执行。
Servlet中有时会用到一些需要初始化与销毁的资源,因此可以把初始化资源的代码放入init方法中,销毁资源的代码放入destroy方法中
96.doGet()方法和 doPost()方法有什么区别?

①get请求用来从服务器上获得资源,而post是用来向服务器提交数据;

②get将表单中数据按照name=value的形式,添加到action 所指向的URL 后面,并且两者使用"?"连接,而各个变量之间使用"&"连接;post是将表单中的数据放在HTTP协议的请求头或消息体中,传递到action所指向URL;

③get传输的数据要受到URL长度限制(最大长度是 2048 个字符);而post可以传输大量的数据,上传文件通常要使用post方式;

④使用get时参数会显示在地址栏上,如果这些数据不是敏感数据,那么可以使用get;对于敏感数据还是应用使用post;
97.什么是 Web 应用程序?

Web 应用程序是对 Web 或者是应用服务器的动态扩展。有两种类型的 Web 应用:面向表现的和面向服务的。面向表现的 Web 应用程序会产生包含了很多种标记语言和动态内容的交互的web页面作为对请求的响应。而面向服务的Web应用实现了 Web服务的端点(endpoint)。一般来说,一个 Web 应用可以看成是一组安装在服务器 URL 名称空间的特定子集下面的Servlet 的集合。
98.什么是服务端包含(Server Side Include)?

服务端包含(SSI)是一种简单的解释型服务端脚本语言,大多数时候仅用在 Web 上,用 servlet标签嵌入进来。SSI 最常用的场景把一个或多个文件包含到 Web 服务器的一个 Web 页面中。当浏览器访问 Web 页面的时候,Web 服务器会用对应的 servlet 产生的文本来替换 Web 页面中的 servlet 标签。
99.什么是 Servlet 链(Servlet Chaining)?

Servlet 链是把一个 Servlet 的输出发送给另一个 Servlet 的方法。第二个 Servlet 的输出可以发送给第三个 Servlet,依次类推。链条上最后一个 Servlet 负责把响应发送给客户端。 
100.如何知道是哪一个客户端的机器正在请求你的 Servlet?

ServletRequest 类可以找出客户端机器的 IP 地址或者是主机名。getRemoteAddr()方法获取客户端主机的 IP 地址,getRemoteHost()可以获取主机名。
101.HTTP 响应的结构是怎么样的?

状态码(Status Code):描述了响应的状态。可以用来检查是否成功的完成了请求。请求失败的情况下,状态码可用来找出失败的原因。如果 Servlet 没有返回状态码,默认会返回成功的状态码 HttpServletResponse.SC_OK。
HTTP 头部(HTTP Header):它们包含了更多关于响应的信息。比如:头部可以指定认为响应过期的过期日期,或者是指定用来给用户安全的传输实体内容的编码格式。如何在 Serlet中检索 HTTP 的头部看这里。 
主体(Body):它包含了响应的内容。它可以包含 HTML 代码,图片,等等。主体是由传输在HTTP 消息中紧跟在头部后面的数据字节组成的。
102.什么是 cookie?session 和 cookie 有什么区别?

Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式
Cookie 一般用来保存用户信息 比如①我们在 Cookie 中保存已经登录过得用户信息,下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了;②一般的网站都会有保持登录也就是说下次你再访问网站的时候就不需要重新登录了,这是因为用户登录的时候我们可以存放了一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写);③登录一次网站后访问网站其他页面不需要重新登录。

Session 的主要作用就是通过服务端记录用户的状态。 典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。

Cookie 数据保存在客户端(浏览器端),如果使用 Cookie 的一些敏感信息不要写入 Cookie 中,最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密。只能存放字符串
Session 数据保存在服务器端。安全性更高,可以存放对象
浏览器和 Servlet 通信使用的是什么协议?

浏览器和 Servlet 通信使用的是 HTTP 协议。
什么是 HTTP 隧道?

HTTP 隧道是一种利用 HTTP 或者是 HTTPS 把多种网络协议封装起来进行通信的技术。因此, HTTP 协议扮演了一个打通用于通信的网络协议的管道的包装器的角色。把其他协议的请求掩盖成 HTTP 的请求就是 HTTP 隧道。
105.sendRedirect()和 forward()方法有什么区别?

转发是服务器行为,重定向是客户端行为。
转发(Forward) 通过RequestDispatcher对象的forward方法实现的。
request.getRequestDispatcher("login_success.jsp").forward(request, response);
forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容再发给浏览器.浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址。
forward:转发页面和转发到的页面可以共享request里面的数据
forward:一般用于用户登陆的时候,根据角色转发到相应的模块。

重定向(Redirect) 是利用服务器返回的状态码来实现的。客户端浏览器请求服务器的时候,服务器会返回一个状态码。服务器通过 HttpServletResponse 的 setStatus(int status) 方法设置状态码。如果服务器返回301或者302,则浏览器会到新的网址重新请求该资源。
redirect是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址.所以地址栏显示的是新的URL。
redirect:不能共享数据。
redirect:一般用于用户注销登陆时返回主页面和跳转到其它的网站等
106.什么是 URL 编码和 URL 解码?

URL 编码是负责把 URL 里面的空格和其他的特殊字符替换成对应的十六进制表示,反之就是解码。
如何让页面做到自动刷新?

自动刷新不仅可以实现一段时间之后自动跳转到另一个页面,还可以实现一段时间之后自动刷新本页面。
Servlet中通过HttpServletResponse对象设置Header属性实现自动刷新

Response.setHeader("Refresh","5;URL=http://localhost:8080/servlet/example.htm");
其中5为时间,单位为秒。URL指定就是要跳转的页面(如果设置自己的路径,就会实现每过5秒自动刷新本页面一次)
如何解决servlet的线程安全问题?

Servlet不是线程安全的,多线程并发的读写会导致数据不同步的问题。
解决的办法是尽量不要定义name属性,而是要把name变量分别定义在doGet()和doPost()方法内。
多线程的并发的读写Servlet类属性会导致数据不同步。但是如果只是并发地读取属性而不写入,则不存在数据不同步的问题。因此Servlet里的只读属性最好定义为final类型的。

JSP

107.什么是 JSP 页面?

JSP 页面是一种包含了静态数据和 JSP 元素两种类型的文本的文本文档。静态数据可以用任
何基于文本的格式来表示,比如:HTML 或者 XML。JSP 是一种混合了静态内容和动态产生
的内容的技术。
108.JSP 请求是如何被处理的?

浏览器首先要请求一个以.jsp 扩展名结尾的页面,发起 JSP 请求,然后,Web 服务器读取这个请求,使用 JSP 编译器把 JSP 页面转化成一个 Servlet 类。需要注意的是,只有当第一次请求页面或者是 JSP 文件发生改变的时候 JSP 文件才会被编译,然后服务器调用 servlet 类,处理浏览器的请求。一旦请求执行结束,servlet 会把响应发送给客户端。
109.JSP 有什么优点?

JSP 页面是被动态编译成 Servlet 的,因此,开发者可以很容易的更新展现代码。 
JSP 页面可以被预编译。 
JSP 页面可以很容易的和静态模板结合,包括:HTML 或者 XML,也可以很容易的和产生动态内容的代码结合起来。 
开发者可以提供让页面设计者以类 XML 格式来访问的自定义的 JSP 标签库。 
开发者可以在组件层做逻辑上的改变,而不需要编辑单独使用了应用层逻辑的页面。
110.include指令include的行为的区别?

include指令: JSP可以通过include指令来包含其他文件。被包含的文件可以是JSP文件、HTML文件或文本文件。包含的文件就好像是该JSP文件的一部分,会被同时编译执行。 语法格式如下: <%@ include file="文件相对 url 地址" %>

include动作: <jsp:include>动作元素用来包含静态和动态的文件。该动作把指定文件插入正在生成的页面。语法格式如下: <jsp:include page="相对 URL 地址" flush="true" />
112.什么是 Scriptlets?

JSP 技术中,scriptlet 是嵌入在 JSP 页面中的一段 Java 代码。scriptlet 是位于标签内部的所有的东西,在标签与标签之间,用户可以添加任意有效的 scriplet。 
113.声明(Decalaration)在哪里?

声明跟 Java 中的变量声明很相似,它用来声明随后要被表达式或者 scriptlet 使用的变量。添加的声明必须要用开始和结束标签包起来。
114.什么是表达式(Expression)?

JSP 表达式是 Web 服务器把脚本语言表达式的值转化成一个 String 对象,插入到返回给客户端的数据流中。表达式是在<%=和%>这两个标签之间定义的。 
115.隐含对象是什么意思?有哪些隐含对象?

JSP 隐含对象是页面中的一些 Java 对象,JSP 容器让这些 Java 对象可以为开发者所使用。开发者不用明确的声明就可以直接使用他们。JSP 隐含对象也叫做预定义变量。下面列出了 JSP页面中的隐含对象: 
application :封装服务器运行环境的对象;
page :JSP页面本身(相当于Java程序中的this);
request :封装客户端的请求,其中包含来自GET或POST请求的参数;
response :封装服务器对客户端的响应;
session :封装用户会话的对象;
exception :封装页面抛出异常的对象。
out :输出服务器响应的输出流对象;
config :Web应用的配置对象;
pageContext:通过该对象可以获取其他对象;
jsp 和Servlet 有什么区别?

jsp是html页面中内嵌的Java代码,侧重页面显示;
Servlet是html代码和Java代码分离,侧重逻辑控制,mvc设计思想中jsp位于视图层,servlet位于控制层
JVM只能识别Java类,并不能识别jsp代码!web容器收到以.jsp为扩展名的url请求时,会将访问请求交给tomcat中jsp引擎处理,每个jsp页面第一次被访问时,jsp引擎将jsp代码解释为一个servlet源程序,接着编译servlet源程序生成.class文件,再有web容器servlet引擎去装载执行servlet程序,实现页面交互
request.getAttribute()和 request.getParameter()有何区别?

从获取方向来看:
getParameter()是获取 POST/GET 传递的参数值;
getAttribute()是获取对象容器中的数据值;

从用途来看:
getParameter()用于客户端重定向时,即点击了链接或提交按扭时传值用,即用于在用表单或url重定向传值时接收数据用。返回的是String,用于读取提交的表单中的值;
getAttribute() 用于服务器端重定向时,返回的是Object,需进行转换,可用setAttribute()设置成任意对象,使用很灵活,可随时用
JSP中的四种作用域是那几个?

page代表与一个页面相关的对象和属性。
request代表与Web客户机发出的一个请求相关的对象和属性。一个请求可能跨越多个页面,涉及多个Web组件;需要在页面显示的临时数据可以置于此作用域。
session代表与某个用户与服务器建立的一次会话相关的对象和属性。跟某个用户相关的数据应该放在用户自己的session中。
application代表与整个Web应用程序相关的对象和属性,它实质上是跨越整个Web应用程序,包括多个页面、请求和会话的一个全局作用域。

Tomcat

Tomcat的缺省端口是多少,怎么修改?

tomcat默认的端口是8080,还会占用8005,8009和8443端口。在server.xml中修改
tomcat容器是如何创建servlet类实例?用到了什么原理?

当容器启动时,会读取在webapps目录下所有的web应用中的web.xml文件,然后对xml文件进行解析,
并读取servlet注册信息。然后,将每个应用中注册的servlet类都进行加载,并通过反射的方式实例化。
(有时候也是在第一次请求时实例化)在servlet注册时加上如果为正数,则在一开始就实例化,
如果不写或为负数,则第一次请求实例化。
怎样进行内存调优?

内存方式的设置是在catalina.sh,在catalina.bat中,调整一下JAVA_OPTS变量即可,因为后面的启动参数会把JAVA_OPTS作为JVM的启动参数来处理。
tomcat 有那几种Connector 运行模式?

bio(blocking I/O)是指阻塞式I/O操作,Tomcat在默认情况下就是以bio模式运行的。当客户端多时,会创建大量的处理线程。每个线程都要占用栈空间和一些CPU时间。阻塞可能带来频繁的上下文切换,而大部分的上下文切换是无意义的。就一般而言,bio模式是三种运行模式中性能最低的一种。

nio(non-blocking I/O)是非阻塞I/O操作。nio是一个基于缓冲区并能提供非阻塞I/O操作的Java API,它拥有比bio更好的并发运行性能。由一个专门的线程来处理所有的 I/O 事件、并负责分发。 事件驱动机制,而不再同步地去监视事件。 线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的,减少无谓的线程切换。NIO采用了双向通道(channel)进行数据传输,而不是单向的流(stream)。

apr(Apache portable Run-time libraries/Apache可移植运行库)是Apache HTTP服务器的支持库。在apr模式下,Tomcat将以JNI(Java Native Interface)的形式调用Apache HTTP服务器的核心动态链接库来处理文件读取或网络传输操作,从而大大提高Tomcat对静态文件的处理性能。Tomcat apr是在Tomcat上运行高并发应用的首选模式。

JDBC

72.什么是 JDBC?

JDBC 是允许用户在不同数据库之间做选择的一个抽象层。JDBC 允许开发者用 JAVA 写数据库应用程序,而不需要关心底层特定数据库的细节。
73.解释下驱动(Driver)在 JDBC 中的角色。

JDBC 驱动提供了特定厂商对 JDBC API 接口类的实现,驱动必须要提供 java.sql 包下面这些类的实现:Connection, Statement, PreparedStatement,CallableStatement, ResultSet 和 Driver。
74.Class.forName()方法有什么作用?

这个方法用来载入跟数据库建立连接的驱动。 
75.PreparedStatement 比 Statement 有什么优势?

PreparedStatements 是预编译的,因此,性能会更好。
代码的可读性和可维护性。Statement需要不断地拼接,而PreparedStatement不会。
PreparedStatement尽最大可能提高性能。DB有缓存机制,相同的预编译语句再次被调用不会再次需要编译
最重要的一点是极大地提高了安全性。Statement容易被SQL注入,而PreparedStatement传入的内容不会和sql 语句发生任何匹配关系
76.什么时候使用 CallableStatement?用来准备 CallableStatement 的方法是什么?

CallableStatement 用来执行存储过程。存储过程是由数据库存储和提供的。存储过程可以接受输入参数,也可以有返回结果。非常鼓励使用存储过程,因为它提供了安全性和模块化。
准备一个 CallableStatement 的方法是: 
CallableStament.prepareCall();
77.数据库连接池是什么意思?

像打开关闭数据库连接这种和数据库的交互可能是很费时的,尤其是当客户端数量增加的时候,会消耗大量的资源,成本是非常高的。可以在应用服务器启动的时候建立很多个数据库连接并维护在一个池中。连接请求由池中的连接提供。在连接使用完毕以后,把连接归还到池中,以用于满足将来更多的请求。 
数据库连接池的工作机制是什么?

因为创建连接和关闭连接的行为是非常耗时的,会显著降低软件的性能表现。解决办法就是先创建N条数据库连接Connection,循环使用,但是不进行关闭,这样再执行sql语句,就不需要额外创建连接了,直接使用现成的连接就可以了,从而节约了创建连接和关闭连接的时间开销。
说下原生JDBC操作数据库流程?

第一步:Class.forName()加载数据库连接驱动
第二步:DriverManager.getConnection()获取数据连接对象
第三步:根据SQL获取sql会话对象,有2种方式 Statement、PreparedStatement 
第四步:执行SQL,执行SQL前如果有参数值就设置参数值setXXX()
第五步:处理结果集
第六步:关闭结果集、关闭会话、关闭连接
http的长连接和短连接区别?

HTTP协议有HTTP/1.0版本和HTTP/1.1版本。HTTP1.1默认保持长连接(HTTP persistent connection,也翻译为持久连接),数据传输完成了保持TCP连接不断开(不发RST包、不四次握手),等待在同域名下继续用这个通道传输数据;相反的就是短连接。
在 HTTP/1.0 中,默认使用的是短连接。也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。从HTTP/1.1起,默认使用的是长连接,用以保持连接特性。
http常见的状态码有哪些?

200 OK 客户端请求成功
301 Moved Permanently(永久移除),请求的URL已移走
302 found 重定向
400 Bad Request 客户端请求有语法错误,不能被服务器所理解
401 Unauthorized 请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
403 Forbidden 服务器收到请求,但是拒绝提供服务
404 Not Found 请求资源不存在,eg:输入了错误的URL
500 Internal Server Error 服务器发生不可预期的错误
503 Server Unavailable 服务器当前不能处理客户端的请求,一段时间后可能恢复正常
session共享怎么做的(分布式如何实现session共享)?

问题描述:一个用户在登录成功以后会把用户信息存储在session当中,这时session所在服务器为server1,那么用户在session失效之前如果再次使用app,那么可能会被路由到 server2,这时问题来了,server2没有该用户的session,所以需要用户重新登录,这时的用户体验会非常不好,所以我们想如何实现多台server之间共享session,让用户状态得以保存。

可以利用gemfire实现session复制共享,还可以session维护在redis中实现session共享,同时可以将session维护在客户端的cookie中,但是前提是数据要加密。这三种方式可以迅速切换,而不影响应用正常执行。我们在实践中,首选gemfire或者redis作为session共享的载体,一旦session不稳定出现问题的时候,可以紧急切换cookie维护session作为备用,不影响应用提供服务。
在单点登录中,如果cookie被禁用了怎么办?

单点登录的原理是后端生成一个sessionID,然后设置到cookie,后面的所有请求浏览器都会带上cookie,然后服务端从cookie里获取sessionID,再查询到用户信息。所以,保持登录的关键不是cookie,而是通过cookie保存和传输的sessionID,其本质是能获取用户信息的数据。除了cookie,还通常使用HTTP请求头来传输。但是这个请求头浏览器不会像cookie一样自动携带,需要手工处理。

Linux

说一下常用的Linux命令?

列出文件列表:ls【参数 -a -l】
创建目录和移除目录:mkdir rmdir
用于显示文件后几行内容:tail
打包:tar -xvf
打包并压缩:tar -zcvf
查找字符串:grep
显示当前所在目录:pwd
创建空文件:touch
编辑器:vim vi
Linux中如何查看日志?

动态打印日志信息:tail –f 日志文件
Linux怎么关闭进程?

通常用ps查看进程PID,用kill命令终止进程。ps命令用于查看当前正在运行的进程。grep是搜索;-aux显示所有状态;

例如:
ps –ef | grep java表示查看所有进程里CMD是java的进程信息。
ps –aux | grep java
kill命令用于终止进程。例如:kill -9 [PID]   -9表示强迫进程立即停止。

mysql

MySQL中四种隔离级别分别是什么?

读未提交(READ UNCOMMITTED):未提交读隔离级别也叫读脏,就是事务可以读取其它事务未提交的数据。
读已提交(READ COMMITTED):在其它数据库系统比如SQL Server默认的隔离级别就是提交读,已提交读隔离级别就是在事务未提交之前所做的修改其它事务是不可见的。
可重复读(REPEATABLE READ):保证同一个事务中的多次相同的查询的结果是一致的,比如一个事务一开始查询了一条记录然后过了几秒钟又执行了相同的查询,保证两次查询的结果是相同的,可重复读也是MySQL的默认隔离级别。
可串行化(SERIALIZABLE):可串行化就是保证读取的范围内没有新的数据插入,比如事务第一次查询得到某个范围的数据,第二次查询也同样得到了相同范围的数据,中间没有新的数据插入到该范围中。
SQL之连接查询(左连接和右连接的区别)?

左连接(左外连接):以左表作为基准进行查询,左表数据会全部显示出来,右表如果和左表匹配的数据则显示相应字段的数据,如果不匹配则显示为null。
右连接(右外连接):以右表作为基准进行查询,右表数据会全部显示出来,左表如果和右表匹配的数据则显示相应字段的数据,如果不匹配则显示为null。
全连接:先以左表进行左外连接,再以右表进行右外连接。
内连接:显示表之间有连接匹配的所有行。
left join on后面 加条件 与 where后面加条件的区别?

数据库在通过连接两张或多张表来返回记录时,都会生成一张中间的临时表,然后再将这张临时表返回给用户。
1、 on条件是在生成临时表时使用的条件,它不管on中的条件是否为真,都会返回左边表中的记录。
2、where条件是在临时表生成好后,再对临时表进行过滤的条件。这时已经没有left join的含义(必须返回左边表的记录)了,条件不为真的就全部过滤掉。
数据库三范式是什么?

1.	第一范式(1NF):字段具有原子性,不可再分。(所有关系型数据库系统都满足第一范式数据库表中的字段都是单一属性的,不可再分)
2.	第二范式(2NF)是在第一范式(1NF)的基础上建立起来的,即满足第二范式(2NF)必须先满足第一范式(1NF)。要求数据库表中的每个实例或行必须可以被惟一地区分。通常需要为表加上一个列,以存储各个实例的惟一标识。这个惟一属性列被称为主关键字或主键。
3.	满足第三范式(3NF)必须先满足第二范式(2NF)。简而言之,第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。

函数

SQL中聚合函数有哪些?

聚合函数是对一组值进行计算并返回单一的值的函数,它经常与select语句中的group by子句一同使用。
avg():返回的是指定组中的平均值,空值被忽略。
count():返回的是指定组中的项目个数。
max():返回指定数据中的最大值。
min():返回指定数据中的最小值。
sum():返回指定数据的和,只能用于数字列,空值忽略。

存储引擎

什么事MySQL的redo log 和 binlog?

MySQL 分两层:Server 层和引擎层。区别如下:
Server 层:主要做的是 MySQL 功能层面的事情。Server 层也有自己的日志,称为 binlog(归档日志)
binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

引擎层:负责存储相关的具体事宜。redo log 是 InnoDB 引擎特有的日志。
redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
redo log 是循环写的,空间固定会用完;

MyISAM存储引擎了解吗?

MyISAM是MySQL官方提供默认的存储引擎,其特点是不支持事务、表锁和全文索引,对于一些OLAP(联机分析处理)系统,操作速度快。
每个MyISAM在磁盘上存储成三个文件。文件名都和表名相同,扩展名分别是.frm(存储表定义)、.MYD(MYData,存储数据)、.MYI(MYIndex,存储索引)。这里特别要注意的是MyISAM不缓存数据文件,只缓存索引文件。
InnoDB存储引擎了解吗?

InnoDB存储引擎支持事务,主要面向OLTP(联机事务处理过程)方面的应用,其特点是行锁设置、支持外键,并支持类似于Oracle的非锁定读,即默认情况下读不产生锁。InnoDB将数据放在一个逻辑表空间中(类似Oracle)。
InnoDB通过多版本并发控制来获得高并发性,实现了ANSI标准的4种隔离级别,默认为Repeatable,使用一种被称为next-keylocking的策略避免幻读。
对于表中数据的存储,InnoDB采用类似Oracle索引组织表Clustered的方式进行存储。InnoDB存储引擎提供了具有提交、回滚和崩溃恢复能力的事务安全。但是对比myisam的存储引擎,InnoDB写的处理效率差一些并且会占用更多的磁盘空间以保留数据和索引。

优化

mysql怎样优化分页查询?

根据主键id,可以用到索引
select * from table where id > ? order by id  limit  ? ,?
SQL之sql注入是什么?

通过在Web表单中输入(恶意)SQL语句得到一个存在安全漏洞的网站上的数据库,而不是按照设计者意图去执行SQL语句。
举例:
当执行的sql为select * from user where username = “admin” or “a” = “a”时,sql语句恒成立,参数username毫无意义。
防止sql注入的方式?

预编译语句:如,select * from user where username = ?,sql语句语义不会发生改变,sql语句中变量用?表示,即使传递参数时为“admin or ‘a’ = ‘a’”,也会把这整体当做一个字符串去查询。
Mybatis框架中的mapper方式中的#也能很大程度的防止sql注入($无法防止sql注入)
MySQL性能优化有哪些?

当只要一行数据时使用limit 1 :查询时如果已知会得到一条数据,这种情况下加上limit 1会增加性能。因为MySQL数据库引擎会在找到一条结果停止搜索,而不是继续查询下一条是否符合标准直到所有记录查询完毕。

选择正确的数据库引擎:MyISAM适用于一些大量查询的应用,但对于有大量写功能的应用不是很好。甚至你只需要update一个字段整个表都会被锁起来。而别的进程就算是读操作也不行要等到当前update操作完成之后才能继续进行。另外,MyISAM对于select count(*)这类操作是超级快的。InnoDB的趋势会是一个非常复杂的存储引擎,对于一些小的应用会比MyISAM还慢,但是支持“行锁”,所以在写操作比较多的时候会比较优秀。并且,它支持事务。

用not exists代替not in :not exists用到了连接能够发挥已经建立好的索引的作用,not in不能使用索引。not in是最慢的方式要同每条记录比较,在数据量比较大的操作红不建议使用这种方式。

对操作符的优化,尽量不采用不利于索引的操作符:如:in、not in、is null、is not null 、<> 等某个字段总要拿来搜索,为其建立索引:MySQL中可以利用alter table语句来为表中的字段添加索引,语法为:alter table表名add index(字段名)
有哪些数据库优化方面的经验?

1.	用PreparedStatement, 一般来说比Statement性能高:一个sql 发给服务器去执行,涉及步骤:语法检查、语义分析, 编译,缓存。
2.	有外键约束会影响插入和删除性能,如果程序能够保证数据的完整性,那在设计数据库时就去掉外键。
3.	表中允许适当冗余,譬如,主题帖的回复数量和最后回复时间等
4.	UNION ALL 要比UNION快很多,所以,如果可以确认合并的两个结果集中不包含重复数据且不需要排序时的话,那么就使用UNION ALL。UNION在进行表链接后会筛选掉重复的记录,Union All不会去除重复记录。Union将会按照字段的顺序进行排序;UNION ALL只是简单的将两个结果合并后就返回。
一张表,里面有ID 自增主键,当insert 了17 条记录之后,删除了第15,16,17 条记录,再把Mysql 重启,再insert 一条记录,这条记录的ID 是18 还是15 ?

如果表的类型是myisam,那么是18,因为myisam会把自增主键的最大ID保存在数据文件里,重启MySQL也不会丢失。
如果表的类型是innodb,那么是15,因为innodb会把自增主键的最大ID保存在内存中,重启后或对表进行OPTIMIZE操作,都会导致最大ID丢失。
在Mysql 中ENUM 的用法是什么?

ENUM 是一个字符串对象,用于指定一组预定义的值,并可在创建表时使用。
Create table size(name ENUM('Smail,'Medium','Large');
CHAR 和VARCHAR 的区别?

以下是CHAR 和VARCHAR 的区别:
CHAR 和VARCHAR 类型在存储和检索方面有所不同
CHAR 列长度固定为创建表时声明的长度,长度值范围是1 到255
当CHAR 值被存储时,它们被用空格填充到特定长度,检索CHAR 值时需删除尾随空格。
如果一个表有一列定义为TIMESTAMP,将发生什么?

每当行被更改时,时间戳字段将获取当前时间戳。
列设置为AUTO INCREMENT 时,如果在表中达到最大值,会发生什么情况?

它会停止递增,任何进一步的插入都将产生错误,因为密钥已被使用。
怎样才能找出最后一次插入时分配了哪个自动增量?

LAST_INSERT_ID 将返回由Auto_increment 分配的最后一个值,并且不需要指定表名称。
BLOB 和TEXT 有什么区别?

BLOB 和TEXT 类型之间的唯一区别在于对BLOB 值进行排序和比较时区分大小写,对TEXT 值不区分大小写。
MySQL当记录不存在时insert,当记录存在时update,语句怎么写?

INSERT INTO table (a,b,c) VALUES (1,2,3) ON DUPLICATE KEY UPDATE c=c+1;

锁机制

Mysql 中有哪几种锁?

MyISAM 支持表锁,InnoDB 支持表锁和行锁,默认为行锁
表级锁:开销小,加锁快,不会出现死锁。锁定粒度大,发生锁冲突的概率最高,并发量最低
表级锁: 意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁。 意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁。
 IS、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录。就是说当对一个行加锁之后,如果有打算给行所在的表加一个表锁,必须先看看该表的行有没有被加锁,否则就会出现冲突。IS锁和IX锁就避免了判断表中行有没有加锁时对每一行的遍历。直接查看表有没有意向锁就可以知道表中有没有行锁。

行级锁:开销大,加锁慢,会出现死锁。锁力度小,发生锁冲突的概率小,并发度最高
行级锁: 共享锁(S Lock),允许事务读一行数据。 排他锁(X Lock),允许事务删除或更新一行数据。

若将上锁的对象看成一颗树,那么对最下层的对象上锁,就是对最细粒度的对象进行上锁,首先就需要对粗粒度的对象上锁,
比如需要给某记录上 X 锁,那么就需要先对数据库 A、表、页 上意向锁 IX,最后对对记录上 X 锁。 2、默认读操作不加锁,走 MVCC 多版本控制机制。
为什么要有意向锁? 如果加了行锁,肯定之前就会给加上意向锁,有其他事务想要锁住表,先看有没有表级意向锁,这样就不用到记录上看有没有 S 或 X 锁,优点就是快,省了很多步骤。

例子
图书馆有很多层,每一层有很多房间,二楼的 201 房间正在装修,不能进入,相当于给 201 房间加了排他锁,然后在二楼门口立了一个警示牌:二楼有人正在装修(相当于加了一个意向排他锁)。 到图书馆关门的点了,管理员开始检查各楼栋是否还有人,发现二楼有个警示牌,呀,还有人在装修啊,暂时就不能关门了。这里把 201 房间当做某行记录,二楼当做表。装修比作事务 1,管理员要关闭图书馆比作事务 2。
数据库怎样保证并发更新不出错?

可以利用数据库锁机制来解决并发更新问题。

悲观锁解决并发更新
MySQL 的 InnoDB 引擎提供了以下两种行锁机制。在查询记录时,使用以下 SQL,可以给对应行加上共享锁和排他锁。
-- 共享锁(S)
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
-- 排他锁(X)并发更新可以通过该机制保证数据一致性
SELECT * FROM table_name WHERE ... FOR UPDATE
需要注意的是,锁在 autocommit=0 状态下使用才有意义,因为锁会在 commit 之后自动释放。默认情况下 MySQL 单行语句就是一个事务,加锁语句执行完,锁立即就被释放了,也就没意义了
下面给出 SELECT ... FOR UPDATE 解决并发更新的示例
-- A和B开启事务
BEGIN;
-- A查询,加上排他锁
SELECT * FROM item WHERE id = 1 FOR UPDATE;
-- B查询加锁,由于锁被A占用,所以阻塞
SELECT * FROM item WHERE id = 1 FOR UPDATE;
-- A更新
UPDATE item SET quantity = quantity - 1 WHERE id = 1;
-- A提交
COMMIT;
-- B成功查询出记录,继续执行更新
UPDATE item SET quantity = quantity - 1 WHERE id = 1;
-- B提交
COMMIT;


乐观锁解决并发更新
乐观锁本质上不加锁,是一种 CAS 无锁机制。所谓 CAS,就是在更新的时候,检查该实际值是不是和期望值一样,一样就更新成功,不一样就更新失败
-- A 查出来 quantity = 100
SELECT * FROM item WHERE id = 1;
-- B 查出来 quantity = 100
SELECT * FROM item WHERE id = 1;
-- A 更新 quantity,同时加上 where 条件检查 quantity 是不是期望值。发现是,更新成功
UPDATE item SET quantity = quantity - 1 WHERE id = 1 AND quantity = 100;
-- B 更新 quantity,发现 quantity 不是期望值,更新失败
UPDATE item SET quantity = quantity - 1 WHERE id = 1 AND quantity = 100;

CAS 存在更新失败的情况。如何判断更新是否失败呢?这也很简单,UPDATE 语句返回值代表更新的行数,直接判断返回值是不是 0 即可,0 就是失败。
现在我们可以判断更新失败了,那如何解决呢?这个得具体业务具体解决了。如果业务容许这种错误发现,可以给用户一个错误提示
如果业务不容许失败,这时候可以加一个死循环进行重试

乐观锁 CAS 的 ABA 问题
这时候需要单独引入一个版本号或时间戳字段来解决
SELECT * FROM item WHERE id = 1;
UPDATE item SET quantity = quantity - 1, version = version + 1 WHERE id = 1 AND version = 预期版本号
对比一下悲观锁和乐观锁?

悲观锁和乐观锁都是抽象概念,而且都是针对并发更新场景提出的,物理上不存在对应的锁。

悲观锁,去查数据的时候都悲观地认为别人会修改,所以每次查数据时直接上锁。排他锁是悲观锁的一种实现方案

乐观锁,相对悲观锁而言,查数据时认为一般不会被修改,所以只在更新数据时检测冲突。CAS 是乐观锁的一种具体实现

写多读少用悲观锁,读多写少用乐观锁

举个例子,假设有10万并发,其中有几个是更新操作,其它都是读操作,这时候就特别适合使用乐观锁。对于更新操作,由于请求数较少,CAS 冲突概率就小,大部分都是成功的。对于读操作,由于没有加锁,就没有性能响应

假设有10万并发,有几个是读操作,其它都是写操作。如果使用乐观锁,CAS 冲突概率极大,大部分都是更新失败。如果还有循环不停地进行 CAS 操作,一个是应用的 CPU 开销过大,一个是给数据库带来过多的并发,严重影响性能。这时候就使用悲观锁,直接上锁。

如果 CAS 业务上存在 ABA 问题,那么就得加版本号或时间戳字段。

如果不存在 ABA 问题的话,直接通过业务字段本身来检测冲突即可,没有必要再引入额外字段
MySQL 怎么保证原子性的?

我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚,在 MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用 回滚日志 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。
什么事undo log 和 mvcc ?

【参考自:https://www.cnblogs.com/rongdi/p/13378892.html】
mysql中读取方式按照是否需要传统意义的锁,分为锁定读和非锁定读两种。锁定读不用多说,那就一堆算法了,行锁,间隙锁,next-key锁,无非就是为了保证,一个事务中锁定读取一条或者多条数据时,不能读到别的事务没有提交的更改(不能脏读),不能同一个事务两次读到的数据内容不一致(应该要可重复读),不能同一个事务,两次读到的数据条数都不一致(不能幻读)。针对最常见的RR隔离级别,什么情况下至少使用什么类型的锁(行锁,gap锁,next-key锁)才能避免脏读,不可重复读,幻读。又会联系到几个事务级别,从锁粒度从小到大,RU(读未提交),RC(读已提交),RR(可重复读),S(序列化)

mvcc作为多版本并发控制,使用undo log实现,同样也可以实现上述四种隔离级别,只不过实现手段不是通过传统意义上的锁罢了。当然针对RU(读未提交)隔离级别,所有更改的语句别的事务都可以直接看到,那根本没有保留多个版本的必要,用到的就是最新的唯一版本,同样S(序列化)级别,排队一个个去读写,也根本没有保留多个版本数据的必要,因为都是用最新的数据就行了。

官方一点的解释:并发访问(读或写)数据库时,对正在事务内处理的数据做 多版本的管理。以达到用来避免写操作的堵塞,从而引发读操作的并发问题。

MVCC是做什么的?用于事务的回滚,mvcc

undo log我们关注的类型有哪些?insert undo log;update undo log

InnoDB中的MVCC实现原理:数据表增加两个隐藏列DATA_TRX_ID和DATA_ROLL_PTR,用于实现mvcc

事务 A 对值 x 进行更新之后,该行即产生一个新版本和旧版本。假设之前插入该行的事务 ID 为 100,事务 A 的 ID 为 200。操作过程如下
1)对 ID = 1 的记录加排他锁,毕竟要修改了,总不能加共享锁把
2)把该行原本的值拷贝到 undo log 中
3)修改改行值并且更新 DATA_TRX_ID,将 DATA_ROLL_PTR 指向刚刚拷贝到 undo log 链中的旧版本记录,记住undo log是个链表,如果多个事务多次修改会继续生成undo log并通过DATA_ROLL_PTR建立指向关系

上文中的undo log是一个链表结构,也就是如果多个事务都修改了这行数据,会根据事务ID的先后,以链表形式存放,至于旧版本存放在链表的先后顺序,这个其实无关紧要,只要方便获取就好,我倾向于每次修改后把旧版放在链表的头部,这样可以保证从指针递归下来,先找到较新的数据,再找到更旧的数据,一个个版本去判断是否是自己可以看到的版本。

那么现在的核心问题就是当前事务读取数据的时候如何判断应该读取哪个版本?mysql中引入了一个可读试图ReadView的概念。主要包含如下属性
1)mIds 代表生成ReadView时,当前活跃所有的事务ID,活跃的意思就是事务开启了还没提交,这里可以提一点,事务开启事务ID会自增,实际上事务ID就是一个全局自增的数字
2)min_trx_id 表示当前活跃的mIds中最小的事务ID
3)max_trx_id 表示生成ReadView时,最大的事务ID,这里一定不要理解成mIds中最大的ID,这是一个相当错误的理解,后面再解释
4)creator_trx_id 该ReadView在那个事务里创建的,

ReadView有了上面4个属性后,那么应该以什么样的规则,判断当前事务到底可以读取哪个版本的数据呢?
1)如果被访问版本的 data_trx_id 小于 m_ids 中的最小值,说明生成该版本的事务在 ReadView 生成前就已经提交了,那么该版本可以被当前事务访问。
2)如果被访问版本的 data_trx_id大于当前事务的最大值,说明生成该版本数据的事务在生成 ReadView 后才生成,那么该版本不可以被当前事务访问。为什么这里的最大值不是mIds的最大值,因为事务ID虽然是全局递增的,但是并不代表事务ID大的一定要在事务ID小的后面提交,也就是事务开启有先后,但是事务结束的先后和开启的先后并不是完全一致的,毕竟事务有长有短。如果此时数据的事务版本是200,而mIds中没有200,那么mIds最大值就可能小于200,那么以规则2判断就可能让本该可以访问到的数据因为这个规则,而访问不到了,归根结底就是因为没有正确找到生成ReadView时的最大事务ID,所以不能肯定的说生成该版本数据的事务在生成 ReadView 后才生成
3)如果被访问版本的 data_trx_id属性值在 最大值和最小值之间(包含),那就需要判断一下 trx_id 的值是不是在 m_ids 列表中。如果在,说明创建 ReadView 时生成该版本所属事务还是活跃的,因此该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

通俗点来说,也就是ReadView中通过最大事务ID,mIds最小事务ID,mIds活跃事务列表,将当前要读的数据的事务ID分成了3种情况,要么小于mIds的最小事务ID,很明显又在当前活跃的最小事务之前生成,又不在活跃事务中,一定是已提交的事务,这个版本肯定可以访问;要么大于生成ReadView的当前的最大事务ID,很明显在所有活跃事务之后,并且也不可能存在于活跃事务列表中,那么就说明,该版本在当前活跃事务之后才出现,总不能读取到未来的版本吧;要么处于最大最小值之间,这时候就有两种情况,因为并不是说最大最小值之间就一定是活跃的,毕竟先开启的事务并不一定会先结束,事务有大小长短,这时候就很简单,在mIds中就是还没提交的活跃版本,不可被读取,不在就是已经提交的版本,可以被读取。当一个事务要读取一行数据,首先用上面规则判断数据的最新版本也就是那行记录,如果发现可以访问就直接读取了,如果发现不能访问,就通过DATA_ROLL_PTR指针找到undo log,递归往下去找每个版本,直到读取到自己可以读取的版本为止,如果读取不到那就返回空呗。

MVCC在RC和RR隔离级别下有啥区别?
很明显,如果是RC级别,那么事务A两次读取到的分别是10和20,如果是RR级别两次读取到的都是10,如果同样由ReadView判断需要怎么样才能区分两个隔离级别取的版本不一样呢?先说RC级别,两个版本不一致,说明可能事务A两次使用的ReadView里的内容肯定是有不一样,结合B事务中间有提交,而提交事务很明显会影响到mIds当前活跃事务列表,因为事务提交之后就不是活跃事务了不可能再出现在mIds列表中了,这一点很好理解。再来看RR隔离级别事务A,如果要两次读取的x值一致,除非两次用来判定的ReadView没有啥变化,这不由得让我们想起了缓存的用法,是不是可以在A事务开启的时候生成一个ReadView,然后在整个A事务期间都用这一份ReadView就行了呢,就像用缓存一样。而RC级别每次查询都生成一个最新的ReadView,是不是就可以产生区别了,这算是一个比较常规并且巧妙的设计了。

索引

请简述常用的索引结构和索引类型有哪些种类?

索引是一种用于快速查询和检索数据的数据结构。常见的索引结构有: B 树, B+树和 Hash。

1.	普通索引: 即针对数据库表创建索引
2.	唯一索引: 与普通索引类似,不同的就是:MySQL数据库索引列的值必须唯一,但允许有空值
3.	主键索引: 它是一种特殊的唯一索引,不允许有空值。一般是在建表的时候同时创建主键索引
4.	组合索引: 为了进一步榨取MySQL的效率,就要考虑建立组合索引。即将数据库表中的多个字段联合起来作为一个组合索引。
建立索引的优势,影响,原则是什么?

索引的优势? 检索速度:快速访问数据表中的特定信息,提高检索速度。 唯一性:创建唯一性索引,保证数据库表中每一行数据的唯一性。 加速连接:加速表和表之间的连接。 减少分组和排序的时间:使用分组和排序进行数据检索时,可以显著减少查询中分组和排序的时间。

索引的负面影响? 耗时:创建索引和维护索引需要耗费时间,这个时间随着数据量的增加而增加。 占空间:索引需要占用物理空间,不光是表需要占用数据空间,每个索引也需要占用物理空间。 维护速度:当对表进行增、删、改、的时候索引也要动态维护,这样就降低了数据的维护速度。

为数据表建立索引的原则有哪些? 在最频繁使用的、用以缩小查询范围的字段上建立索引。 在频繁使用的、需要排序的字段上建立索引。

什么情况下不适合建立索引? 对于查询中很少涉及的列或者重复值比较多的列,不宜建立索引。 对于一些特殊的数据类型,不宜建立索引,比如文本字段(text)。
hash索引的底层数据结构是怎样的?


哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。
为何能够通过 key 快速取出 value呢? 原因在于 哈希算法(也叫散列算法)。通过哈希算法,我们可以快速找到 key 对应的 index,找到了 index 也就找到了对应的 value。
hash = hashfunc(key)
index = hash % array_size

但是!哈希算法有个 Hash 冲突 问题,也就是说多个不同的 key 最后得到的 index 相同。通常情况下,我们常用的解决办法是 链地址法。链地址法就是将哈希冲突数据存放在链表中。就比如 JDK1.8 之前 HashMap 就是通过链地址法来解决哈希冲突的。不过,JDK1.8 以后HashMap为了减少链表过长的时候搜索时间过长引入了红黑树。

为了减少 Hash 冲突的发生,一个好的哈希函数应该“均匀地”将数据分布在整个可能的哈希值集合中。

既然哈希表这么快,为什么MySQL 没有使用其作为索引的数据结构呢?
1.Hash 冲突问题 :我们上面也提到过Hash 冲突了,不过对于数据库来说这还不算最大的缺点。
2.Hash 索引不支持顺序和范围查询(Hash 索引不支持顺序和范围查询是它最大的缺点: 假如我们要对表中的数据进行排序或者进行范围查询,那 Hash 索引可就不行了。

B 树& B+树索引的底层数据结构是怎样的?

B 树也称 B-树,全称为 多路平衡查找树 ,B+ 树是 B 树的一种变体。B 树和 B+树中的 B 是 Balanced (平衡)的意思。
目前大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构。

B 树& B+树两者有何异同呢?
B 树的所有节点既存放键(key) 也存放 数据(data),而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。
B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。

在 MySQL 中,MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是,两者的实现方式不太一样。
1:MyISAM 引擎中,B+Tree 叶节点的 data 域存放的是数据记录的地址。在索引检索的时候,首先按照 B+Tree 搜索算法搜索索引,如果指定的 Key 存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。
2:InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶节点 data 域保存了完整的数据记录。这个索引的 key 是数据表的主键,因此 InnoDB 表数据文件本身就是主索引。这被称为“聚簇索引(或聚集索引)”,而其余的索引都作为辅助索引,辅助索引的 data 域存储相应记录主键的值而不是地址,这也是和 MyISAM 不同的地方。在根据主索引搜索时,直接找到 key 所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。
什么事MySQL的覆盖索引?

概念: 如果一个索引包含(或者说覆盖了)所有满足查询所需要的数据,那么就称这类索引为覆盖索引(Covering Index)。在 MySQL 中,可以通过使用 explain 命令输出的 Extra 列来判断是否使用了索引覆盖查询。若使用了索引覆盖查询,则 Extra 列包含“Using index””字符串。
大白话解释: select 的数据列只用从索引中就能够取得,不必从数据表中读取,换句话说查询列要被所使用的索引覆盖。
覆盖索引能有效地提高查询性能,因为覆盖索引只需要读取索引而不需要再回表读取数据。MySQL查询优化器在执行查询前会判断是否有一个索引能执行覆盖查询。
索引项通常比记录要小,所以MySQL会访问更少的数据。
不是所有类型的索引都可以成为覆盖索引。覆盖索引必须要存储索引的列,而哈希索引、空间索引和全文索引等都不存储索引列的值,所以MySQL只能使用B-Tree索引做覆盖索引。
联合索引的匹配规则有哪些?

全值匹配我最爱,最左前缀要遵守;
带头大哥不能死,中间兄弟不能断;
索引列上少计算,范围之后全失效;
like百分写最右,覆盖索引不写星;
不等空值还有or,索引失效要少用;
var引号不能丢,索引规则也不难;

mysql的索引下推是什么?

索引条件下推,也叫索引下推,英文全称Index Condition Pushdown,简称ICP。
在MySQL5.6之前,通过使用非主键索引进行查询的时候,存储引擎通过索引查询数据,然后将结果返回给MySQL server层,在server层判断是否符合条件。
在MySQL5.6及以上版本,可以使用索引下推的特性。当存在索引的列做为判断条件时,MySQL server将这一部分判断条件传递给存储引擎,然后存储引擎会筛选出符合MySQL server传递条件的索引项,即在存储引擎层根据索引条件过滤掉不符合条件的索引项,然后回表查询得到结果,将结果返回给MySQL server。
有了索引下推的优化,在满足一定的条件下,存储引擎层会在回表查询之前对数据进行过滤,可以减少存储引擎回表查询的次数。
其实就是顺序变了,索引下推之前是:查索引,回表,再筛选。
使用之后就是:查索引,筛选,再回表

什么是MySQL的回表?

回表查询:先到普通索引上定位主键值,再到聚集索引上定位行记录,它的性能较扫一遍索引树低(一般情况下)。

一般我们自己建的索引不管是单列索引还是联合索引,都称为普通索引,相对应的另外一种就是聚簇索引。 每个普通索引就对应着一颗独立的索引B+树,索引 B+ 树的节点仅仅包含了索引里的几个字段的值以及主键值。

根据索引树按照条件找到了需要的数据,仅仅是索引里的几个字段的值和主键值,如果用 select * 则还需要很多其他的字段,就得走一个回表操作,根据主键再到主键的聚簇索引里去找,聚簇索引的叶子节点是数据页,找到数据页里才能把一行数据的所有字段值提取出来。

假设 select * from table order by a,b,c 的语句,(table 有 abcdef 6 个字段),首先得从联合索引的索引树里按照顺序 a、b、c 取出来所有数据,接着对每一条数据都根据主键到聚簇索引的查找,其实性能不高。

有时候 MySQL 引擎会觉得用了既用了联合索引和聚簇索引来查找指定的字段,太慢了,那不不如直接全表扫描得了,只用聚集索引就行。

聚簇(聚集)索引补充:
只有一个聚集索引,聚簇索引的叶子节点存储行记录。根据聚簇索引的 key 查找是非常快的。

(1)如果表定义了主键,则主键就是聚集索引;

(2)如果表没有定义PK,则第一个 not NULL unique 列是聚集索引;

(3)否则,InnoDB 会创建一个隐藏的 row-id 作为聚集索引。

缓冲池

什么事MySQL缓冲池?

MySQL 先把磁盘里面的数据加载到 Buffer Pool 中,增删改都是基于 Buffer Pool 里面的内存数据进行操作的,内存的效率比 IO 高很多倍。改了内存数据后,再定期刷新到磁盘。

Buffer Pool 有三大双端链表:free、flush、lru 链表。

free 主要指向空闲缓存页。
flush 指向已修改的缓存页。
lru 指向被修改的缓存页,并根据最近最少使用的规则进行排序。
流程: 一边不停地加载数据到缓存页里去,一边不停地查询和修改缓存数据,然后free链表中的缓存页不停地减少↓,flush链表中的缓存页不停地增加↑,lru链表中的缓存页不停的在增加↑和移动←→。 另外一边,后台线程不停的把 lru 链表的冷数据区域的缓存页以及 flush 链表的缓存页,刷入磁盘中来清空缓存 页,然后flush链表和lru链表中的缓存页在减少↓,free链表中的缓存页在增加↑。

执行计划

讲一下执行计划explain?

type 字段 =>>
ALL - 最差,全表查
range - 索引范围查询;
ref - 二级索引等值查询;
const - 主键/唯一索引等值查询;
index_merge - 多个索引查询;
index - 覆盖索引且需要扫描全部的索引;
eq_ref - 被连接表主键/唯一引等值查询;
从好到坏:system > const > eq_ref > ref > range > index > all

possible_keys 字段 =>> 可能用到的索引(备胎);

key 字段 =>> 实际用到的索引(转正);

key_len 字段 =>> 实际使用索引的最大长度

ref 字段 =>> 等值查询有一个常数/列值
const - 索引等值查询;
[DB].[table].[column] - 被调用表索引列等值查询;


Extra字段 =>>
Impossible WHERE - where false;
Using index - 覆盖索引;
Using index condition - 索引下推;
Using where - 顺序扫描,where条件查询。

主从复制

主从复制的流程是什么?

master提交完事务后,写入binlog。
slave连接到master,获取binlog。
master创建dump线程,推送binglog到slave。
slave启动一个IO线程读取同步过来的master的binlog,记录到relay log中继日志中。
slave再开启一个sql线程读取relay log事件并在slave执行,完成同步。
slave记录自己的binglog。

spring

Spring常见面试题总结

谈谈你对Spring的理解?

Spring是一个轻量级的IoC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。常见的配置方式有三种:基于XML的配置、基于注解的配置、基于Java的配置。
Spring能帮我们做什么?

Spring能帮我们根据配置文件创建及组装对象之间的依赖关系。

Spring面向切面编程能帮助我们无耦合的实现日志记录,性能统计,安全控制。

Spring能非常简单的帮我们管理数据库事务。采用Spring,我们只需获取连接,执行SQL,其他事务相关的都交给Spring来管理了

Spring还能与第三方数据库访问框架(如Hibernate、JPA)无缝集成,而且自己也提供了一套JDBC访问模板,来方便数据库访问。

Spring还能与第三方Web(如Struts、JSF)框架无缝集成,而且自己也提供了一套SpringMVC框架,来方便web层搭建。

Spring能方便的与JavaEE(如JavaMail、任务调度)整合,与更多技术整合(比如缓存框架)。
BeanFactory和ApplicationContext有什么区别?

 BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。其中ApplicationContext是BeanFactory的子接口。
 
 (1)BeanFactory:是Spring里面最底层的接口,包含了各种Bean的定义,读取bean配置文档,管理bean的加载、实例化,控制bean的生命周期,维护bean之间的依赖关系。ApplicationContext接口作为BeanFactory的派生,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能:①继承MessageSource,因此支持国际化。②统一的资源文件访问方式。③提供在监听器中注册bean的事件。④同时加载多个配置文件。⑤载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。
 
 (2)①BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。②ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。③相对于基本的BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。
 
 (3)BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。
 
 (4)BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。

IOC与AOP

Spring容器的主要核心是什么?

控制反转(IOC),传统的java开发模式中,当需要一个对象时,我们会自己使用new或者getInstance等直接或者间接调用构造方法创建一个对象。而在spring开发模式中,spring容器使用了工厂模式为我们创建了所需要的对象,不需要我们自己创建了,直接调用spring提供的对象就可以了,这是控制反转的思想。

依赖注入(DI),spring使用JavaBean对象的set方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程,就是依赖注入的思想。

面向切面编程(AOP),在面向对象编程(oop)思想中,我们将事物纵向抽成一个个的对象。而在面向切面编程中,我们将一个个的对象某些类似的方面横向抽成一个切面,对这个切面进行一些如权限控制、事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想。
谈谈自己对于 Spring IoC 的理解?初始化过程是怎样的?

IoC(Inverse of Control:控制反转)是一种设计思想,就是 将原本在程序中手动创建对象的控制权,交由Spring框架来管理。
IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。
IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。

xml--(读取)-->Resource--(解析)-->BeanDefinition--(注册)-->BeanFactory

IOC源码分析

谈谈自己对于 AOP 的理解?

OOP面向对象,允许开发者定义纵向的关系,但并适用于定义横向的关系,导致了大量代码的重复,而不利于各个模块的重用。
AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证、日志、事务处理。
AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。
(1)AspectJ是静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,他会在编译阶段将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理:
①JDK动态代理只提供接口的代理,不支持类的代理。核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例,  生成目标类的代理对象。
 ②如果代理类没有实现 InvocationHandler 接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
(3)静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。
AOP 的核心概念有哪几个? 通知类型有哪些?

1:切面(aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象
2:横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点。
3:连接点(joinpoint):被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring 中连
接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。
4:切入点(pointcut):对连接点进行拦截的定义
5:通知(advice):所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异
常、最终、环绕通知五类。
6:目标对象:代理的目标对象
7:织入(weave):将切面应用到目标对象并导致代理对象创建的过程
8:引入(introduction):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段。

通知类型:
1:前置通知(@Before):logStart,在目标方法(div)运行之前运行
2:后置通知(@After):logEnd,在目标方法(div)运行之前运行
3:返回通知(@AfterReturning):logReturn,在目标方法(div)执行返回(无论是正常返回还是异常返回)之后运行
4:异常通知(@AfterThrowing):logException,在目标方法(div)出现异常之后运行
5:环绕通知(@Around):动态代理,手动推进目标方法运行(joinPoint.procced())

设计模式

Spring中的设计模式有哪些?

单例模式——spring中两种代理方式,若目标对象实现了若干接口,spring使用jdk的java.lang.reflect.Proxy类代理。若目标对象没有实现任何接口,spring使用CGLIB库生成目标类的子类。单例模式——在spring的配置文件中设置bean默认为单例模式。

模板方式模式——用来解决代码重复的问题。比如:RestTemplate、JmsTemplate、JpaTemplate

前端控制器模式——spring提供了前端控制器DispatherServlet来对请求进行分发。

试图帮助(viewhelper)——spring提供了一系列的JSP标签,高效宏来帮助将分散的代码整合在试图中。

依赖注入——贯穿于BeanFactory/ApplacationContext接口的核心理念。

工厂模式——在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用同一个接口来指向新创建的对象。Spring中使用BeanFactory 和 ApplicationContext 来生产 Bean 对象。

代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术;

注解

Spring的常用注解有哪些?

注解注入将会被容器在XML注入之前被处理,所以后者会覆盖掉前者对于同一个属性的处理结果。
注解装配在spring中默认是关闭的。所以需要在spring的核心配置文件中配置一下才能使用基于注解的装配模式。配置方式如下:<context:annotation-config/>
@Required:该注解应用于设值方法。
@Autowired:该注解应用于有值设值方法、非设值方法、构造方法和变量。
@Qualifier:该注解和@Autowired搭配使用,用于消除特定bean自动装配的歧义。
@RestController和@Controller的区别?

Controller 返回一个页面,单独使用 @Controller 不加 @ResponseBody的话一般使用在要返回一个视图的情况,这种情况属于比较传统的Spring MVC 的应用,对应于前后端不分离的情况。

@RestController返回JSON或XML形式数据,但@RestController只返回对象,对象数据直接以 JSON 或 XML 形式写入 HTTP 响应(Response)中,这种情况属于 RESTful Web服务,这也是目前日常开发所接触的最常用的情况(前后端分离)。

@Controller +@ResponseBody 返回JSON 或 XML 形式数据
@ResponseBody 注解的作用是将 Controller 的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到HTTP 响应(Response)对象的 body 中,通常用来返回 JSON 或者 XML 数据
@Component 和 @Bean 的区别是什么?

作用对象不同: @Component 注解作用于类,而@Bean注解作用于方法
@Component通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用 @ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。
@Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了Spring这是某个类的示例,当我需要用它的时候还给我。
@Bean 注解比 Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册bean。比如当我们引用第三方库中的类需要装配到 Spring容器时,则只能通过 @Bean来实现。
将一个类声明为Spring的 bean 的注解有哪些?

@Component :通用的注解,可标注任意类为 Spring 组件。如果一个Bean不知道属于哪个层,可以使用@Component 注解标注。
@Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
@Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。
@Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。
@PathVariable 和 @RequestParam 的区别是什么?

@PathVariable用于获取路径参数,@RequestParam用于获取查询参数。
例子
@GetMapping("/klasses/{klassId}/teachers")
public List<Teacher> getKlassRelatedTeachers(
         @PathVariable("klassId") Long klassId,
         @RequestParam(value = "type", required = false) String type ) {

}
如果我们请求的 url 是:/klasses/123456/teachers?type=web
那么我们服务获取到的数据就是:klassId=123456,type=web
读取配置文件的注解有哪些?

使用 @Value("${property}") 读取比较简单的配置信息

通过 @ConfigurationProperties(prefix = "library")读取配置信息并与 bean 绑定。

通过 @PropertySource("classpath:website.properties")读取指定 properties 文件
常用的参数校验注解有哪些?

所有的注解,推荐使用 JSR 注解,即javax.validation.constraints,而不是org.hibernate.validator.constraints

1:@NotEmpty 被注释的字符串的不能为 null 也不能为空
2:@NotBlank 被注释的字符串非 null,并且必须包含一个非空白字符
3:@Null 被注释的元素必须为 null
4:@NotNull 被注释的元素必须不为 null
5:@AssertTrue 被注释的元素必须为 true
6:@AssertFalse 被注释的元素必须为 false
7:@Pattern(regex=,flag=)被注释的元素必须符合指定的正则表达式
8:@Email 被注释的元素必须是 Email 格式。
9:@Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
10:@Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
11:@DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
12:@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
13:@Size(max=, min=)被注释的元素的大小必须在指定的范围内
14:@Digits(integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内
15:@Past被注释的元素必须是一个过去的日期
16:@Future 被注释的元素必须是一个将来的日期

我们在需要验证的参数上加上了@Valid注解,如果验证失败,它将抛出MethodArgumentNotValidException。
@RestController
@RequestMapping("/api")
public class PersonController {

    @PostMapping("/person")
    public ResponseEntity<Person> getPerson(@RequestBody @Valid Person person) {
        return ResponseEntity.ok().body(person);
    }
}

一定一定不要忘记在类上加上 @Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。
@RestController
@RequestMapping("/api")
@Validated
public class PersonController {

    @GetMapping("/person/{id}")
    public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) {
        return ResponseEntity.ok().body(id);
    }
}

生命周期

介绍一下spring bean的生命周期?

bean定义---bean初始化---bean调用---bean销毁

1:Bean 容器找到配置文件中 Spring Bean 的定义。
2:Bean 容器利用 Java Reflection API 创建一个Bean的实例。
3:如果涉及到一些属性值 利用 set()方法设置一些属性值。
4:如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入Bean的名字。
5:如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例。
6:如果Bean实现了 BeanFactoryAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoade r对象的实例。
7:如果实现了其他 *.Aware接口,就调用相应的方法。
8:如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessBeforeInitialization() 方法
9:如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。
10:如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
11:如果有和加载这个 Bean的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessAfterInitialization() 方法
12:当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。
13:当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。

总结:
实例化bean对象---设置对象属性---检查Aware相关接口并设置相关依赖---BeanPostProcessor前置处理---
检查是否是InitializingBean以决定是否调用afterPropertiesSet方法---检查是否配置有自定义的init-method
---BeanPostProcessor后置处理---注册必要的Destruction相关回调接口---使用中---是否实现DisposableBean接口---是否配置有自定义的destroy方法
分析一下Spring结构图?

Core模块:封装了框架依赖的最底层部分,包括资源访问、类型转换及一些常用工具类。

Beans模块:提供了框架的基础部分,包括反转控制和依赖注入。其中BeanFactory是容器核心,本质是“工厂设计模式”的实现,而且无需编程实现“单例设计模式”,单例完全由容器控制,而且提倡面向接口编程;所有应用程序对象及对象间关系由框架管理,从而真正把你从程序逻辑中把维护对象之间的依赖关系提取出来,这些依赖关系都由BeanFactory来维护。

Context模块:以Core和Beans为基础,集成Beans模块功能并添加资源绑定、数据验证、国际化、JavaEE支持、容器生命周期、事件传播等;核心接口是ApplicationContext。

EL模块:提供强大的表达式语言支持,支持访问和修改属性值,方法调用,支持从Spring容器获取Bean

AOP模块:SpringAOP模块提供了面向方面的编程实现,提供比如日志记录、权限控制、性能统计等通用功能和业务逻辑分离的技术,并且能动态的把这些功能添加到需要的代码中;这样各专其职,降低业务逻辑和通用功能的耦合。

Aspects模块:提供了对AspectJ的集成,AspectJ提供了比SpringASP更强大的功能。数据访问/集成模块:该模块包括了JDBC、ORM、OXM、JMS和事务管理。

事务模块:该模块用于Spring管理事务,只要是Spring管理对象都能得到Spring管理事务的好处,无需在代码中进行事务控制了,而且支持编程和声明性的事务管理。

JDBC模块:提供了一个JBDC的样例模板,使用这些模板能消除传统冗长的JDBC编码还有必须的事务控制,而且能享受到Spring管理事务的好处。

ORM模块:提供与流行的“对象-关系”映射框架的无缝集成,包括Hibernate、JPA、MyBatis等。而且可以使用Spring事务管理,无需额外控制事务。

OXM模块:提供了一个对Object/XML映射实现,将java对象映射成XML数据,或者将XML数据映射成java对象,Object/XML映射实现包括JAXB、Castor、XMLBeans和XStream。

JMS模块:用于JMS(JavaMessagingService),提供一套“消息生产者、消息消费者”模板用于更加简单的使用JMS,JMS用于用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。

Web模块:提供了基础的web功能。例如多文件上传、集成IoC容器、远程过程访问(RMI、Hessian、Burlap)以及WebService支持,并提供一个RestTemplate类来提供方便的Restfulservices访问。

Web-Servlet模块:提供了一个SpringMVCWeb框架实现。SpringMVC框架提供了基于注解的请求资源注入、更简单的数据绑定、数据验证等及一套非常易用的JSP标签,完全无缝与Spring其他技术协作。

Web-Struts模块:提供了与Struts无缝集成,Struts1.x和Struts2.x都支持

Test模块:Spring支持Junit和TestNG测试框架,而且还额外提供了一些基于Spring的测试功能,比如在测试Web框架时,模拟Http请求的功能。

bean

Spring 中的 bean 的作用域有哪些?

singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。
prototype : 每次请求都会创建一个新的 bean 实例。
request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效。
Spring 中的单例 bean 的线程安全问题了解吗?

单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。
常见的有两种解决办法:
1.在Bean对象中尽量避免定义可变的成员变量(不太现实)。
2.在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。

循环依赖

spring是怎样解决循环依赖的?

单例bean利用三级缓存解决循环依赖 A--->B--->A
1 调用doGetBean()方法,想要获取beanA,于是调用getSingleton()方法从缓存中查找beanA

2 在getSingleton()方法中,从一级缓存中查找,没有,返回null

3 doGetBean()方法中获取到的beanA为null,于是走对应的处理逻辑,调用getSingleton()的重载方法(参数为ObjectFactory的)

4 在getSingleton()方法中,先将beanA_name添加到一个集合中,用于标记该bean正在创建中。然后回调匿名内部类的creatBean方法

5 进入AbstractAutowireCapableBeanFactory#doCreateBean,先反射调用构造器创建出beanA的实例,然后判断。是否为单例、是否允许提前暴露引用(对于单例一般为true)、是否正在创建中〈即是否在第四步的集合中)。判断为true则将beanA添加到【三级缓存】中

6 对beanA进行属性填充,此时检测到beanA依赖于beanB,于是开始查找beanB

7 调用doGetBean()方法,和上面beanA的过程一样,到缓存中查找beanB,没有则创建,然后给beanB填充属性

8 此时beanB依赖于beanA,调用getsingleton()获取beanA,依次从一级、二级、三级缓存中找,此时从三级缓存中获取到beanA的创建工厂,通过创建工厂获取到singletonObject,此时这个singletonObject指向的就是上面在doCreateBean()方法中实例化的beanA

9 这样beanB就获取到了beanA的依赖,于是beanB顺利完成实例化,并将beanA从三级缓存移动到二级缓存中,同时删除三级缓存中的beanA,此时,beanB已经实例化并且初始化完成,把beanB放入一级缓存。

10 随后beanA继续他的属性填充工作,此时也获取到了beanB,beanA也随之完成了创建,回到getsingleton()方法中继续向下执行,将beanA从二级缓存移动到一级缓存中

事务

事务的特性是什么?

原子性(Atomicity): 一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。

一致性(Consistency): 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。

隔离性(Isolation): 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read uncommitted)、提交读(read committed)、可重复读(repeatable read)和串行化(Serializable)。

持久性(Durability): 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
Spring 管理事务的方式有几种?

编程式事务 : 在代码中硬编码(不推荐使用) : 通过 TransactionTemplate或者 TransactionManager 手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。

声明式事务 : 在 XML 配置文件中配置或者直接基于注解(推荐使用) : 实际是通过 AOP 实现(基于@Transactional 的全注解方式使用最多)
Spring 事务中哪几种事务传播行为?

事务传播行为是为了解决业务层方法之间互相调用的事务问题。
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。

正确的事务传播行为可能的值如下:
1.TransactionDefinition.PROPAGATION_REQUIRED
使用的最多的一个事务传播行为,我们平时经常使用的@Transactional注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
如果外部方法没有开启事务的话,Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
如果外部方法开启事务并且被Propagation.REQUIRED的话,所有Propagation.REQUIRED修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务均回滚。
@Service
Class A {
    @Autowired
    B b;
    @Transactional(propagation = Propagation.REQUIRED)
    public void aMethod {
        //do something
        b.bMethod();
    }
}
@Service
Class B {
    @Transactional(propagation = Propagation.REQUIRED)
    public void bMethod {
       //do something
    }
}
如果我们上面的aMethod()和bMethod()使用的都是PROPAGATION_REQUIRED传播行为的话,两者使用的就是同一个事务,只要其中一个方法回滚,整个事务均回滚。


2.TransactionDefinition.PROPAGATION_REQUIRES_NEW
创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
@Service
Class A {
    @Autowired
    B b;
    @Transactional(propagation = Propagation.REQUIRED)
    public void aMethod {
        //do something
        b.bMethod();
    }
}

@Service
Class B {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void bMethod {
       //do something
    }
}
如果我们上面的bMethod()使用PROPAGATION_REQUIRES_NEW事务传播行为修饰,aMethod还是用PROPAGATION_REQUIRED修饰的话。如果aMethod()发生异常回滚,bMethod()不会跟着回滚,因为 bMethod()开启了独立的事务。但是,如果 bMethod()抛出了未被捕获的异常并且这个异常满足事



3.TransactionDefinition.PROPAGATION_NESTED
如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
在外部方法开启事务的情况下,在内部开启一个新的事务,作为嵌套事务存在。
如果外部方法无事务,则单独开启一个事务,与 PROPAGATION_REQUIRED 类似。
@Service
Class A {
    @Autowired
    B b;
    @Transactional(propagation = Propagation.REQUIRED)
    public void aMethod {
        //do something
        b.bMethod();
    }
}

@Service
Class B {
    @Transactional(propagation = Propagation.NESTED)
    public void bMethod {
       //do something
    }
}
如果 bMethod() 回滚的话,aMethod()也会回滚。



4.TransactionDefinition.PROPAGATION_MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)这个使用的很少。

若是错误的配置以下 3 种事务传播行为,事务将不会发生回滚:
TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。
Spring 事务中的隔离级别有哪几种?
@Transactional(isolation = Isolation.DEFAULT)

TransactionDefinition.ISOLATION_DEFAULT :使用后端数据库默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ 隔离级别 Oracle 默认采用的 READ_COMMITTED 隔离级别

TransactionDefinition.ISOLATION_READ_UNCOMMITTED :最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读

TransactionDefinition.ISOLATION_READ_COMMITTED : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生

TransactionDefinition.ISOLATION_REPEATABLE_READ : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。

TransactionDefinition.ISOLATION_SERIALIZABLE : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
事务的超时属性是什么?
@Transactional(readOnly = false, timeout = -1)

所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为-1,这表示事务的超时时间取决于底层事务系统或者没有超时时间。
对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。
如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持 SQL 执行期间的读一致性;
如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持
事务的回滚规则是什么?

1:这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)时才会回滚,Error 也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚。
2:当 @Transactional 注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。
3:如果你想要回滚你定义的特定的异常类型的话,可以这样:
@Transactional(rollbackFor= MyException.class)
4:被 @Transactional 注解的方法所在的类必须被 Spring 管理,否则不生效;
5:底层使用的数据库必须支持事务机制,否则不生效;
@Transactional注解了解吗?

作用范围:
方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
类 :如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
接口 :不推荐在接口上使用。

常用配置参数:
propagation:事务的传播行为,默认值为 REQUIRED
isolation:事务的隔离级别,默认值采用 DEFAULT
timeout:事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。
readOnly:指定事务是否为只读事务,默认值为 false。
rollbackFor:用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。

事务注解的原理:
1:@Transactional 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。
2:createAopProxy() 方法 决定了是使用 JDK 还是 Cglib 来做动态代理
3:如果一个类或者一个类中的 public 方法上被标注@Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional 注解的 public 方法的时候,实际调用的是,TransactionInterceptor 类中的 invoke()方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。
4:若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有@Transactional 注解的方法的事务会失效。这是由于Spring AOP代理的原因造成的,因为只有当 @Transactional 注解的方法在类以外被调用的时候,Spring 事务管理才生效。

springmvc

什么是Spring MVC ?简单介绍下你对springMVC的理解?

Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。
SpringMVC的流程?

(1)用户发送请求至前端控制器DispatcherServlet;
(2) DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handle;
(3)处理器映射器根据请求url找到具体的处理器(controller),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet;
(4)DispatcherServlet 调用 HandlerAdapter处理器适配器;
(5)HandlerAdapter 经过适配调用 具体处理器(Handler,也叫后端控制器controller);
(6)Handler执行完成返回ModelAndView;
(7)HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet;
(8)DispatcherServlet将ModelAndView传给ViewResolver视图解析器进行解析;
(9)ViewResolver解析后返回具体View;
(10)DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)
(11)DispatcherServlet响应用户。
Springmvc的优点有哪些?

(1)可以支持各种视图技术,而不仅仅局限于JSP;
(2)与Spring框架集成(如IoC容器、AOP等);
(3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器(ViewResolver)。
(4) 支持各种请求资源的映射策略。
Spring MVC的主要组件?

(1)前端控制器 DispatcherServlet(不需要程序员开发):接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。
(2)处理器映射器HandlerMapping(不需要程序员开发):根据请求的URL来查找Handler
(3)处理器适配器HandlerAdapter:在编写Handler的时候要按照HandlerAdapter要求的规则去编写,这样适配器HandlerAdapter才可以正确的去执行Handler。
(4)处理器Handler(需要程序员开发)
(5)视图解析器 ViewResolver(不需要程序员开发):进行视图的解析,根据视图逻辑名解析成真正的视图(view)
(6)视图View(需要程序员开发jsp)
SpringMVC怎么样设定重定向和转发的?

(1)转发:在返回值前面加"forward:",譬如"forward:user.do?name=method4"
(2)重定向:在返回值前面加"redirect:",譬如"redirect:http://www.baidu.com"
SpringMvc怎么和AJAX相互调用的?

通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象
(1)加入Jackson.jar
(2)在配置文件中配置json的映射
(3)在接受Ajax方法里面可以直接返回Object,List等,但方法前面要加上@ResponseBody注解。
SpringMVC常用的注解有哪些?

@RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。
@RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
@ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。

如果在拦截请求中,我想拦截get方式提交的方法,怎么配置?

可以在@RequestMapping注解里面加上method=RequestMethod.GET。
怎样接收前台传入的参数?如果这些参数都是一个对象的呢?

直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。
直接在方法中声明这个对象,SpringMvc就自动会把属性赋值到这个对象里面。
SpringMvc中函数的返回值是什么?

返回值可以有很多类型,有String, ModelAndView。ModelAndView类把视图和数据都合并的一起的,但一般用String比较好。
怎么样把ModelMap里面的数据放入Session里面?

可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。

springBoot

启动流程

说一说springBoot的启动流程?

1:准备环境,根据不同的环境创建不同的Environment
2:准备、加载上下文,为不同的环境选择不同的Spring Context,然后加载资源,配置Bean
3:初始化,这个阶段刷新Spring Context,启动应用
4:最后结束流程

具体:
一、SpringBoot启动的时候,会构造一个SpringApplication的实例,构造SpringApplication的时候会进行初始化的工作,初始化的时候会做以下几件事:

1、把参数sources设置到SpringApplication属性中,这个sources可以是任何类型的参数.
2、判断是否是web程序,并设置到webEnvironment的boolean属性中.
3、创建并初始化ApplicationInitializer,设置到initializers属性中 。
4、创建并初始化ApplicationListener,设置到listeners属性中 。
5、初始化主类mainApplicatioClass。


二、SpringApplication构造完成之后调用run方法,启动SpringApplication,run方法执行的时候会做以下几件事:

1、构造一个StopWatch计时器,用来记录SpringBoot的启动时间 。
2、初始化监听器,获取SpringApplicationRunListeners并启动监听,用于监听run方法的执行。
3、创建并初始化ApplicationArguments,获取run方法传递的args参数。
4、创建并初始化ConfigurableEnvironment(环境配置)。封装main方法的参数,初始化参数,写入到 Environment中,发布 ApplicationEnvironmentPreparedEvent(环境事件),做一些绑定后返回Environment。
5、打印banner和版本。
6、构造Spring容器(ApplicationContext)上下文。先填充Environment环境和设置的参数,如果application有设置beanNameGenerator(bean)、resourceLoader(加载器)就将其注入到上下文中。调用初始化的切面,发布ApplicationContextInitializedEvent(上下文初始化)事件。
7、SpringApplicationRunListeners发布finish事件。
8、StopWatch计时器停止计时,日志打印总共启动的时间。
9、发布SpringBoot程序已启动事件(started())
10、调用ApplicationRunner和CommandLineRunner
11、最后发布就绪事件ApplicationReadyEvent,标志着SpringBoot可以处理就收的请求了(running())

springboot的优点有哪些?

开发基于 Spring 的应用程序很容易。
Spring Boot 项目所需的开发或工程时间明显减少,通常会提高整体生产力。
Spring Boot不需要编写大量样板代码、XML配置和注释。
Spring引导应用程序可以很容易地与Spring生态系统集成,如Spring JDBC、Spring ORM、Spring Data、Spring Security等。
Spring Boot遵循“固执己见的默认配置”,以减少开发工作(默认配置可以修改)。
Spring Boot 应用程序提供嵌入式HTTP服务器,如Tomcat和Jetty,可以轻松地开发和测试web应用程序。(这点很赞!普通运行Java程序的方式就能运行基于Spring Boot web 项目,省事很多)
Spring Boot提供命令行接口(CLI)工具,用于开发和测试Spring Boot应用程序,如Java或Groovy。
Spring Boot提供了多种插件,可以使用内置工具(如Maven和Gradle)开发和测试Spring Boot应用程序。
什么是 Spring Boot Starters?

Spring Boot Starters 是一系列依赖关系的集合,因为它的存在,项目的依赖之间的关系对我们来说变的更加简单了。举个例子:在没有Spring Boot Starters之前,我们开发REST服务或Web应用程序时; 我们需要使用像Spring MVC,Tomcat和Jackson这样的库,这些依赖我们需要手动一个一个添加。但是,有了 Spring Boot Starters 我们只需要一个只需添加一个spring-boot-starter-web一个依赖就可以了,这个依赖包含的字依赖中包含了我们开发REST 服务需要的所有依赖。
如何在Spring Boot应用程序中使用Jetty而不是Tomcat?

Spring Boot Web starter使用Tomcat作为默认的嵌入式servlet容器, 如果你想使用 Jetty 的话只需要修改pom.xml(Maven)或者build.gradle(Gradle)就可以了。

<!--从Web启动器依赖中排除Tomcat-->
<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>
<!--添加Jetty依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
介绍一下@SpringBootApplication注解?

package org.springframework.boot.autoconfigure;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
   ......
}

package org.springframework.boot;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {

}

可以看出大概可以把 @SpringBootApplication看作是 @Configuration(允许在上下文中注册额外的bean或导入其他配置类)、@EnableAutoConfiguration(启用 SpringBoot 的自动配置机制)、@ComponentScan(扫描被@Component (@Service,@Controller)注解的bean,注解默认会扫描该类所在的包下所有的类。) 注解的集合。

Spring Boot 的自动配置是如何实现的?

@EnableAutoConfiguration是启动自动配置的关键。注解通过Spring 提供的 @Import 注解导入了AutoConfigurationImportSelector类(@Import 注解可以导入配置类或者Bean到当前类中)。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}

AutoConfigurationImportSelector类中getCandidateConfigurations方法会将所有自动配置类的信息以 List 的形式返回。这些配置信息会被 Spring 容器作 bean 来管理。
@Conditional 注解。@ConditionalOnClass(指定的类必须存在于类路径下),@ConditionalOnBean(容器中是否有指定的Bean)等等都是对@Conditional注解的扩展。

mybatis

什么是mybatis?

Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直接编写原生态sql,可以严格控制sql执行性能,灵活度高。
MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。
为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里?

Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。
而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。
mybatis的优点有哪些?缺点有哪些?

基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。
与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
能够与Spring很好的集成;
提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。

SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
#{}和${}的区别是什么?

#{}是预编译处理,${}是字符串替换。
Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
Mybatis在处理${}时,就是把${}替换成变量的值。
使用#{}可以有效的防止SQL注入,提高系统安全性。
当实体类中的属性名和表中的字段名不一样 ,怎么办 ?

第1种:通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
第2种:通过 <resultMap>来映射字段名和实体类属性名的一一对应的关系。
模糊查询like语句该怎么写?

第1种:在Java代码中添加sql通配符。推荐使用,不会引起sql注入
string wildcardname = “%smi%”;
list<name> names = mapper.selectlike(wildcardname);

第2种:在sql语句中拼接通配符,会引起sql注入
select * from foo where bar like "%"#{value}"%"
通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?

Dao接口即Mapper接口。接口的全限名,就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。
Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement。在Mybatis中,每一个 <select>、<insert>、<update>、<delete>标签,都会被解析为一个MapperStatement对象。
Mapper接口里的方法,是不能重载的,因为是使用 全限名+方法名 的保存和寻找策略。
Mapper 接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy,代理对象会拦截接口方法,转而执行MapperStatement所代表的sql,然后将sql执行结果返回。
什么是MyBatis的接口绑定?有哪些实现方式?

接口绑定,就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。
接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加上 @Select、@Update等注解,里面包含Sql语句来绑定;
另外一种就是通过xml里面写SQL来绑定, 在这种情况下,要指定xml映射文件里面的namespace必须为接口的全路径名。
当Sql语句比较简单时候,用注解绑定, 当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多。
使用MyBatis的mapper接口调用时有哪些要求?

Mapper接口方法名和mapper.xml中定义的每个sql的id相同;
Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同;
Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同;
Mapper.xml文件中的namespace即是mapper接口的类路径。
Mapper编写有哪几种方式?

第一种:接口实现类继承SqlSessionDaoSupport:使用此种方法需要编写mapper接口,mapper接口实现类、mapper.xml文件。
在sqlMapConfig.xml中配置mapper.xml的位置
<mappers>
    <mapper resource="mapper.xml文件的地址" />
    <mapper resource="mapper.xml文件的地址" />
</mappers>
定义mapper接口,实现类集成SqlSessionDaoSupport,mapper方法中可以this.getSqlSession()进行数据增删改查。
spring 配置
<bean id=" " class="mapper接口的实现">
    <property name="sqlSessionFactory" ref="sqlSessionFactory"></property>
</bean>

第二种:使用 org.mybatis.spring.mapper.MapperFactoryBean
在sqlMapConfig.xml中配置mapper.xml的位置,如果mapper.xml和mappre接口的名称相同且在同一个目录,这里可以不用配置
<mappers>
    <mapper resource="mapper.xml文件的地址" />
    <mapper resource="mapper.xml文件的地址" />
</mappers>
定义mapper接口,mapper.xml中的namespace为mapper接口的地址,mapper接口中的方法名和mapper.xml中的定义的statement的id保持一致
Spring中定义
<bean id="" class="org.mybatis.spring.mapper.MapperFactoryBean">
    <property name="mapperInterface" value="mapper接口地址" />
    <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

第三种:使用mapper扫描器:最常使用
mapper.xml中的namespace为mapper接口的地址;mapper接口中的方法名和mapper.xml中的定义的statement的id保持一致;如果将mapper.xml和mapper接口的名称保持一致则不用在sqlMapConfig.xml中进行配置。
定义mapper接口:注意mapper.xml的文件名和mapper的接口名称保持一致,且放在同一个目录
spring中定义:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="mapper接口包地址"></property>
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>
Mybatis是如何进行分页的?分页插件的原理是什么?

Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
简述Mybatis的插件运行原理,以及如何编写一个插件?

Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。
编写插件:实现Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。
Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?

第一种是使用 <resultMap>标签,逐一定义数据库列名和对象属性名之间的映射关系。
第二种是使用sql列的别名功能,将列的别名书写为对象属性名。
有了列名与属性名的映射关系后,Mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。
//如何执行批量插入?

/*
<insert id=”insertname”>
	insert into names (name) values (#{value})
</insert>
*/
list < string> names = new arraylist();

names.add(fred);
names.add(barney);
names.add(betty);
names.add(wilma);
// 注意这里 executortype.batch
sqlsession sqlsession = sqlsessionfactory.opensession(executortype.batch);
try
 {
    namemapper mapper = sqlsession.getmapper(namemapper.class);    
    for (string name: names) {
            mapper.insertname(name);
        }
    sqlsession.commit();
} 
catch(Exception e) {
    e.printStackTrace();
    sqlSession.rollback();  
	throw e;
}
finally{
    sqlsession.close();
}
如何获取自动生成的(主)键值?

如果采用自增长策略,自动生成的键值在 insert 方法执行完后可以被设置到传入的参数对象中。
<insert id=”insertname” usegeneratedkeys=”true” keyproperty=”id”>
     insert into names (name) values (#{name})
</insert>

Name name = new Name();
name.setName = "admin";
int rows = mapper.insertName(name);
//插入成功后主键会赋值给name对象的id属性
System.out.println("generator id=:"+name.getId());
在mapper中如何传递多个参数?

第一种:DAO层的函数:
对应的xml,#{0}代表接收的是dao层中的第一个参数,#{1}代表dao层中第二参数,更多参数一致往后加即可。
第二种:DAO层的函数的参数使用 @param 注解进行数据绑定
第三种:多个参数封装成map,在xml中通过#{key}就能拿到对应的值了
Mybatis动态sql有什么用?执行原理?有哪些动态sql?

Mybatis动态sql可以在Xml映射文件内,以标签的形式编写动态sql,执行原理是根据表达式的值 完成逻辑判断并动态拼接sql的功能。
Mybatis提供了9种动态sql标签:trim|where|set|foreach|if|choose|when|otherwise|bind。
Xml映射文件中,除了常见的select|insert|updae|delete标签之外,还有哪些标签?

<resultMap>、<parameterMap>、<sql>、<include>、<selectKey>,加上动态sql的9个标签,其中 <sql>为sql片段标签,通过 <include>标签引入sql片段, <selectKey>为不支持自增的主键生成策略标签。
Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复?

不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;
原因就是namespace+id是作为Map <String,MapperStatement>的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。
<-- 一对一、一对多的关联查询 ?-->
<mapper namespace="com.lcb.mapping.userMapper">
	<!--association 一对一关联查询 -->
	<select id="getClass" parameterType="int" resultMap="ClassesResultMap">
        select * from class c,teacher t where c.teacher_id=t.t_id and c.c_id=#{id}		     </select>
	<resultMap type="com.lcb.user.Classes" id="ClassesResultMap">
		<!-- 实体类的字段名和数据表的字段名映射 -->
		<id property="id" column="c_id" />
		<result property="name" column="c_name" />
		<association property="teacher" javaType="com.lcb.user.Teacher">
			<id property="id" column="t_id" />
			<result property="name" column="t_name" />
		</association>
	</resultMap>
	<!--collection 一对多关联查询 -->
	<select id="getClass2" parameterType="int" resultMap="ClassesResultMap2">
        select * from class c,teacher t,student s where c.teacher_id=t.t_id and 		  c.c_id=s.class_id and c.c_id=#{id}
    </select>
	<resultMap type="com.lcb.user.Classes" id="ClassesResultMap2">
		<id property="id" column="c_id" />
		<result property="name" column="c_name" />
		<association property="teacher" javaType="com.lcb.user.Teacher">
			<id property="id" column="t_id" />
			<result property="name" column="t_name" />
		</association>
		<collection property="student" ofType="com.lcb.user.Student">
			<id property="id" column="s_id" />
			<result property="name" column="s_name" />
		</collection>
	</resultMap>
</mapper>
MyBatis实现一对一有几种方式?具体怎么操作的?

有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成;
嵌套查询是先查一个表,根据这个表里面的结果的 外键id,去再另外一个表里面查询数据,也是通过association配置,但另外一个表的查询通过select属性配置。
MyBatis实现一对多有几种方式,怎么操作的?

有联合查询和嵌套查询。联合查询是几个表联合查询,只查询一次,通过在resultMap里面的collection节点配置一对多的类就可以完成;
嵌套查询是先查一个表,根据这个表里面的 结果的外键id,去再另外一个表里面查询数据,也是通过配置collection,但另外一个表的查询通过select节点配置。
Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?

Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。
在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。

它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
Mybatis的一级、二级缓存?

一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。

二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache,redis。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置 <cache/> ;

对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。
Mybatis 映射文件中,如果A 标签通过include 引用了B 标签的内容,请问,B 标签能否定义在A 标签的后面,还是说必须定义在A 标签的前面?

虽然Mybatis 解析Xml 映射文件是按照顺序解析的,但是,被引用的B 标签依然可以定义在任何地方,Mybatis 都可以正确识别。原理是,Mybatis 解析A 标签,发现A 标签引用了B 标签,但是B 标签尚未解析到,尚不存在,此时,Mybatis 会将A 标签标记为未解析状态,然后继续解析余下的标签,包含B 标签,待所有标签解析完毕,Mybatis 会重新解析那些被标记为未解析的标签,此时再解析A 标签时,B 标签已经存在,A 标签也就可以正常解析完成了。
简述Mybatis 的Xml 映射文件和Mybatis 内部数据结构之间的映射关系?

Mybatis 将所有Xml 配置信息都封装到All-In-One 重量级对象Configuration 内部。在Xml 映射文件中,<parameterMap>标签会被解析为ParameterMap 对象,其每个子元素会被解析为ParameterMapping 对象。<resultMap>标签会被解析为ResultMap 对象,其每个子元素会被解析为ResultMapping 对象。每一个<select>、<insert>、<update>、<delete>标签均会被解析为MappedStatement 对象,标签内的sql 会被解析为BoundSql 对象。

redis

redis是单线程还是多线程?

Redis 不同版本之间采用的线程模型是不一样的,在 Redis4.0 版本之前使用的是单线程模型,在 4.0 版本之后增加了多线程的支持。
在 4.0 之前虽然我们说 Redis 是单线程,也只是说它的网络 I/O 线程以及 Set 和 Get 操作是由一个线程完成的。但是 Redis 的持久化、集群同步还是使用其他线程来完成。
4.0 之后添加了多线程的支持,主要是体现在大数据的异步删除功能上,例如 unlink key、flushdb async、flushall async 等


============================================
为什么 Redis 在 4.0 之前会选择使用单线程?而且使用单线程还那么快?

选择单线程个人觉得主要是使用简单,不存在锁竞争,可以在无锁的情况下完成所有操作,不存在死锁和线程切换带来的性能和时间上的开销,但同时单线程也不能完全发挥出多核 CPU 的性能。
至于为什么单线程那么快我觉得主要有以下几个原因:
Redis 的大部分操作都在内存中完成,内存中的执行效率本身就很快,并且采用了高效的数据结构,比如哈希表和跳表。
使用单线程避免了多线程的竞争,省去了多线程切换带来的时间和性能开销,并且不会出现死锁。
采用 I/O 多路复用机制处理大量客户端的 Socket 请求,因为这是基于非阻塞的 I/O 模型,这就让 Redis 可以高效地进行网络通信,I/O 的读写流程也不再阻塞。

=============================================
那 Redis 是如何实现数据不丢失的呢?
Redis 数据是存储在内存中的,为了保证 Redis 数据不丢失,那就要把数据从内存存储到磁盘上,以便在服务器重启后还能够从磁盘中恢复原有数据,这就是 Redis 的数据持久化。Redis 数据持久化有三种方式。
1)AOF 日志(Append Only File,文件追加方式):记录所有的操作命令,并以文本的形式追加到文件中。
2)RDB 快照(Redis DataBase):将某一个时刻的内存数据,以二进制的方式写入磁盘。
3)混合持久化方式:Redis 4.0 新增了混合持久化的方式,集成了 RDB 和 AOF 的优点。

==============================================
分别说说 AOF 和 RDB 的实现原理

AOF 采用的是写后日志的方式,Redis 先执行命令把数据写入内存,然后再记录日志到文件中。AOF 日志记录的是操作命令,不是实际的数据,如果采用 AOF 方法做故障恢复时需要将全量日志都执行一遍。
RDB 采用的是内存快照的方式,它记录的是某一时刻的数据,而不是操作,所以采用 RDB 方法做故障恢复时只需要直接把 RDB 文件读入内存即可,实现快速恢复。

==============================================
我们平时用的 MySQL 采用的是 “写前日志”,那 Redis 为什么要先执行命令,再把数据写入日志呢?

这个主要是由于 Redis 在写入日志之前,不对命令进行语法检查,所以只记录执行成功的命令,避免出现记录错误命令的情况,而且在命令执行后再写日志不会阻塞当前的写操作。

==============================================
后写日志又有什么风险呢?

后写日志主要有两个风险可能会发生:
数据可能会丢失:如果 Redis 刚执行完命令,此时发生故障宕机,会导致这条命令存在丢失的风险。
可能阻塞其他操作:AOF 日志其实也是在主线程中执行,所以当 Redis 把日志文件写入磁盘的时候,还是会阻塞后续的操作无法执行。

==============================================
RDB 做快照时会阻塞线程吗?

Redis 提供了两个命令来生成 RDB 快照文件,分别是 save 和 bgsave。save 命令在主线程中执行,会导致阻塞。而 bgsave 命令则会创建一个子进程,用于写入 RDB 文件的操作,避免了对主线程的阻塞,这也是 Redis RDB 的默认配置。

==============================================
RDB 做快照的时候数据能修改吗?

save 是同步的会阻塞客户端命令,bgsave 的时候是可以修改的。

==============================================
Redis 是怎么解决在 bgsave 做快照的时候允许数据修改呢?

这里主要是利用 bgsave 的子线程实现的,具体操作如下:
如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响;
如果主线程执行写操作,则被修改的数据会复制一份副本,然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据。
Redis 对 RDB 的执行频率非常重要,因为这会影响快照数据的完整性以及 Redis 的稳定性,所以在 Redis 4.0 后,增加了 AOF 和 RDB 混合的数据持久化机制: 把数据以 RDB 的方式写入文件,再将后续的操作命令以 AOF 的格式存入文件,既保证了 Redis 重启速度,又降低数据丢失风险。


==============================================
Redis 如何实现高可用?

Redis 实现高可用主要有三种方式:主从复制、哨兵模式,以及 Redis 集群。
主从复制:将从前的一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,这个跟 MySQL 主从复制的原理一样。
哨兵模式:使用 Redis 主从服务的时候,会有一个问题,就是当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复,为了解决这个问题,Redis 增加了哨兵模式(因为哨兵模式做到了可以监控主从服务器,并且提供自动容灾恢复的功能)。
集群模式:Redis Cluster 是一种分布式去中心化的运行模式,是在 Redis 3.0 版本中推出的 Redis 集群方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。


==============================================
使用哨兵模式在数据上有副本数据做保证,在可用性上又有哨兵监控,一旦 master 宕机会选举 salve 节点为 master 节点,这种已经满足了我们的生产环境需要,那为什么还需要使用集群模式呢?

哨兵模式归根节点还是主从模式,在主从模式下我们可以通过增加 salve 节点来扩展读并发能力,但是没办法扩展写能力和存储能力,存储能力只能是 master 节点能够承载的上限。所以为了扩展写能力和存储能力,我们就需要引入集群模式。

==============================================
集群中那么多 Master 节点,Redis Cluster 在存储的时候如何确定选择哪个节点呢?

Redis Cluster 采用的是类一致性哈希算法实现节点选择的
Redis Cluster 将自己分成了 16384 个 Slot(槽位),哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中,具体执行过程分为两大步。
1)根据键值对的 key,按照 CRC16 算法计算一个 16 bit 的值。
2)再用 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。
每个 Redis 节点负责处理一部分槽位
假如你有三个 master 节点 ABC,每个节点负责的槽位如下:
A节点槽位:0-5000
B节点槽位:5001-10000
C节点槽位:10001-16383
redis有哪些优势?

redis的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业务场景。除此之外,redis 支持事务 、持久化、LUA 脚本、LRU驱动事件、多种集群方案。
为什么要使用redis缓存?

主要从“高性能”和“高并发”这两点来看待这个问题。
高性能:假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s 。所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
高并发:直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
为什么要用 redis 而不用 map/guava 做缓存?

缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached 服务的高可用,整个程序架构上较为复杂。
redis 和 memcached 的区别?

redis 支持更丰富的数据类型(支持更复杂的应用场景):Redis 不仅仅支持简单的 k/v 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。memcache 支持简单的数据类型,String。

Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memecache 把数据全部存在内存之中。

集群模式:memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的.

Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。
redis 常见数据结构以及使用场景分析?

String
常用命令: set,get,decr,incr,mget 等。
String 数据结构是简单的 key-value 类型,value 其实不仅可以是 String,也可以是数字。 常规 key-value 缓存应用; 常规计数:微博数,粉丝数等。

Hash
常用命令: hget,hset,hgetall 等。
hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。

List
常用命令: lpush,rpush,lpop,rpop,lrange 等
list 就是链表,Redis list 的应用场景非常多,也是 Redis 最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用 Redis 的 list 结构来实现。Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。

Set
常用命令: sadd,spop,smembers,sunion 等
Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。可以基于 set 轻易实现交集、并集、差集的操作。比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。

Sorted Set
常用命令: zadd,zrange,zrem,zcard 等
和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列。在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。
redis怎样设置过期时间?

我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间。

如果假设你设置了一批 key 只能存活 1 个小时,那么接下来 1 小时后,redis 是怎么对这批 key 进行删除的?

定期删除+惰性删除。
定期删除:redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔 100ms 就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!
惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被 redis 给删除掉。这就是所谓的惰性删除,也是够懒的哈!
redis 内存淘汰机制(MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?)

redis 提供 8 种数据淘汰策略:
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

使用策略规则:

(1)如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用 allkeys-lru
(2)如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random
redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以进行恢复)?

很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
Redis 不同于 Memcached 的很重一点就是,Redis 支持持久化,而且支持两种不同的持久化操作。Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file,AOF)。
什么是快照持久化?

Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。
快照持久化是 Redis 默认采用的持久化方式,在 redis.conf 配置文件中默认有此下配置:
save 900 1     #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 300 10    #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
什么是追加文件持久化?

与快照持久化相比,AOF 持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:
appendonly yes
开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。
在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec  #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no        #让操作系统决定何时进行同步

为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
redis事务机制是怎样的?

Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。在 Redis 中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务也具有持久性(Durability)。

redis 同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。
1.若在事务队列中存在命令性错误(类似于java编译性错误),则执行EXEC命令时,所有命令都不会执行
2.若在事务队列中存在语法性错误(类似于java的1/0的运行时异常),则执行EXEC命令时,其他正确命令会被执行,错误命令抛出异常。

第一步,客户端要使用一个命令显式地表示一个事务的开启。在 Redis 中,这个命令就是 MULTI。

第二步,客户端把事务中本身要执行的具体操作(例如增删改数据)发送给服务器端。这些操作就是 Redis 本身提供的数据读写命令,例如 GET、SET 等。不过,这些命令虽然被客户端发送到了服务器端,但 Redis 实例只是把这些命令暂存到一个命令队列中,并不会立即执行。

第三步,客户端向服务器端发送提交事务的命令,让数据库实际执行第二步中发送的具体操作。Redis 提供的 EXEC 命令就是执行事务提交的。当服务器端收到 EXEC 命令后,才会实际执行命令队列中的所有命令。

什么是缓存雪崩?怎样处理?

简介:缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 崩掉
事后:利用 redis 持久化机制保存的数据尽快恢复缓存

image.png

什么是缓存穿透?怎么解决?

缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。
一般 MySQL 默认的最大连接数在 150 左右,这个可以通过 show variables like '%max_connections%';命令来查看。一般 3000 个并发请求就能打死大部分数据库了。

最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。
1)缓存无效 key : 如果缓存和数据库都查不到某个 key 的数据就写一个到 redis 中去并设置过期时间,如果黑客恶意攻击,每次构建不同的请求 key,会导致 redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
一般情况下我们是这样设计 key 的: 表名:列名:主键名:主键值。
2)布隆过滤器:通过它我们可以非常方便地判断一个给定数据是否存在与海量数据中。具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。

image.png

如何解决 Redis 的并发竞争 Key 问题?

所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)
基于 zookeeper 临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在 zookeeper 上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。

在实践中,当然是从以可靠性为主。所以首推 Zookeeper。
如何保证缓存与数据库双写时的数据一致性?

一般情况下我们都是这样使用缓存的:先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。这种方式很明显会存在缓存和数据库的数据不一致的情况。
一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况
串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
一个字符串类型的值能存储最大容量是多少?

512M
Redis 常见性能问题和解决方案:

(1)Master 最好不要写内存快照,如果 Master 写内存快照,save 命令调度 rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务

(2)如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一

(3)为了主从复制的速度和连接的稳定性,Master 和 Slave 最好在同一个局域网

(4)尽量避免在压力很大的主库上增加从

(5)主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1<- Slave2 <- Slave3…这样的结构方便解决单点故障问题,实现 Slave 对 Master的替换。如果 Master 挂了,可以立刻启用 Slave1 做 Master,其他不变。

redis 过期键的删除策略?

(1)定时删除:在设置键的过期时间的同时,创建一个定时器 timer). 让定时器在键的过期时间来临时,立即执行对键的删除操作。

(2)惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。

(3)定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。

Redis 的同步机制了解么?

答:Redis 可以使用主从同步,从从同步。第一次同步时,主节点做一次 bgsave,并同时将后续修改操作记录到内存 buffer,待完成后将 rdb 文件全量同步到复制节点,复制节点接受完成后将 rdb 镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。
Pipeline 有什么好处,为什么要用 pipeline?

答:可以将多次 IO 往返的时间缩减为一次,前提是 pipeline 执行的指令之间没有因果相关性。使用 redis-benchmark 进行压测的时候可以发现影响 redis 的 QPS峰值的一个重要因素是 pipeline 批次指令的数目。
是否使用过 Redis 集群,集群的原理是什么?

(1)Redis Sentinal 着眼于高可用,在 master 宕机时会自动将 slave 提升为master,继续提供服务。
(2)Redis Cluster 着眼于扩展性,在单个 redis 内存不足时,使用 Cluster 进行分片存储。
Redis 集群方案什么情况下会导致整个集群不可用?

答:有 A,B,C 三个节点的集群,在没有复制模型的情况下,如果节点 B 失败了,那么整个集群就会以为缺少 5501-11000 这个范围的槽而不可用。
Jedis 与Redisson 对比有什么优缺点?

Jedis 是Redis 的Java 实现的客户端,其API 提供了比较全面的Redis 命令的支持;Redisson 实现了分布式和可扩展的Java 数据结构,和Jedis 相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis 特性。Redisson 的宗旨是促进使用者对Redis 的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
说说Redis 哈希槽的概念?

Redis 集群没有使用一致性hash,而是引入了哈希槽的概念,Redis 集群有16384 个哈希槽,每个key 通过CRC16 校验后对16384 取模来决定放置哪个槽,集群的每个节点负责一部分hash 槽。
Redis 如何做内存优化?

尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web 系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面
Redis 回收进程如何工作的?使用的是什么算法?

LRU 算法
一个客户端运行了新的命令,添加了新的数据。
Redi 检查内存使用情况,如果大于maxmemory 的限制, 则根据设定好的策略进行回收。
一个新的命令被执行,等等。
所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。
如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。
为什么要做Redis 分区?有哪些Redis分区实现方案?Redis 分区有什么缺点?

分区可以让Redis 管理更大的内存,Redis 将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使Redis 的计算能力通过简单地增加计算机得到成倍提升,Redis 的网络带宽也会随着计算机和网卡的增加而成倍增长。

客户端分区就是在客户端就已经决定数据会被存储到哪个Redis 节点或者从哪个Redis 节点读取。大多数客户端已经实现了客户端分区。
代理分区意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis 实例,然后根据Redis 的响应结果返回给客户端。Redis 和memcached 的一种代理实现就是Twemproxy
查询路由(Query routing) 的意思是客户端随机地请求任意一个Redis 实例,然后由Redis将请求转发给正确的Redis 节点。Redis Cluster 实现了一种混合形式的查询路由,但并不是直接将请求从一个Redis 节点转发到另一个Redis 节点,而是在客户端的帮助下直接redirected 到正确的Redis 节点。

涉及多个key 的操作通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存储到不同的Redis 实例(实际上这种情况也有办法,但是不能直接使用交集指令)。
同时操作多个key,则不能使用Redis 事务
分区使用的粒度是key,不能使用一个非常长的排序key 存储一个数据集
当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的Redis 实例和主机同时收集RDB / AOF 文件。
分区时动态扩容或缩容可能非常复杂。Redis 集群在运行时增加或者删除Redis 节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。
Redis 持久化数据和缓存怎么做扩容?

如果Redis 被当做缓存使用,使用一致性哈希实现动态扩容缩容。
如果Redis 被当做一个持久化存储使用,必须使用固定的keys-to-nodes 映射关系,节点的数量一旦确定不能变化。否则的话(即Redis 节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis 集群可以做到这样。
Redis 常见性能问题和解决方案?

Master 最好不要做任何持久化工作,如RDB 内存快照和AOF 日志文件
如果数据比较重要,某个Slave 开启AOF 备份数据,策略设置为每秒同步一次
为了主从复制的速度和连接的稳定性,Master 和Slave 最好在同一个局域网内
尽量避免在压力很大的主库上增加从库
主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2<- Slave3...
Redis单点吞吐量是多少?什么是QPS?什么是TPS?

单点TPS达到8万/秒,QPS达到10万/秒,补充下TPS和QPS的概念
QPS: 应用系统每秒钟最大能接受的用户访问量:每秒钟处理完请求的次数,注意这里是处理完,具体是指发出请求到服务器处理完成功返回结果。可以理解在server中有个counter,每处理一个请求加1,1秒后counter=QPS。
TPS: 每秒钟最大能处理的请求数。每秒钟处理完的事务次数,一个应用系统1s能完成多少事务处理,一个事务在分布式处理中,可能会对应多个请求,对于衡量单个接口服务的处理能力,用QPS比较合理。
怎样用Redis实现分布式锁?怎样用zookeeper实现分布式锁?怎样用数据库实现分布式锁?

分布式的CAP理论告诉我们任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。一般情况下,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证最终一致性,只要这个最终时间是在用户可以接受的范围内即可。在很多时候,为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。

一个可靠的、高可用的分布式锁需要满足以下几点
互斥性:任意时刻只能有一个客户端拥有锁,不能被多个客户端获取
安全性:锁只能被持有该锁的客户端删除,不能被其它客户端删除
死锁避免:获取锁的客户端因为某些原因而宕机,而未能释放锁,其它客户端也就无法获取该锁,需要有机制来避免该类问题的发生
高可用:当部分节点宕机,客户端仍能获取锁或者释放锁

基于数据库实现的乐观锁
乐观锁的通常是基于数据版本号来实现的。比如,有个商品表t_goods,有一个字段left_count用来记录商品的库存个数。在并发的情况下,为了保证不出现超卖现象,即left_count不为负数。乐观锁的实现方式为给商品表增加一个版本号字段version,默认为0,每修改一次数据,将版本号加1。
无版本号并发超卖示例:
线程1查询,当前left_count为1,则有记录
select * from t_goods where id = 10001 and left_count > 0
线程2查询,当前left_count为1,也有记录
select * from t_goods  where id = 10001 and left_count > 0
线程1下单成功库存减一,修改left_count为0,
update t_goods set left_count = left_count - 1 where id = 10001
线程2下单成功库存减一,修改left_count为-1,产生脏数据
update t_goods set left_count = left_count - 1 where id = 10001
有版本号的乐观锁示例:
线程1查询,当前left_count为1,则有记录,当前版本号为999
select left_count, version from t_goods where id = 10001 and left_count > 0;
线程2查询,当前left_count为1,也有记录,当前版本号为999
select left_count, version from t_goods where id = 10001 and left_count > 0;
线程1,更新完成后当前的version为1000,update状态为1,更新成功
update t_goods set version = 1000, left_count = left_count-1 where id = 10001 and version = 999;
线程2,更新由于当前的version为1000,udpate状态为0,更新失败,再针对相关业务做异常处理
update t_goods set version = 1000, left_count = left_count-1 where id = 10001 and version = 999;
可以发现,这种和CAS的乐观锁机制是类似的,所不同的是CAS的硬件来保证原子性,而这里是通过数据库来保证单条SQL语句的原子性。顺带一提CAS的ABA问题一般也是通过版本号来解决。

基于数据库实现的排他锁
基于数据库的排他锁需要通过数据库的唯一性约束UNIQUE KEY来保证数据的唯一性,从而为锁的独占性提供基础。
CREATE TABLE `distribute_lock` (
   `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
   `unique_mutex` varchar(64) NOT NULL COMMENT '需要锁住的资源或者方法',
   -- `state` tinyint NOT NULL DEFAULT 1 COMMENT '1:未分配;2:已分配
   PRIMARY KEY (`id`),
   UNIQUE KEY `unique_mutex`
);
其中,unique_mutex就是我们需要加锁的对象,需要用UNIQUE KEY来保证此对象唯一
加锁时增加一条记录
insert into distribute_lock(unique_mutex) values('mutex_demo');
如果当前SQL执行成功代表加锁成功,如果抛出唯一索引异常(DuplicatedKeyException)则代表加锁失败,当前锁已经被其他竞争者获取。
解锁锁时删除该记录
delete from distribute_lock(unique_mutex) values('muetx_demo');
除了增删记录,也可以通过更新state字段来标识是否获取到锁
update distribute_lock set state = 2 where `unique_mutex` = 'muetx_demo' and state=1;
更新之前需要SELECT确认锁在数据库中存在,没有则创建之。如果创建或更新失败,则说明这个资源已经被别的线程占用了。

数据库排他锁可能出现的问题及解决思路:
没有失效时间, 一旦解锁失败,会导致锁记录一直在数据库中,其他线程无法再获得锁。可通过定时任务清除超时数据来解决
是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。可通过增加字段记录当前主机信息和当线程信息
这个锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的在线程并不会进入阻塞队列,需要不停自旋直到获得锁,相对耗资源。
基于数据库的分布式锁,能够满足一些简单的需求,好处是能够少引入依赖,实现较为简单,缺点是性能较低,且难以满足复杂场景下的高并发需求。


基于redis的实现
一个简单的分布式锁机制是使用setnx、expire 、del 三个命令的组合来实现的。setnx命令的含义为:当且仅当key不存在时,value设置成功,返回1;否则返回0。
加锁,设置锁的唯一标识key,返回1说明加锁成功,返回0加锁失败
setnx key value
设置锁超时时间为30s,防止死锁
expire key 30
解锁, 删除锁
del key
这种思路存在的问题:
1:setnx和expire的非原子性:如果加锁之后,服务器宕机,导致expire和del均执行不了,会导致死锁。
将加锁和设置锁过期时间做成一个原子性操作;在Redis 2.6.12版本之后,set命令增加了NX可选参数,可替代setnx命令;增加了EX可选参数,可以设置key的同时指定过期时间;或者将两个操作封装在lua脚本中,发送给Redis执行,从而实现操作的原子性。
2:del导致误删:A线程超时之后未执行完, 锁过期释放;B线程获得锁,此时A线程执行完,执行del将B线程的锁删除。
将key的value设置为线程相关信息,del释放锁之前先判断一下锁是不是自己的。(释放和判断不是原子性的,需要封装在lua脚本中或者用Redis自身事务)
3:锁过期后引起的并发:A线程超时之后未执行完, 锁过期释放;B线程获得锁,此时A、B线程并发执行会导致线程安全问题。启动一个守护线程,在后台自动给自己的锁''续期“,执行完成,显式关掉守护进程;redis集群环境下,我们自己写的也不OK, 直接上RedLock之Redisson落地实现

redis分布式锁的缺点
在大型的应用中,一般redis服务都是集群形式部署的,由于Slave同步Master是异步的,所以会出现客户端A在Master上加锁,此时Master宕机,Slave没有完成锁的同步,Slave变为Master,客户端B此时可以完成加锁操作。为了解决这一问题,官方给出了redlock算法,即使这样在一些较复杂的场景下也不能100%保证没有问题。


基于zookeeper的实现
zookeeper 是一个开源的分布式协调服务框架,主要用来解决分布式集群中的一致性问题和数据管理问题。zookeeper本质上是一个分布式文件系统,由一群树状节点组成,每个节点可以存放少量数据,且具有唯一性
zookeeper有四种类型的节点:
持久节点(PERSISTENT)默认节点类型,断开连接仍然存在
持久顺序节点(PERSISTENT_SEQUENTIAL)在持久节点的基础上,增加了顺序性。指定创建同名节点,会根据创建顺序在指定的节点名称后面带上顺序编号,以保证节点具有唯一性和顺序性
临时节点(EPHEMERAL)断开连接后,节点会被删除
临时顺序节点(EPHEMERAL_SEQUENTIAL)在临时节点的基础上,增加了顺序性。
基于zookeeper实现的分布式锁主要利用了zookeeper临时顺序节点的特性和事件监听机制。主要思路如下:
1:创建节点实现加锁,通过节点的唯一性,来实现锁的互斥;如果使用临时节点,节点创建成功表示获取到锁;如果使用临时顺序节点,客户端创建的节点为顺序最小节点,表示获取到锁
2:删除节点实现解锁
3:通过临时节点的断开连接自动删除的特性来避免持有锁的服务器宕机而导致的死锁
4:通过节点的顺序性和事件监听机制,大节点监听小节点,形成节点监听链,来实现等待队列(公平锁)
不使用监听机制,未获取到锁的线程自旋重试或者失败退出(根据业务决定),可实现非阻塞的乐观锁。
不使用临时顺序节点,而使用临时节点,所有客户端都去监听该临时节点,可实现非公平锁。但是会产生"羊群效应",单个事件,引发多个服务器响应,占用服务器资源和网络带宽,需要根据业务场景选用。

zookeeper分布式锁的缺点:
1:zookeeper分布式锁是性能可能没有redis分布式锁高,因为每次在创建锁和释放锁的过程中,都要动态创建、销毁临时节点来实现锁功能。
2:使用zookeeper也有可能带来并发问题,只是并不常见而已。比如,由于网络抖动,客户端与zk集群的session连接断了,那么zk以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。就可能产生并发问题。这个问题不常见是因为zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。


总结
从实现的复杂性角度(从高到低)zookeeper >= redis> 数据库
数据库实现的分布式锁易于理解和实现,且不会给项目引入其他依赖。zookeeper和redis需要考虑的情况更多,实现相对较为复杂,但是都有现成的分布式锁框架curator和redision,用起来代码反而可能会更简洁。
从性能角度(从高到低)redis>zookeeper > 数据库
redis数据存在内存,速度很快;zookeeper虽然数据也存在内存中,但是本身维护节点的一致性。需要耗费一些性能;数据库则只有索引在内存中,数据存于磁盘,性能较差。
从可靠性角度(从高到低)zookeeper > redis > 数据库
zookeeper天生设计定位就是分布式协调,强一致性,可靠性较高;redis分布式锁需要较多额外手段去保证可靠性;数据库则较难满足复杂场景的需求。
为了保证缓存和数据库一致性,说说只读缓存的方案?

有数据新增时,会直接写入数据库;

有数据删改时,就需要把只读缓存中的数据标记为无效。这样一来,应用后续再访问这些增删改的数据时,因为缓存中没有相应的数据,就会发生缓存缺失。此时,应用再从数据库中把数据读入缓存,这样后续再访问数据时,就能够直接从缓存中读取了。

建议:优先使用先更新数据库再删除缓存的方法。

原因:先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力;如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。

redis性能排查步骤?

1:获取 Redis 实例在当前环境下的基线性能。命令:./redis-cli --intrinsic-latency 120。用来监测和统计测试期间内的最大延迟

2:是否用了慢查询命令?如果是的话,就使用其他命令替代慢查询命令,或者把聚合计算命令放在客户端做。

3:是否对过期 key 设置了相同的过期时间?对于批量删除的 key,可以在每个 key 的过期时间上加一个随机数,避免同时删除。

4:是否存在 bigkey? 对于 bigkey 的删除操作,如果你的 Redis 是 4.0 及以上的版本,可以直接利用异步线程机制减少主线程阻塞;如果是 Redis 4.0 以前的版本,可以使用 SCAN 命令迭代删除;对于 bigkey 的集合查询和聚合操作,可以使用 SCAN 命令在客户端完成。

5:Redis AOF 配置级别是什么?业务层面是否的确需要这一可靠性级别?如果我们需要高性能,同时也允许数据丢失,可以将配置项 no-appendfsync-on-rewrite 设置为 yes,避免 AOF 重写和 fsync 竞争磁盘 IO 资源,导致 Redis 延迟增加。当然, 如果既需要高性能又需要高可靠性,最好使用高速固态盘作为 AOF 日志的写入盘。

6:Redis 实例的内存使用是否过大?发生 swap 了吗?如果是的话,就增加机器内存,或者是使用 Redis 集群,分摊单机 Redis 的键值对数量和内存压力。同时,要避免出现 Redis 和其他内存需求大的应用共享机器的情况。

7:在 Redis 实例的运行环境中,是否启用了透明大页机制?如果是的话,直接关闭内存大页机制就行了。

8:是否运行了 Redis 主从集群?如果是的话,把主库实例的数据量大小控制在 2~4GB,以免主从复制时,从库因加载大的 RDB 文件而阻塞。

9:是否使用了多核 CPU 或 NUMA 架构的机器运行 Redis 实例?使用多核 CPU 时,可以给 Redis 实例绑定物理核;使用 NUMA 架构时,注意把 Redis 实例和网络中断处理程序运行在同一个 CPU Socket 上。
操作缓存的模式有几种?

根据是否接收写请求,分为读写缓存和只读缓存。
有哪些与redis交互的操作?

和 Redis 实例交互的对象分别有四种:客户端、磁盘、主从节点、切片集群实例。

这些对象和 Redis 交互时,有不同的操作:

客户端对象会有网络 IO交互、键值对 增删改查 操作、数据库操作。

磁盘对象会有生成 RDB 快照、记录 AOF 日志、AOF 日志重写操作。

主从节点对象会有主库生成、传输 RDB 文件、从库接受 RDB 文件、清空数据库、加载 RDB 文件操作。。

切片集群实例对象会有向其他实例传输哈希槽信息、数据迁移操作。

消息中间件

消息中间件怎样保证不丢失消息的?

什么事消息队列?为什么使用mq?使用mq会带来什么问题?使用场景有哪些?

我们可以把消息队列看作是一个存放消息的容器,当我们需要使用消息的时候,直接从容器中取出消息供自己使用即可。队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。

使用消息队列能为我们的系统带来下面三点好处:
1:通过异步处理提高系统性能(减少响应所需时间)。
2:削峰/限流
3:降低系统耦合性。

带来的问题:
1:系统可用性降低: 系统可用性在某种程度上降低,为什么这样说呢?在加入 MQ 之前,你不用考虑消息丢失或者说 MQ 挂掉等等的情况,但是,引入 MQ 之后你就需要去考虑了!
2:系统复杂性提高: 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题!
3:一致性问题: 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了!

最常用的场景就是下单
介绍下JMS和AMQP?

JMS(JAVA Message Service,java 消息服务)是 java 的消息服务,JMS 的客户端之间可以通过 JMS 服务进行异步的消息传输。JMS API 是一个消息服务的标准或者说是规范,允许应用程序组件基于 JavaEE 平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。

JMS 两种消息模型
1:点到点(P2P)模型
使用队列(Queue)作为消息通信载体;满足生产者与消费者模式,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。比如:我们生产者发送 100 条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。)
2:发布/订阅(Pub/Sub)模型
发布订阅模型(Pub/Sub) 使用主题(Topic)作为消息通信载体,类似于广播模式;发布者发布一条消息,该消息通过主题传递给所有的订阅者,在一条消息广播之后才订阅的用户则是收不到该条消息的。

JMS 五种不同的消息正文格式:
StreamMessage -- Java 原始值的数据流
MapMessage--一套名称-值对
TextMessage--一个字符串对象
ObjectMessage--一个序列化的 Java 对象
BytesMessage--一个字节的数据流


AMQP,即 Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准 高级消息队列协议(二进制应用层协议),是应用层协议的一个开放标准,为面向消息的中间件设计,兼容 JMS。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件同产品,不同的开发语言等条件的限制。RabbitMQ 就是基于 AMQP 协议实现的。

AMQP 为消息定义了线路层(wire-level protocol)的协议,而 JMS 所定义的是 API 规范。在 Java 体系中,多个 client 均可以通过 JMS 进行交互,不需要应用修改代码,但是其对跨平台的支持较差。而 AMQP 天然具有跨平台、跨语言特性。
JMS 支持 TextMessage、MapMessage 等复杂的消息类型;而 AMQP 仅支持 byte[] 消息类型(复杂的类型可序列化后发送)。
由于 Exchange 提供的路由算法,AMQP 可以提供多样化的路由方式来传递消息到消息队列,而 JMS 仅支持 队列 和 主题/订阅 方式两种。
对比一下常用的消息队列?

吞吐量:万级的 ActiveMQ 和 RabbitMQ 的吞吐量(ActiveMQ 的性能最差)要比 十万级甚至是百万级的 RocketMQ 和 Kafka 低一个数量级。

可用性:都可以实现高可用。ActiveMQ 和 RabbitMQ 都是基于主从架构实现高可用性。RocketMQ 基于分布式架构。 kafka 也是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用

时效性:RabbitMQ 基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。其他三个都是 ms 级。

功能支持:除了 Kafka,其他三个功能都较为完备。 Kafka 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准

消息丢失:ActiveMQ 和 RabbitMQ 丢失的可能性非常低, RocketMQ 和 Kafka 理论上不会丢失。

ActiveMQ 的社区算是比较成熟,但是较目前来说,ActiveMQ 的性能比较差,而且版本迭代很慢,不推荐使用。

RabbitMQ 在吞吐量方面虽然稍逊于 Kafka 和 RocketMQ ,但是由于它基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。但是也因为 RabbitMQ 基于 erlang 开发,所以国内很少有公司有实力做 erlang 源码级别的研究和定制。如果业务场景对并发量要求不是太高(十万级、百万级),那这四种消息队列中,RabbitMQ 一定是你的首选。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。

RocketMQ 阿里出品,Java 系开源项目,源代码我们可以直接阅读,然后可以定制自己公司的 MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。RocketMQ 社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用RocketMQ 挺好的

Kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。
什么是幂等性?

所谓幂等性就是无论多少次操作和第一次的操作结果一样。如果消息被多次消费,很有可能造成数据的不一致。而如果消息不可避免地被消费多次,如果我们开发人员能通过技术手段保证数据的前后一致性,那也是可以接受的
常见的幂等性场景有哪些?

微信支付结果通知场景
微信官方文档上提到微信支付通知结果可能会推送多次,需要开发者自行保证幂等性。第一次我们可以直接修改订单状态(如支付中 -> 支付成功),第二次就根据订单状态来判断,如果不是支付中,则不进行订单处理逻辑。

插入数据库场景
每次插入数据时,先检查下数据库中是否有这条数据的主键 id,如果有,则进行更新操作。

写 Redis 场景
Redis 的 Set 操作天然幂等性,所以不用考虑 Redis 写数据的问题。

其他场景方案
生产者发送每条数据时,增加一个全局唯一 id,类似订单 id。每次消费时,先去 Redis 查下是否有这个 id,如果没有,则进行正常处理消息,且将 id 存到 Redis。如果查到有这个 id,说明之前消费过,则不要进行重复处理这条消息。
不同业务场景,可能会有不同的幂等性方案,大家选择合适的即可,上面的几种方案只是提供常见的解决思路。

消息丢失的解决方案

rabbitMQ消息乱序场景

rabbitMQ消息乱序解决方案

kafka

介绍下kafka?

Kafka 是一个分布式流式处理平台。

流平台具有三个关键功能:
消息队列:发布和订阅消息流,这个功能类似于消息队列,这也是 Kafka 也被归类为消息队列的原因。
容错的持久方式存储记录消息流: Kafka 会把消息持久化到磁盘,有效避免了消息丢失的风险。
流式处理平台: 在消息发布的时候进行处理,Kafka 提供了一个完整的流式处理类库。

Kafka 主要有两大应用场景:
消息队列 :建立实时流数据管道,以可靠地在系统或应用程序之间获取数据。
数据处理: 构建实时的流数据处理程序来转换或处理数据流。
kafka有什么优势?

极致的性能 :基于 Scala 和 Java 语言开发,设计中大量使用了批量处理和异步的思想,最高可以每秒处理千万级别的消息。
生态系统兼容性无可匹敌 :Kafka 与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域。
kafka的消息模型是什么?

Kafka 采用的就是发布 - 订阅模型。
发布订阅模型(Pub-Sub) 使用主题(Topic) 作为消息通信载体,类似于广播模式;发布者发布一条消息,该消息通过主题传递给所有的订阅者,在一条消息广播之后才订阅的用户则是收不到该条消息的。

在发布 - 订阅模型中,如果只有一个订阅者,那它和队列模型就基本是一样的了。所以说,发布 - 订阅模型在功能层面上是可以兼容队列模型的。
什么是Producer、Consumer、Broker、Topic、Partition?

Kafka 将生产者发布的消息发送到 Topic(主题) 中,需要这些消息的消费者可以订阅这些 Topic(主题)
Kafka 比较重要的几个概念:

Producer(生产者) : 产生消息的一方。
Consumer(消费者) : 消费消息的一方。
Broker(代理) : 可以看作是一个独立的 Kafka 实例。多个 Kafka Broker 组成一个 Kafka Cluster。
同时,每个 Broker 中又包含了 Topic 以及 Partition 这两个重要的概念:

Topic(主题) : Producer 将消息发送到特定的主题,Consumer 通过订阅特定的 Topic(主题) 来消费消息。
Partition(分区) : Partition 属于 Topic 的一部分。一个 Topic 可以有多个 Partition ,并且同一 Topic 下的 Partition 可以分布在不同的 Broker 上,这也就表明一个 Topic 可以横跨多个 Broker 。

什么是kafka分区的多副本机制?

分区(Partition)中的多个副本之间会有一个叫做 leader 的家伙,其他副本称为 follower。我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。

生产者和消费者只与 leader 副本交互。你可以理解为其他副本只是 leader 副本的拷贝,它们的存在只是为了保证消息存储的安全性。当 leader 副本发生故障时会从 follower 中选举出一个 leader,但是 follower 中如果有和 leader 同步程度达不到要求的参加不了 leader 的竞选。

Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力(负载均衡)。
Partition 可以指定对应的 Replica 数, 这也极大地提高了消息存储的安全性, 提高了容灾能力,不过也相应的增加了所需要的存储空间。
zookeeper再kafka中的作用是什么?

roker 注册 :在 Zookeeper 上会有一个专门用来进行 Broker 服务器列表记录的节点。每个 Broker 在启动时,都会到 Zookeeper 上进行注册,即到 /brokers/ids 下创建属于自己的节点。每个 Broker 就会将自己的 IP 地址和端口等信息记录到该节点中去

Topic 注册 : 在 Kafka 中,同一个Topic 的消息会被分成多个分区并将其分布在多个 Broker 上,这些分区信息及与 Broker 的对应关系也都是由 Zookeeper 在维护。比如我创建了一个名字为 my-topic 的主题并且它有两个分区,对应到 zookeeper 中会创建这些文件夹:/brokers/topics/my-topic/Partitions/0、/brokers/topics/my-topic/Partitions/1

负载均衡 :上面也说过了 Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力。 对于同一个 Topic 的不同 Partition,Kafka 会尽力将这些 Partition 分布到不同的 Broker 服务器上。当生产者产生消息后也会尽量投递到不同 Broker 的 Partition 里面。当 Consumer 消费的时候,Zookeeper 可以根据当前的 Partition 数量以及 Consumer 数量来实现动态负载均衡。
Kafka 如何保证消息的消费顺序?

Kafka 中 Partition(分区)是真正保存消息的地方,我们发送的消息都被放在了这里。而我们的 Partition(分区) 又存在于 Topic(主题) 这个概念中,并且我们可以给特定 Topic 指定多个 Partition。
每次添加消息到 Partition(分区) 的时候都会采用尾加法。 Kafka 只能为我们保证 Partition(分区) 中的消息有序。
消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。Kafka 通过偏移量(offset)来保证消息在分区内的顺序性。
一种很简单的保证消息消费顺序的方法:1 个 Topic 只对应一个 Partition。这样当然可以解决问题,但是破坏了 Kafka 的设计初衷。
另一种是 Kafka 中发送 1 条消息的时候,可以指定 topic, partition, key,data(数据) 4 个参数。如果你发送消息的时候指定了 Partition 的话,所有消息都会被发送到指定的 Partition。并且,同一个 key 的消息可以保证只发送到同一个 partition,这个我们可以采用表/对象的 id 来作为 key 。
Kafka 如何保证消息不丢失?

生产者丢失消息的情况
生产者(Producer) 调用send方法发送消息之后,消息可能因为网络问题并没有发送过去。所以,我们不能默认在调用send方法发送消息之后消息发送成功了。为了确定消息是发送成功,我们要判断消息发送的结果。但是要注意的是 Kafka 生产者(Producer) 使用 send 方法发送消息实际上是异步的操作,我们可以通过 get()方法获取调用结果,但是这样也让它变为了同步操作,但是一般不推荐这么做!可以采用为其添加回调函数的形式
ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topic, o);
        future.addCallback(result -> logger.info("生产者成功发送消息到topic:{} partition:{}的消息", result.getRecordMetadata().topic(), result.getRecordMetadata().partition()),
                ex -> logger.error("生产者发送消失败,原因:{}", ex.getMessage()));
                

另外这里推荐为 Producer 的retries (重试次数)设置一个比较合理的值,一般是 3 ,但是为了保证消息不丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送,避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了,网络波动一次你3次一下子就重试完了


消费者丢失消息的情况
当消费者拉取到了分区的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际上并没有被消费,但是 offset 却被自动提交了。
解决办法也比较粗暴,我们手动关闭自动提交 offset,每次在真正消费完消息之后再自己手动提交 offset 。 但是,细心的朋友一定会发现,这样会带来消息被重新消费的问题。比如你刚刚消费完消息之后,还没提交 offset,结果自己挂掉了,那么这个消息理论上就会被消费两次。

Kafka 弄丢了消息
假如 leader 副本所在的 broker 突然挂掉,那么就要从 follower 副本重新选出一个 leader ,但是 leader 的数据还有一些没有被 follower 副本的同步的话,就会造成消息丢失
设置 acks = all
解决办法就是我们设置 acks = all。acks 是 Kafka 生产者(Producer) 很重要的一个参数。
acks 的默认值即为1,代表我们的消息被leader副本接收之后就算被成功发送。当我们配置 acks = all 代表则所有副本都要接收到该消息之后该消息才算真正成功被发送。

设置 replication.factor >= 3
为了保证 leader 副本能有 follower 副本能同步消息,我们一般会为 topic 设置 replication.factor >= 3。这样就可以保证每个 分区(partition) 至少有 3 个副本。虽然造成了数据冗余,但是带来了数据的安全性。

设置 min.insync.replicas > 1
一般情况下我们还需要设置 min.insync.replicas> 1 ,这样配置代表消息至少要被写入到 2 个副本才算是被成功发送。min.insync.replicas 的默认值为 1 ,在实际生产中应尽量避免默认值 1。

但是,为了保证整个 Kafka 服务的高可用性,你需要确保 replication.factor > min.insync.replicas 。为什么呢?设想一下假如两者相等的话,只要是有一个副本挂掉,整个分区就无法正常工作了。这明显违反高可用性!一般推荐设置成 replication.factor = min.insync.replicas + 1。

设置 unclean.leader.election.enable = false
我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。多个 follower 副本之间的消息同步情况不一样,当我们配置了 unclean.leader.election.enable = false 的话,当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。
Kafka 如何保证消息不重复消费?

kafka出现消息重复消费的原因:
服务端侧已经消费的数据没有成功提交 offset(根本原因)。
Kafka 侧 由于服务端处理业务时间长或者网络链接等等原因让 Kafka 认为服务假死,触发了分区 rebalance。

解决方案:
消费消息服务做幂等校验,比如 Redis 的set、MySQL 的主键等天然的幂等功能。这种方法最有效。
将 enable.auto.commit 参数设置为 false,关闭自动提交,开发者在代码中手动提交 offset。那么这里会有个问题:什么时候提交offset合适?
处理完消息再提交:依旧有消息重复消费的风险,和自动提交一样
拉取到消息即提交:会有消息丢失的风险。允许消息延时的场景,一般会采用这种方式。然后,通过定时任务在业务不繁忙(比如凌晨)的时候做数据兜底。

activeMQ

rocketMQ

介绍下rocketMQ

RocketMQ 是一个 队列模型 的消息中间件,具有高性能、高可靠、高实时、分布式 的特点。它是一个采用 Java 语言开发的分布式的消息系统
队列模型
消息中间件的队列模型就真的只是一个队列;“广播” 的概念,也就是说如果我们此时我们需要将一个消息发送给多个消费者(比如此时我需要将信息发送给短信系统和邮件系统),这个时候单个队列即不能满足需求了。

主题模型
在主题模型中,消息的生产者称为 发布者(Publisher) ,消息的消费者称为 订阅者(Subscriber) ,存放消息的容器称为 主题(Topic) 。
其中,发布者将消息发送到指定主题中,订阅者需要 提前订阅主题 才能接受特定主题的消息。

zookeeper

介绍下zookeeper?

ZooKeeper 是一个开源的分布式协调服务。它是一个为分布式应用提供一致性 服务的软件,分布式应用程序可以基于 Zookeeper 实现诸如数据发布/订阅、 负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和 分布式队列等功能。
ZooKeeper 的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性 能高效、功能稳定的系统提供给用户。
Zookeeper 保证了如下分布式一致性特性:
(1)顺序一致性
(2)原子性
(3)单一视图
(4)可靠性
(5)实时性(最终一致性)
客户端的读请求可以被集群中的任意一台机器处理,如果读请求在节点上注册了 监听器,这个监听器也是由所连接的 zookeeper 机器来处理。对于写请求,这 些请求会同时发给其他 zookeeper 机器并且达成一致后,请求才会返回成功。因此,随着 zookeeper 的集群机器增多,读请求的吞吐会提高但是写请求的吞 吐会下降。
有序性是 zookeeper 中非常重要的一个特性,所有的更新都是全局有序的,每 个更新都有一个唯一的时间戳,这个时间戳称为 zxid(Zookeeper Transaction Id)。而读请求只会相对于更新有序,也就是读请求的返回结果中 会带有这个zookeeper 最新的 zxid。
zookeeper提供了什么?文件系统是什么?

提供了文件系统和通知机制

Zookeeper 提供一个多层级的节点命名空间(节点称为 znode)。与文件系统 不同的是,这些节点都可以设置关联的数据,而文件系统中只有文件节点可以存 放数据而目录节点不行。
Zookeeper 为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构, 这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限 为1M。
Zookeeper 怎么保证主从节点的状态同步?

Zookeeper 的核心是原子广播机制,这个机制保证了各个 server 之间的同步。 实现这个机制的协议叫做 Zab 协议。Zab 协议有两种模式,它们分别是恢复模 式和广播模式。
恢复模式
当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出 来,且大多数 server 完成了和 leader 的状态同步以后,恢复模式就结束了。状 态同步保证了leader 和 server 具有相同的系统状态。
广播模式
一旦 leader 已经和多数的 follower 进行了状态同步后,它就可以开始广播消息 了,即进入广播状态。这时候当一个 server 加入 ZooKeeper 服务中,它会在 恢复模式下启动,发现 leader,并和 leader 进行状态同步。待到同步结束,它也参与消息广播。ZooKeeper 服务一直维持在 Broadcast 状态,直到 leader 崩溃了或者 leader 失去了大部分的followers 支持。
zookeeper有哪几种数据节点?

(1)PERSISTENT-持久节点
除非手动删除,否则节点一直存在于 Zookeeper 上 
(2)EPHEMERAL-临时节点
临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与 zookeeper 连接断开不一定会话失效),那么这个客户端创建的所有临时节点 都会被移除。
(3)PERSISTENT_SEQUENTIAL-持久顺序节点
基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维 护的自增整型数字。
(4)EPHEMERAL_SEQUENTIAL-临时顺序节点
基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的 自增整型数字。
zookeeper 是如何保证事务的顺序一致性的?

zookeeper 采用了全局递增的事务 Id 来标识,所有的 proposal(提议)都在被提出的时候加上了 zxid,zxid 实际上是一个 64 位的数字,高 32 位是epoch( 时期; 纪元; 世; 新时代)用来标识 leader 周期,如果有新的 leader 产生出来,epoch会自增,低 32 位用来递增计数。当新产生 proposal 的时候,会依据数据库的两阶段过程,首先会向其他的 server 发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。
zookeeper的选举机制是怎样的?

当 leader 崩溃或者 leader 失去大多数的 follower,这时 zk 进入恢复模式,恢复模式需要重新选举出一个新的 leader,让所有的 Server 都恢复到一个正确的状态。
Zk 的选举算法有两种:一种是基于 basic paxos 实现的,另外一种是基于 fast paxos 算法实现的。系统默认的选举算法为fast paxos。 1、Zookeeper 选主流程(basic paxos)
(1) 选举线程由当前 Server 发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的Server; 
(2) 选举线程首先向所有 Server 发起一次询问(包括自己); 
(3) 选举线程收到回复后,验证是否是自己发起的询问(验证 zxid 是否一致),然后获取对方的 id(myid),并存储到当前询问对象列表中,最后获取对方提议的 leader 相关信息(id,zxid),并将这些信息存储到当次选举的投票记录表中;
(4) 收到所有 Server 回复以后,就计算出 zxid 最大的那个 Server,并将这个 Server 相关信息设置成下一次要投票的 Server; (5) 线程将当前 zxid 最大的 Server 设置为当前 Server 要推荐的 Leader,如果此时获胜的 Server 获得 n/2 + 1 的 Server 票数,设置当前推荐的 leader 为获胜的 Server,将根据获胜的 Server 相关信息设置自己的状态,否则,继续这个过程,直到 leader 被选举出来。 通过流程分析我们可以得出:要使 Leader 获得多数Server 的支持,则 Server总数必须是奇数 2n+1,且存活的 Server 的数目不得少于 n+1. 每个 Server 启动后都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的 server 还会从磁盘快照中恢复数据和会话信息,zk 会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。

dubbo

dubbo的分层是什么?
从大的范围来说,dubbo分为三层,business业务逻辑层由我们自己来提供接口和实现还有一些配置信息,RPC层就是真正的RPC调用的核心层,封装整个RPC的调用过程、负载均衡、集群容错、代理,remoting则是对网络传输协议和数据转换的封装。
划分到更细的层面,就是图中的10层模式,整个分层依赖由上至下,除开business业务逻辑之外,其他的几层都是SPI机制。

dubbo的工作原理是什么?

服务启动的时候,provider和consumer根据配置信息,连接到注册中心register,分别向注册中心注册和订阅服务

register根据服务订阅关系,返回provider信息到consumer,同时consumer会把provider信息缓存到本地。如果信息有变更,consumer会收到来自register的推送

consumer生成代理对象,同时根据负载均衡策略,选择一台provider,同时定时向monitor记录接口的调用次数和时间信息

拿到代理对象之后,consumer通过代理对象发起接口调用

provider收到请求后对数据进行反序列化,然后通过代理调用具体的接口实现

为什么要通过代理对象通信?

主要是为了实现接口的透明代理,封装调用细节,让用户可以像调用本地方法一样调用远程方法,同时还可以通过代理实现一些其他的策略,比如:

1、调用的负载均衡策略

2、调用失败、超时、降级和容错机制

3、做一些过滤操作,比如加入缓存、mock数据

4、接口调用数据统计
阐述一下服务暴露的流程?

1:在容器启动的时候,通过ServiceConfig解析标签,创建dubbo标签解析器来解析dubbo的标签,容器创建完成之后,触发ContextRefreshEvent事件回调开始暴露服务
2:通过ProxyFactory获取到invoker,invoker包含了需要执行的方法的对象信息和具体的URL地址
3:再通过DubboProtocol的实现把包装后的invoker转换成exporter,然后启动服务器server,监听端口
4:最后RegistryProtocol保存URL地址和invoker的映射关系,同时注册到服务中心

阐述一下服务引用的流程?

服务暴露之后,客户端就要引用服务,然后才是调用的过程。

1:首先客户端根据配置文件信息从注册中心订阅服务

2:之后DubboProtocol根据订阅的得到provider地址和接口信息连接到服务端server,开启客户端client,然后创建invoker

3:invoker创建完成之后,通过invoker为服务接口生成代理对象,这个代理对象用于远程调用provider,服务的引用就完成了


负载均衡策略有哪些?

1:加权随机:假设我们有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3, 2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上就可以了。

2:最小活跃数:每个服务提供者对应一个活跃数 active,初始情况下,所有服务提供者活跃数均为0。每收到一个请求,活跃数加1,完成请求后则将活跃数减1。在服务运行一段时间后,性能好的服务提供者处理请求的速度更快,因此活跃数下降的也越快,此时这样的服务提供者能够优先获取到新的服务请求。

3:一致性hash:通过hash算法,把provider的invoke和随机节点生成hash,并将这个 hash 投射到 [0, 2^32 - 1] 的圆环上,查询的时候根据key进行md5然后进行hash,得到第一个节点的值大于等于当前hash的invoker。

4:加权轮询:比如服务器 A、B、C 权重比为 5:2:1,那么在8次请求中,服务器 A 将收到其中的5次请求,服务器 B 会收到其中的2次请求,服务器 C 则收到其中的1次请求。
集群容错方式有哪些?

1:Failover Cluster失败自动切换:dubbo的默认容错方案,当调用失败时自动切换到其他可用的节点,具体的重试次数和间隔时间可用通过引用服务的时候配置,默认重试次数为1也就是只调用一次。

2:Failback Cluster快速失败:在调用失败,记录日志和调用信息,然后返回空结果给consumer,并且通过定时任务每隔5秒对失败的调用进行重试

3:Failfast Cluster失败自动恢复:只会调用一次,失败后立刻抛出异常

4:Failsafe Cluster失败安全:调用出现异常,记录日志不抛出,返回空结果

5:Forking Cluster并行调用多个服务提供者:通过线程池创建多个线程,并发调用多个provider,结果保存到阻塞队列,只要有一个provider成功返回了结果,就会立刻返回结果

6:Broadcast Cluster广播模式:逐个调用每个provider,如果其中一台报错,在循环调用结束后,抛出异常。
自定义一个RPC框架怎样设计?

1:首先需要一个服务注册中心,这样consumer和provider才能去注册和订阅服务
2:需要负载均衡的机制来决定consumer如何调用客户端,这其中还当然要包含容错和重试的机制
3:需要通信协议和工具框架,比如通过http或者rmi的协议通信,然后再根据协议选择使用什么框架和工具来进行通信,当然,数据的传输序列化要考虑
4:除了基本的要素之外,像一些监控、配置管理页面、日志是额外的优化考虑因素。

ES

什么是ELK?
E:Elasticsearch


L:Logstash
Logstash 是 Elastic Stack 的核心产品之一,可用来对数据进行聚合和处理,并将数据发送到 Elasticsearch。Logstash 是一个开源的服务器端数据处理管道,允许您在将数据索引到 Elasticsearch 之前同时从多个来源采集数据,并对数据进行充实和转换。

K:Kibana
Kibana 是一款适用于 Elasticsearch 的数据可视化和管理工具,可以提供实时的直方图、线性图等。
ES索引是什么?

Elasticsearch 索引指相互关联的文档集合。Elasticsearch 会以 JSON 文档的形式存储数据。每个文档都会在一组键 ( 字段或属性的名称 ) 和它们对应的值 ( 字符串、数字、布尔值、日期、数值组、地理位置或其他类型的数据 ) 之间建立联系。

Elasticsearch 使用的是一种名为倒排索引的数据结构,这一结构的设计可以允许十分快速地进行全文本搜索。倒排索引会列出在所有文档中出现的每个特有词汇,并且可以找到包含每个词汇的全部文档。

在索引过程中,Elasticsearch 会存储文档并构建倒排索引,这样用户便可以近实时地对文档数据进行搜索。索引过程是在索引 API 中启动的,通过此 API 您既可向特定索引中添加 JSON 文档,也可更改特定索引中的 JSON 文档。

分布式

CAP理论

什么是CAP?

CAP 理论是对分布式系统的特性做了一个高度的抽象,变成了三大指标:
一致性(Consistency)
一致性强调的是数据正确,每次读取节点中的数据都是最新写入的数据。

可用性(Availability)
可用性,每个节点使用本地数据来响应客户端的请求。另外当节点不可用时,可以使用快速失败策略,至少不能让服务长时间不能响应可用性强调的是服务可用,不保证数据正确。

分区容错性(Partition Tolerance)
分区容错性的含义就是节点间出现任意数量的消息丢失或高延迟的时候,系统仍然在继续工作。分布式系统告诉客户端,我的内部不论出现什么样的数据同步问题,我会一直运行。强调的是集群堆分区故障的容错能力。

对于分布式系统,CAP 三个指标只能选择其中两个。
CA:保证一致性和可用性。当分布式系统正常运行时(大部分时候所处的状态),这个时候不需要 P,那么 C 和 A 能够同时保证。只有在发生分区故障时,才需要 P,这个时候就只能在 C 和 A 之间做出选择。典型应用:单机版部署的 MySQL。

CP:保证数据的一致性和分区容错性,比如配置信息,必须保证每个节点存的都是最新的,正确的数据。比如 Raft 的强一致性系统,会导致无法执行读操作和写操作。典型应用:Etcd、Consul、Hbase。

AP:保证分布式系统的可用性和分区容错性。用户访问系统,都能得到相应数据,不会出现响应错误,但是可能会读到旧的数据。典型应用:Cassandra 和 DynamoDB。

BASE理论

什么是BASE理论?

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的缩写。BASE 理论是对 CAP  中 AP 的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足 BASE 理论的事务,我们称之为柔性事务。

基本可用 : 分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如电商网址交易付款出现问题来,商品依然可以正常浏览。

软状态: 由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫软状态),这个状态不影响系统可用性,如订单中的“支付中”、“数据同步中”等状态,待数据最终一致后状态改为“成功”状态。

最终一致性: 最终一致是指的经过一段时间后,所有节点数据都将会达到一致。如订单的“支付中”状态,最终会变为“支付成功”或者“支付失败”,使订单状态与实际交易结果达成一致,但需要一定时间的延迟、等待。

负载均衡

常用的负载均衡算法有哪几个?

轮询:请求依次按顺序分发到不同的可用服务器执行,循环分发请求。
最小连接:分发请求到连接数最少的服务器。场景:处理请求用时较长的场景。
散列:根据用户请求的IP地址的散列(hash)来选择要转发的服务器。场景:需要处理状态而要求用户能连接到相同服务器。

服务熔断降级

什么是雪崩?熔断?降级?

第一次滚雪球:库存服务不可用(如响应超时等),库存服务收到的很多请求都未处理完,库存服务将无法处理更多请求。
第二次滚雪球:因商品服务的请求都在等库存服务返回结果,导致商品服务调用库存服务的很多请求未处理完,商品服务将无法处理其他请求,导致商品服务不可用
第三次滚雪球:因商品服务不可用,订单服务调用商品服务的的其他请求无法处理,导致订单服务不可用。
第四次滚雪球:因订单服务不可用,客户端将不能下单,更多客户将重试下单,将导致更多下单请求不可用。

熔断:设置服务的超时,当被调用的服务某段时间内失败率达到某个阈值,则对该服务开启短路保护,后来的请求不调用这个服务,直接返回默认的数据。

降级:
对非核心业务降级运行:某些服务不处理,或者简单处理(抛异常、返回Null、返回Mock数据)

上一篇 八股文-基础

Comments

Content