多是一份最适合你的后端面试指南(部份内容前端一样适用)| 掘金技术征文

本文系掘金首发,禁止转载哦! 若是以为文章内容不错的话,欢迎为我转身,啊!不对,是给我一个赞!点赞以后会有惊喜哦!html

看本文以前,推荐给你们一个阿里云双11活动,真的很是很是很是推荐,对于新人阿里云真的是下血本了,建议阿里云新人必定必定必定不要错过。若是以为这单纯是广告的话,你能够直接跳过看正文。前端

阿里云双11最新活动(仅限阿里云新用户购买,老用户拉新用户能够得到返现红包,后续有机会平分百万红包),优惠力度很是很是很是大,另外加入拼团,后续还有机会平分100w红包!目前个人战队已经有12位新人了,如今是折上5折了也就是1折购买!!!。 划重点了: 1核2G云服务器1年仅需99.5元!!!1核2G云服务器3年仅需298.50元!!!一个月仅需8.2元 该折扣仅限新人!这是个人拼团团队地址:m.aliyun.com/act/team111…java


写本文以前,其实我本身已经开源了一个 Java学习指南的文档,里面包含了一些基础知识和一些后端(偏 Java 方向)的知识。到目前为止收获了 6.1k star 以及 1.5 k fork,close 了 10个 pr 以及 10 个issue。开源只是为了让更多的人看到和参与进来,这样文档的正确性和质量才能很好的保证。毕竟,我我的能力、时间以及知识广度和深度有限,一份好的项目的诞生确定离不开和其余人的共同努力。linux

另外,我我的以为不论你是前端仍是后端(部份内容可能会偏 Java 方向一点)都能从本文中学到东西。git

本人技术水平有限,欢迎各位指正!写的很差的话,请多见谅!程序员

目录

  • 前言
  • 一 简历该如何写
    • 1.1 为何说简历很重要?
    • 1.2-这3点你必须知道
    • 1.3-两大法则了解一
    • 1.4-项目经历怎么写?
    • 1.5-专业技能该怎么写?
    • 1.6-开源程序员简历模板分享
    • 1.7 其余的一些小tips
  • 二 计算机网络常见面试点总结
    • 计算机网络常见问题回顾
    • 2.1 TCP、UDP 协议的区别
    • 2.2 在浏览器中输入url地址 ->> 显示主页的过程
    • 2.3 各类协议与HTTP协议之间的关系
    • 2.4 HTTP长链接、短链接
    • 2.5 TCP 三次握手和四次挥手
  • 三 Linux
    • 3.1-简单介绍一下-linux-文件系统?
    • 3.2 一些常见的 Linux 命令了解吗?
  • 四 MySQL
    • 4.1 说说本身对于 MySQL 常见的两种存储引擎:MyISAM与InnoDB的理解
    • 4.2 数据库索引了解吗?
    • 4.3 对于大表的常见优化手段说一下
  • 五 Redis
    • 5.1 redis 简介
    • 5.2 为何要用 redis /为何要用缓存
    • 5.3 为何要用 redis 而不用 map/guava 作缓存?
    • 5.4 redis 和 memcached 的区别
    • 5.5 redis 常见数据结构以及使用场景分析
    • 5.6 redis 设置过时时间
    • 5.7 redis 内存淘汰机制
    • 5.8 redis 持久化机制(怎么保证 redis 挂掉以后再重启数据能够进行恢复)
    • 5.9 缓存雪崩和缓存穿透问题解决方案
    • 5.10 如何解决 Redis 的并发竞争 Key 问题
    • 5.11 如何保证缓存与数据库双写时的数据一致性?
  • 六 Java
    • 6.1 Java 基础知识
    • 6.2 Java 集合框架
    • 6.3 Java多线程
    • 6.4 Java虚拟机
    • 6.5 设计模式
  • 七 数据结构
  • 八 算法
    • 8.1 举个栗子(手写快排)
  • 九 Spring
    • 9.1 Spring Bean 的做用域
    • 9.2 Spring 事务中的隔离级别
    • 9.3 Spring 事务中的事务传播行为
    • 9.4 AOP
    • 9.5 IOC
  • 十 实际场景题
  • 写在最后

前言

不管是校招仍是社招都避免不了各类面试、笔试,如何去准备这些东西就显得格外重要。不管是笔试仍是面试都是有章可循的,我这个“有章可循”说的意思只是说应对技术面试是能够提早准备。 我其实特别不喜欢那种临近考试就提早背啊记啊各类题的行为,很是反对!我以为这种方法特别极端,并且在稍有一点经验的面试官面前是根本没有用的。建议你们仍是一步一个脚印踏踏实实地走。github

指挥若定以后,决胜千里以外!不打毫无准备的仗,我以为你们能够先从下面几个方面来准备面试:面试

  1. 自我介绍。(你可千万这样介绍:“我叫某某,性别,来自哪里,学校是那个,本身爱干什么”,记住:多说点简历上没有的,多说点本身哪里比别人强!)
  2. 本身面试中可能涉及哪些知识点、那些知识点是重点。
  3. 面试中哪些问题会被常常问到、面试中本身改如何回答。(强烈不推荐背题,第一:经过背这种方式你能记住多少?能记住多久?第二:背题的方式的学习很难坚持下去!)
  4. 本身的简历该如何写。

“80%的offer掌握在20%的人手中” 这句话也不是不无道理的。决定你面试可否成功的因素中实力当然占有很大一部分比例,可是若是你的心态或者说运气很差的话,依然没法拿到满意的 offer。运气暂且不谈,就拿心态来讲,千万不要由于面试失败而气馁或者说怀疑本身的能力,面试失败以后多总结一下失败的缘由,后面你就会发现本身会愈来愈强大。redis

另外,你们要明确的很重要的几点是:算法

  1. 写在简历上的东西必定要慎重,这多是面试官大量提问的地方;
  2. 大部分应届生找工做的硬伤是没有工做经验或实习经历;
  3. 将本身的项目经历完美的展现出来很是重要。

笔主能力有限,若是有不对的地方或者和你想法不一样的地方,敬请雅正、不舍赐教。

若是想了解个人更多信息,能够关注个人 Github 或者微信公众号:"Java面试通关手册"。

一 简历该如何写

程序员的简历之道

俗话说的好:“工欲善其事,必先利其器”。准备一份好的简历对于能不能找到一份好工做起到了相当重要的做用。

1.1 为何说简历很重要?

假如你是网申,你的简历必然会通过HR的筛选,一张简历HR可能也就花费10秒钟看一下,而后HR就会决定你这一关是Fail仍是Pass。

假如你是内推,若是你的简历没有什么优点的话,就算是内推你的人再用心,也无能为力。

另外,就算你经过了筛选,后面的面试中,面试官也会根据你的简从来判断你到底是否值得他花费不少时间去面试。

1.2 这3点你必须知道

  1. 大部分应届生找工做的硬伤是没有工做经验或实习经历;
  2. 写在简历上的东西必定要慎重,这多是面试官大量提问的地方;
  3. 将本身的项目经历完美的展现出来很是重要。

1.3 两大法则了解一下

目前写简历的方式有两种广泛被承认,一种是 STAR, 一种是 FAB。

STAR法则(Situation Task Action Result):

  • Situation: 事情是在什么状况下发生;
  • Task:: 你是如何明确你的任务的;
  • Action: 针对这样的状况分析,你采用了什么行动方式;
  • Result: 结果怎样,在这样的状况下你学习到了什么。

FAB 法则(Feature Advantage Benefit):

  • Feature: 是什么;
  • Advantage: 比别人好在哪些地方;
  • Benefit: 若是雇佣你,招聘方会获得什么好处。

1.4 项目经历怎么写?

简历上有一两个项目经历很正常,可是真正能把项目经历很好的展现给面试官的很是少。对于项目经历你们能够考虑从以下几点来写:

  1. 对项目总体设计的一个感觉
  2. 在这个项目中你负责了什么、作了什么、担任了什么角色
  3. 从这个项目中你学会了那些东西,使用到了那些技术,学会了那些新技术的使用
  4. 另外项目描述中,最好能够体现本身的综合素质,好比你是如何协调项目组成员协同开发的或者在遇到某一个棘手的问题的时候你是如何解决的。

1.5 专业技能该怎么写?

先问一下你本身会什么,而后看看你意向的公司须要什么。通常HR可能并不太懂技术,因此他在筛选简历的时候可能就盯着你专业技能的关键词来看。对于公司有要求而你不会的技能,你能够花几天时间学习一下,而后在简历上能够写上本身了解这个技能。好比你能够这样写:

  • Dubbo:精通
  • Spring:精通
  • Docker:掌握
  • SOA分布式开发 :掌握
  • Spring Cloud:了解

1.6 开源程序员简历模板分享

分享一个Github上开源的程序员简历模板。包括PHP程序员简历模板、iOS程序员简历模板、Android程序员简历模板、Web前端程序员简历模板、Java程序员简历模板、C/C++程序员简历模板、NodeJS程序员简历模板、架构师简历模板以及通用程序员简历模板 。 Github地址:github.com/geekcompany…

若是想学如何用 Markdown 写简历写一份高质量简历,请看这里:github.com/Snailclimb/…

1.7 其余的一些小tips

  1. 尽可能避免主观表述,少一点语义模糊的形容词,尽可能要简洁明了,逻辑结构清晰。
  2. 注意排版(不须要花花绿绿的),尽可能使用Markdown语法。
  3. 若是本身有博客或者我的技术栈点的话,写上去会为你加分不少。
  4. 若是本身的Github比较活跃的话,写上去也会为你加分不少。
  5. 注意简历真实性,必定不要写本身不会的东西,或者带有欺骗性的内容
  6. 项目经历建议以时间倒序排序,另外项目经历不在于多,而在于有亮点。
  7. 若是内容过多的话,不须要非把内容压缩到一页,保持排版干净整洁就能够了。
  8. 简历最后最好能加上:“感谢您花时间阅读个人简历,期待能有机会和您共事。”这句话,显的你会颇有礼貌。

二 计算机网络常见面试点总结

网络分层结构

计算机网络常见问题回顾

  • TCP三次握手和四次挥手、
  • 在浏览器中输入url地址->>显示主页的过程
  • TCP 协议如何保证可靠传输
  • HTTP和HTTPS的区别
  • TCP、UDP协议的区别
  • 常见的状态码。

下面列举几个常见问题的回答!

2.1 TCP、UDP 协议的区别

TCP、UDP协议的区别

UDP 在传送数据以前不须要先创建链接,远地主机在收到 UDP 报文后,不须要给出任何确认。虽然 UDP 不提供可靠交付,但在某些状况下 UDP 确是一种最有效的工做方式(通常用于即时通讯),好比: QQ 语音、 QQ 视频 、直播等等

TCP 提供面向链接的服务。在传送数据以前必须先创建链接,数据传送结束后要释放链接。 TCP 不提供广播或多播服务。因为 TCP 要提供可靠的,面向链接的运输服务(TCP的可靠体如今TCP在传递数据以前,会有三次握手来创建链接,并且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开链接用来节约系统资源),这一难以免增长了许多开销,如确认,流量控制,计时器以及链接管理等。这不只使协议数据单元的首部增大不少,还要占用许多处理机资源。TCP 通常用于文件传输、发送和接收邮件、远程登陆等场景。

2.2 在浏览器中输入url地址 ->> 显示主页的过程

百度好像最喜欢问这个问题。

打开一个网页,整个过程会使用哪些协议

图片来源:《图解HTTP》

状态码

整体来讲分为如下几个过程:

  1. DNS解析
  2. TCP链接
  3. 发送HTTP请求
  4. 服务器处理请求并返回HTTP报文
  5. 浏览器解析渲染页面
  6. 链接结束

具体能够参考下面这篇文章:

2.3 各类协议与HTTP协议之间的关系

通常面试官会经过这样的问题来考察你对计算机网络知识体系的理解。

图片来源:《图解HTTP》

各类协议与HTTP协议之间的关系

2.4 HTTP长链接、短链接

在HTTP/1.0中默认使用短链接。也就是说,客户端和服务器每进行一次HTTP操做,就创建一次链接,任务结束就中断链接。当客户端浏览器访问的某个HTML或其余类型的Web页中包含有其余的Web资源(如JavaScript文件、图像文件、CSS文件等),每遇到这样一个Web资源,浏览器就会从新创建一个HTTP会话。

而从HTTP/1.1起,默认使用长链接,用以保持链接特性。使用长链接的HTTP协议,会在响应头加入这行代码:

Connection:keep-alive
复制代码

在使用长链接的状况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP链接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经创建的链接。Keep-Alive不会永久保持链接,它有一个保持时间,能够在不一样的服务器软件(如Apache)中设定这个时间。实现长链接须要客户端和服务端都支持长链接。

HTTP协议的长链接和短链接,实质上是TCP协议的长链接和短链接。

—— 《HTTP长链接、短链接到底是什么?》

2.5 TCP 三次握手和四次挥手(面试常客)

为了准确无误地把数据送达目标处,TCP协议采用了三次握手策略。

漫画图解:

图片来源:《图解HTTP》

TCP三次握手

简单示意图:

TCP三次握手

  • 客户端–发送带有 SYN 标志的数据包–一次握手–服务端
  • 服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端
  • 客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端

为何要三次握手?

三次握手的目的是创建可靠的通讯信道,说到通信,简单来讲就是数据的发送与接收,而三次握手最主要的目的就是双方确认本身与对方的发送与接收是正常的。

第一次握手:Client 什么都不能确认;Server 确认了对方发送正常

第二次握手:Client 确认了:本身发送、接收正常,对方发送、接收正常;Server 确认了:本身接收正常,对方发送正常

第三次握手:Client 确认了:本身发送、接收正常,对方发送、接收正常;Server 确认了:本身发送、接收正常,对方发送接收正常

因此三次握手就能确认双发收发功能都正常,缺一不可。

为何要传回 SYN

接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。

SYN 是 TCP/IP 创建链接时使用的握手信号。在客户机和服务器之间创建正常的 TCP 网络链接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement[汉译:确认字符 ,在数据通讯传输中,接收站发给发送站的一种传输控制字符。它表示确认发来的数据已经接受无误。 ])消息响应。这样在客户机和服务器之间才能创建起可靠的TCP链接,数据才能够在客户机和服务器之间传递。

传了 SYN,为啥还要传 ACK

双方通讯无误必须是二者互相发送信息都无误。传了 SYN,证实发送方到接收方的通道没有问题,可是接收方到发送方的通道还须要 ACK 信号来进行验证。

TCP四次挥手

断开一个 TCP 链接则须要“四次挥手”:

  • 客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送
  • 服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加1 。和 SYN 同样,一个 FIN 将占用一个序号
  • 服务器-关闭与客户端的链接,发送一个FIN给客户端
  • 客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加1

为何要四次挥手

任何一方均可以在数据传送结束后发出链接释放的通知,待对方确认后进入半关闭状态。当另外一方也没有数据再发送的时候,则发出链接释放通知,对方确认后就彻底关闭了TCP链接。

举个例子:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B回答“我知道了”,可是 B 可能还会有要说的话,A 不能要求 B 跟着本身的节奏结束通话,因而 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,这样通话才算结束。

上面讲的比较归纳,推荐一篇讲的比较细致的文章:

三 Linux

Linux

3.1 简单介绍一下 Linux 文件系统?

Linux文件系统简介

在Linux操做系统中,全部被操做系统管理的资源,例如网络接口卡、磁盘驱动器、打印机、输入输出设备、普通文件或是目录都被看做是一个文件。

也就是说在LINUX系统中有一个重要的概念:一切都是文件。其实这是UNIX哲学的一个体现,而Linux是重写UNIX而来,因此这个概念也就传承了下来。在UNIX系统中,把一切资源都看做是文件,包括硬件设备。UNIX系统把每一个硬件都当作是一个文件,一般称为设备文件,这样用户就能够用读写文件的方式实现对硬件的访问。

文件类型与目录结构

Linux支持5种文件类型 :

文件类型

Linux的目录结构以下:

Linux文件系统的结构层次鲜明,就像一棵倒立的树,最顶层是其根目录:

Linux的目录结构

常见目录说明:

  • /bin: 存放二进制可执行文件(ls,cat,mkdir等),经常使用命令通常都在这里;
  • /etc: 存放系统管理和配置文件;
  • /home: 存放全部用户文件的根目录,是用户主目录的基点,好比用户user的主目录就是/home/user,能够用~user表示;
  • /usr : 用于存放系统应用程序;
  • /opt: 额外安装的可选应用程序包所放置的位置。通常状况下,咱们能够把tomcat等都安装到这里;
  • /proc: 虚拟文件系统目录,是系统内存的映射。可直接访问这个目录来获取系统信息;
  • /root: 超级用户(系统管理员)的主目录(特权阶级^o^);
  • /sbin: 存放二进制可执行文件,只有root才能访问。这里存放的是系统管理员使用的系统级别的管理命令和程序。如ifconfig等;
  • /dev: 用于存放设备文件;
  • /mnt: 系统管理员安装临时文件系统的安装点,系统提供这个目录是让用户临时挂载其余的文件系统;
  • /boot: 存放用于系统引导时使用的各类文件;
  • /lib : 存放着和系统运行相关的库文件 ;
  • /tmp: 用于存放各类临时文件,是公用的临时文件存储点;
  • /var: 用于存放运行时须要改变数据的文件,也是某些大文件的溢出区,比方说各类服务的日志文件(系统启动日志等。)等;
  • /lost+found: 这个目录平时是空的,系统非正常关机而留下“无家可归”的文件(windows下叫什么.chk)就在这里。

3.2 一些常见的 Linux 命令了解吗?

目录切换命令

  • cd usr 切换到该目录下usr目录
  • cd ..(或cd../) 切换到上一层目录
  • cd / 切换到系统根目录
  • cd ~ 切换到用户主目录
  • cd - 切换到上一个所在目录

目录的操做命令(增删改查)

  1. mkdir 目录名称 增长目录

  2. ls或者ll(ll是ls -l的缩写,ll命令以看到该目录下的全部目录和文件的详细信息):查看目录信息

  3. find 目录 参数 寻找目录(查)

  4. mv 目录名称 新目录名称 修改目录的名称(改)

    注意:mv的语法不只能够对目录进行重命名并且也能够对各类文件,压缩包等进行 重命名的操做。mv命令用来对文件或目录从新命名,或者将文件从一个目录移到另外一个目录中。后面会介绍到mv命令的另外一个用法。

  5. mv 目录名称 目录的新位置 移动目录的位置---剪切(改)

    注意:mv语法不只能够对目录进行剪切操做,对文件和压缩包等均可执行剪切操做。另外mv与cp的结果不一样,mv好像文件“搬家”,文件个数并未增长。而cp对文件进行复制,文件个数增长了。

  6. cp -r 目录名称 目录拷贝的目标位置 拷贝目录(改),-r表明递归拷贝

    注意:cp命令不只能够拷贝目录还能够拷贝文件,压缩包等,拷贝文件和压缩包时不 用写-r递归

  7. rm [-rf] 目录: 删除目录(删)

    注意:rm不只能够删除目录,也能够删除其余文件或压缩包,为了加强你们的记忆, 不管删除任何目录或文件,都直接使用rm -rf 目录/文件/压缩包

文件的操做命令(增删改查)

  1. touch 文件名称: 文件的建立(增)

  2. cat/more/less/tail 文件名称 文件的查看(查)

    • cat 只能显示最后一屏内容
    • more 能够显示百分比,回车能够向下一行, 空格能够向下一页,q能够退出查看
    • less 能够使用键盘上的PgUp和PgDn向上 和向下翻页,q结束查看
    • tail-10 查看文件的后10行,Ctrl+C结束

    注意:命令 tail -f 文件 能够对某个文件进行动态监控,例如tomcat的日志文件, 会随着程序的运行,日志会变化,能够使用tail -f catalina-2016-11-11.log 监控 文 件的变化

  3. vim 文件 修改文件的内容(改)

    vim编辑器是Linux中的强大组件,是vi编辑器的增强版,vim编辑器的命令和快捷方式有不少,但此处不一一阐述,你们也无需研究的很透彻,使用vim编辑修改文件的方式基本会使用就能够了。

    在实际开发中,使用vim编辑器主要做用就是修改配置文件,下面是通常步骤:

    vim 文件------>进入文件----->命令模式------>按i进入编辑模式----->编辑文件 ------->按Esc进入底行模式----->输入:wq/q! (输入wq表明写入内容并退出,即保存;输入q!表明强制退出不保存。)

  4. rm -rf 文件 删除文件(删)

    同目录删除:熟记 rm -rf 文件 便可

压缩文件的操做命令

1)打包并压缩文件:

Linux中的打包文件通常是以.tar结尾的,压缩的命令通常是以.gz结尾的。

而通常状况下打包和压缩是一块儿进行的,打包并压缩后的文件的后缀名通常.tar.gz。 命令:tar -zcvf 打包压缩后的文件名 要打包压缩的文件 其中:

z:调用gzip压缩命令进行压缩

c:打包文件

v:显示运行过程

f:指定文件名

好比:加入test目录下有三个文件分别是 :aaa.txt bbb.txt ccc.txt,若是咱们要打包test目录并指定压缩后的压缩包名称为test.tar.gz能够使用命令:tar -zcvf test.tar.gz aaa.txt bbb.txt ccc.txt或:tar -zcvf test.tar.gz /test/

2)解压压缩包:

命令:tar [-xvf] 压缩文件

其中:x:表明解压

示例:

1 将/test下的test.tar.gz解压到当前目录下能够使用命令:tar -xvf test.tar.gz

2 将/test下的test.tar.gz解压到根目录/usr下:tar -xvf xxx.tar.gz -C /usr(- C表明指定解压的位置)

其余经常使用命令

  • pwd 显示当前所在位置

  • grep 要搜索的字符串 要搜索的文件 --color 搜索命令,--color表明高亮显示

  • ps -ef/ps aux 这两个命令都是查看当前系统正在运行进程,二者的区别是展现格式不一样。若是想要查看特定的进程能够使用这样的格式:ps aux|grep redis (查看包括redis字符串的进程)

    注意:若是直接用ps((Process Status))命令,会显示全部进程的状态,一般结合grep命令查看某进程的状态。

  • kill -9 进程的pid 杀死进程(-9 表示强制终止。)

    先用ps查找进程,而后用kill杀掉

  • 网络通讯命令:

    • 查看当前系统的网卡信息:ifconfig
    • 查看与某台机器的链接状况:ping
    • 查看当前系统的端口使用:netstat -an
  • shutdown shutdown -h now: 指定如今当即关机;shutdown +5 "System will shutdown after 5 minutes":指定5分钟后关机,同时送出警告信息给登入用户。

  • reboot reboot 重开机。reboot -w 作个重开机的模拟(只有纪录并不会真的重开机)。

四 MySQL

MySQL

4.1 说说本身对于 MySQL 常见的两种存储引擎:MyISAM与InnoDB的理解

关于两者的对比与总结:

  1. count运算上的区别:由于MyISAM缓存有表meta-data(行数等),所以在作COUNT(*)时对于一个结构很好的查询是不须要消耗多少资源的。而对于InnoDB来讲,则没有这种缓存。
  2. 是否支持事务和崩溃后的安全恢复: MyISAM 强调的是性能,每次查询具备原子性,其执行数度比InnoDB类型更快,可是不提供事务支持。可是InnoDB 提供事务支持事务,外部键等高级数据库功能。 具备事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。
  3. 是否支持外键: MyISAM不支持,而InnoDB支持。

MyISAM更适合读密集的表,而InnoDB更适合写密集的的表。 在数据库作主从分离的状况下,常常选择MyISAM做为主库的存储引擎。 通常来讲,若是须要事务支持,而且有较高的并发读取频率(MyISAM的表锁的粒度太大,因此当该表写并发量较高时,要等待的查询就会不少了),InnoDB是不错的选择。若是你的数据量很大(MyISAM支持压缩特性能够减小磁盘的空间占用),并且不须要支持事务时,MyISAM是最好的选择。

4.2 数据库索引了解吗?

Mysql索引使用的数据结构主要有BTree索引 和 哈希索引 。对于哈希索引来讲,底层的数据结构就是哈希表,所以在绝大多数需求为单条记录查询的时候,能够选择哈希索引,查询性能最快;其他大部分场景,建议选择BTree索引。

Mysql的BTree索引使用的是B数中的B+Tree,但对于主要的两种存储引擎的实现方式是不一样的。

  • MyISAM: B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,若是指定的Key存在,则取出其 data 域的值,而后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。
  • InnoDB: 其数据文件自己就是索引文件。相比MyISAM,索引文件和数据文件是分离的,其表数据文件自己就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,所以InnoDB表数据文件自己就是主索引。这被称为“聚簇索引(或汇集索引)”。而其他的索引都做为辅助索引(非汇集索引),辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不一样的地方。在根据主索引搜索时,直接找到key所在的节点便可取出数据;在根据辅助索引查找时,则须要先取出主键的值,在走一遍主索引。 所以,在设计表的时候,不建议使用过长的字段做为主键,也不建议使用非单调的字段做为主键,这样会形成主索引频繁分裂。 PS:整理自《Java工程师修炼之道》

另外,再推荐几篇比较好的关于索引的文章:

4.3 对于大表的常见优化手段说一下

当MySQL单表记录数过大时,数据库的CRUD性能会明显降低,一些常见的优化措施以下:

  1. 限定数据的范围: 务必禁止不带任何限制数据范围条件的查询语句。好比:咱们当用户在查询订单历史的时候,咱们能够控制在一个月的范围内。;

  2. 读/写分离: 经典的数据库拆分方案,主库负责写,从库负责读;

  3. 缓存: 使用MySQL的缓存,另外对重量级、更新少的数据能够考虑使用应用级别的缓存;

  4. 垂直分区:

    根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登陆信息又有用户的基本信息,能够将用户表拆分红两个单独的表,甚至放到单独的库作分库。

    简单来讲垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。 以下图所示,这样来讲你们应该就更容易理解了。

    垂直拆分的优势: 能够使得行数据变小,在查询时减小读取的Block数,减小I/O次数。此外,垂直分区能够简化表的结构,易于维护。

    垂直拆分的缺点: 主键会出现冗余,须要管理冗余列,并会引发Join操做,能够经过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;

  5. 水平分区:

    保持数据表结构不变,经过某种策略存储数据分片。这样每一片数据分散到不一样的表或者库中,达到了分布式的目的。 水平拆分能够支撑很是大的数据量。

    水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时能够把一张的表的数据拆成多张表来存放。举个例子:咱们能够将用户信息表拆分红多个用户信息表,这样就能够避免单一表数据量过大对性能形成影响。

    数据库水平拆分

    水品拆分能够支持很是大的数据量。须要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但因为表的数据仍是在同一台机器上,其实对于提高MySQL并发能力没有什么意义,因此 水品拆分最好分库

    水平拆分可以 支持很是大的数据量存储,应用端改造也少,但 分片事务难以解决 ,跨界点Join性能较差,逻辑复杂。《Java工程师修炼之道》的做者推荐 尽可能不要对数据进行分片,由于拆分会带来逻辑、部署、运维的各类复杂度 ,通常的数据表在优化得当的状况下支撑千万如下的数据量是没有太大问题的。若是实在要分片,尽可能选择客户端分片架构,这样能够减小一次和中间件的网络I/O。

    下面补充一下数据库分片的两种常见方案:

    • 客户端代理: 分片逻辑在应用端,封装在jar包中,经过修改或者封装JDBC层来实现。 当当网的 Sharding-JDBC 、阿里的TDDL是两种比较经常使用的实现。
    • 中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 咱们如今谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。

五 Redis

Redis

关于 redis 必知必会的11个问题!后两个问题,暂未更新!若有须要,能够关注个人 Github 或者微信公众号:“Java面试通关手册”获取后续更新内容。

  1. redis 简介
  2. 为何要用 redis /为何要用缓存
  3. 为何要用 redis 而不用 map/guava 作缓存?
  4. redis 和 memcached 的区别
  5. redis 常见数据结构以及使用场景分析
  6. redis 设置过时时间
  7. redis 内存淘汰机制
  8. redis 持久化机制(怎么保证 redis 挂掉以后再重启数据能够进行恢复)
  9. 缓存雪崩和缓存穿透问题解决方案
  10. 如何解决 Redis 的并发竞争 Key 问题
  11. 如何保证缓存与数据库双写时的数据一致性?

5.1 redis 简介

简单来讲 redis 就是一个数据库,不过与传统数据库不一样的是 redis 的数据是存在内存中的,因此存写速度很是快,所以 redis 被普遍应用于缓存方向。另外,redis 也常常用来作分布式锁。redis 提供了多种数据类型来支持不一样的业务场景。除此以外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。

5.2 为何要用 redis /为何要用缓存

主要从“高性能”和“高并发”这两点来看待这个问题。

高性能:

假如用户第一次访问数据库中的某些数据。这个过程会比较慢,由于是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就能够直接从缓存中获取了。操做缓存就是直接操做内存,因此速度至关快。若是数据库中的对应数据改变的以后,同步改变缓存中相应的数据便可!

高并发:

直接操做缓存可以承受的请求是远远大于直接访问数据库的,因此咱们能够考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用通过数据库。

5.3 为何要用 redis 而不用 map/guava 作缓存?

下面的内容来自 segmentfault 一位网友的提问,地址:segmentfault.com/q/101000000…

缓存分为本地缓存和分布式缓存。以java为例,使用自带的map或者guava实现的是本地缓存,最主要的特色是轻量以及快速,生命周期随着 jvm 的销毁而结束,而且在多实例的状况下,每一个实例都须要各自保存一份缓存,缓存不具备一致性。

使用 redis 或 memcached 之类的称为分布式缓存,在多实例的状况下,各实例共用一份缓存数据,缓存具备一致性。缺点是须要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。

5.4 redis 和 memcached 的区别

对于 redis 和 memcached 我总结了下面四点。如今公司通常都是用 redis 来实现缓存,并且 redis 自身也愈来愈强大了!

  1. redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不只仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
  2. Redis支持数据的持久化,能够将内存中的数据保持在磁盘中,重启的时候能够再次加载进行使用,而Memecache把数据所有存在内存之中。
  3. 集群模式:memcached没有原生的集群模式,须要依靠客户端来实现往集群中分片写入数据;可是redis目前是原生支持cluster模式的,redis官方就是支持redis cluster集群模式的,比memcached来讲要更好。
  4. Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。

来自网络上的一张图,这里分享给你们!

redis 和 memcached 的区别

5.5 redis 常见数据结构以及使用场景分析

1. String

经常使用命令: set,get,decr,incr,mget 等。

String数据结构是简单的key-value类型,value其实不只能够是String,也能够是数字。 常规key-value缓存应用; 常规计数:微博数,粉丝数等。

2.Hash

经常使用命令: hget,hset,hgetall 等。

Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操做的时候,你能够直接仅仅修改这个对象中的某个字段的值。 好比咱们能够Hash数据结构来存储用户信息,商品信息等等。好比下面我就用 hash 类型存放了我本人的一些信息:

key=JavaUser293847
value={
  “id”: 1,
  “name”: “SnailClimb”,
  “age”: 22,
  “location”: “Wuhan, Hubei”
}

复制代码

3.List

经常使用命令: lpush,rpush,lpop,rpop,lrange等

list就是链表,Redis list的应用场景很是多,也是Redis最重要的数据结构之一,好比微博的关注列表,粉丝列表,消息列表等功能均可以用Redis的 list 结构来实现。

Redis list 的实现为一个双向链表,便可以支持反向查找和遍历,更方便操做,不过带来了部分额外的内存开销。

另外能够经过 lrange 命令,就是从某个元素开始读取多少个元素,能够基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,能够作相似微博那种下拉不断分页的东西(一页一页的往下走),性能高。

4.Set

经常使用命令: sadd,spop,smembers,sunion 等

set对外提供的功能与list相似是一个列表的功能,特殊之处在于set是能够自动排重的。

当你须要存储一个列表数据,又不但愿出现重复数据时,set是一个很好的选择,而且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。能够基于 set 轻易实现交集、并集、差集的操做。

好比:在微博应用中,能够将一个用户全部的关注人存在一个集合中,将其全部粉丝存在一个集合。Redis能够很是方便的实现如共同关注、共同粉丝、共同喜爱等功能。这个过程也就是求交集的过程,具体命令以下:

sinterstore key1 key2 key3     将交集存在key1内
复制代码

5.Sorted Set

经常使用命令: zadd,zrange,zrem,zcard等

和set相比,sorted set增长了一个权重参数score,使得集合中的元素可以按score进行有序排列。

举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各类礼物排行榜,弹幕消息(能够理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。

5.6 redis 设置过时时间

Redis中有个设置时间过时的功能,即对存储在 redis 数据库中的值能够设置一个过时时间。做为一个缓存数据库,这是很是实用的。如咱们通常项目中的token或者一些登陆信息,尤为是短信验证码都是有时间限制的,按照传统的数据库处理方式,通常都是本身判断过时,这样无疑会严重影响项目性能。

咱们set key的时候,均可以给一个expire time,就是过时时间,经过过时时间咱们能够指定这个 key 能够存货的时间。

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

按期删除+惰性删除。

经过名字大概就能猜出这两个删除方式的意思了。

  • 按期删除:redis默认是每隔 100ms 就随机抽取一些设置了过时时间的key,检查其是否过时,若是过时就删除。注意这里是随机抽取的。为何要随机呢?你想想假如 redis 存了几十万个 key ,每隔100ms就遍历全部的设置过时时间的 key 的话,就会给 CPU 带来很大的负载!
  • 惰性删除 :按期删除可能会致使不少过时 key 到了时间并无被删除掉。因此就有了惰性删除。假如你的过时 key,靠按期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,也是够懒的哈!

可是仅仅经过设置过时时间仍是有问题的。咱们想一下:若是按期删除漏掉了不少过时 key,而后你也没及时去查,也就没走惰性删除,此时会怎么样?若是大量过时key堆积在内存里,致使redis内存块耗尽了。怎么解决这个问题呢?

redis 内存淘汰机制。

5.7 redis 内存淘汰机制(MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?)

redis 配置文件 redis.conf 中有相关注释,我这里就不贴了,你们能够自行查阅或者经过这个网址查看: download.redis.io/redis-stabl…

redis 提供 6种数据淘汰策略:

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

备注: 关于 redis 设置过时时间以及内存淘汰机制,我这里只是简单的总结一下,后面会专门写一篇文章来总结!

5.8 redis 持久化机制(怎么保证 redis 挂掉以后再重启数据能够进行恢复)

不少时候咱们须要持久化数据也就是将内存中的数据写入到硬盘里面,大部分缘由是为了以后重用数据(好比重启机器、机器故障以后回复数据),或者是为了防止系统故障而将数据备份到一个远程位置。

Redis不一样于Memcached的很重一点就是,Redis支持持久化,并且支持两种不一样的持久化操做。Redis的一种持久化方式叫快照(snapshotting,RDB),另外一种方式是只追加文件(append-only file,AOF).这两种方法各有千秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合本身的持久化方法。

快照(snapshotting)持久化(RDB)

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(append-only file)持久化

与快照持久化相比,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还会优雅的放慢本身的速度以便适应硬盘的最大写入速度。

补充内容:AOF 重写

AOF重写能够产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态同样,但体积更小。

AOF重写是一个有歧义的名字,该功能是经过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任伺读入、分析或者写人操做。

在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程建立新AOF文件期间,记录服务器执行的全部写命令。当子进程完成建立新AOF文件的工做以后,服务器会将重写缓冲区中的全部内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操做

更多内容能够查看个人这篇文章:

5.9 缓存雪崩和缓存穿透问题解决方案

缓存雪崩

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

解决办法(中华石杉老师在他的视频中提到过):

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

缓存穿透

简介:通常是黑客故意去请求缓存中不存在的数据,致使全部的请求都落到数据库上,形成数据库短期内承受大量请求而崩掉。

解决办法: 有不少种方法能够有效地解决缓存穿透问题,最多见的则是采用布隆过滤器,将全部可能存在的数据哈希到一个足够大的bitmap中,一个必定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(咱们采用的就是这种),若是一个查询返回的数据为空(无论是数 据不存在,仍是系统故障),咱们仍然把这个空结果进行缓存,但它的过时时间会很短,最长不超过五分钟。

参考:

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

所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操做,可是最后执行的顺序和咱们指望的顺序不一样,这样也就致使告终果的不一样!

推荐一种方案:分布式锁(zookeeper 和 redis 均可以实现分布式锁)。(若是不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)

基于zookeeper临时有序节点能够实现的分布式锁。大体思想为:每一个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个惟一的瞬时有序节点。 判断是否获取锁的方式很简单,只须要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除便可。同时,其能够避免服务宕机致使的锁没法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。

在实践中,固然是从以可靠性为主。因此首推Zookeeper。

参考:

5.11 如何保证缓存与数据库双写时的数据一致性?

你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就必定会有数据一致性的问题,那么你如何解决一致性问题?

通常来讲,就是若是你的系统不是严格要求缓存+数据库必须一致性的话,缓存能够稍微的跟数据库偶尔有不一致的状况,最好不要作这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就能够保证必定不会出现不一致的状况

串行化以后,就会致使系统的吞吐量会大幅度的下降,用比正常状况下多几倍的机器去支撑线上的一个请求。

参考:

  • Java工程师面试突击第1季(多是史上最好的Java面试突击课程)-中华石杉老师。视频地址见下面!

六 Java

6.1 Java 基础知识

重载和重写的区别

重载: 发生在同一个类中,方法名必须相同,参数类型不一样、个数不一样、顺序不一样,方法返回值和访问修饰符能够不一样,发生在编译时。   

重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;若是父类方法访问修饰符为 private 则子类就不能重写该方法。

String 和 StringBuffer、StringBuilder 的区别是什么?String 为何是不可变的?

可变性

简单的来讲:String 类中使用 final 关键字字符数组保存字符串,private final char value[],因此 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 可是没有用 final 关键字修饰,因此这两种对象都是可变的。

StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,你们能够自行查阅源码。

AbstractStringBuilder.java

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;
    int count;
    AbstractStringBuilder() {
    }
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }
复制代码

线程安全性

String 中的对象是不可变的,也就能够理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操做,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,因此是线程安全的。StringBuilder 并无对方法进行加同步锁,因此是非线程安全的。   

性能

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,而后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象自己进行操做,而不是生成新的对象并改变对象引用。相同状况下使用 StirngBuilder 相比使用 StringBuffer 仅能得到 10%~15% 左右的性能提高,但却要冒多线程不安全的风险。

对于三者使用的总结:

  1. 操做少许的数据 = String
  2. 单线程操做字符串缓冲区下操做大量数据 = StringBuilder
  3. 多线程操做字符串缓冲区下操做大量数据 = StringBuffer

自动装箱与拆箱

装箱:将基本类型用它们对应的引用类型包装起来;

拆箱:将包装类型转换为基本数据类型;

== 与 equals

== : 它的做用是判断两个对象的地址是否是相等。即,判断两个对象是否是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)

equals() : 它的做用也是判断两个对象是否相等。但它通常有两种使用状况:

  • 状况1:类没有覆盖 equals() 方法。则经过 equals() 比较该类的两个对象时,等价于经过“==”比较这两个对象。
  • 状况2:类覆盖了 equals() 方法。通常,咱们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

举个例子:

public class test1 {
    public static void main(String[] args) {
        String a = new String("ab"); // a 为一个引用
        String b = new String("ab"); // b为另外一个引用,对象的内容同样
        String aa = "ab"; // 放在常量池中
        String bb = "ab"; // 从常量池中查找
        if (aa == bb) // true
            System.out.println("aa==bb");
        if (a == b) // false,非同一对象
            System.out.println("a==b");
        if (a.equals(b)) // true
            System.out.println("aEQb");
        if (42 == 42.0) { // true
            System.out.println("true");
        }
    }
}
复制代码

说明:

  • String 中的 equals 方法是被重写过的,由于 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
  • 当建立 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要建立的值相同的对象,若是有就把它赋给当前引用。若是没有就在常量池中从新建立一个 String 对象。

关于 final 关键字的一些总结

final关键字主要用在三个地方:变量、方法、类。

  1. 对于一个final变量,若是是基本数据类型的变量,则其数值一旦在初始化以后便不能更改;若是是引用类型的变量,则在对其初始化以后便不能再让其指向另外一个对象。
  2. 当用final修饰一个类时,代表这个类不能被继承。final类中的全部成员方法都会被隐式地指定为final方法。
  3. 使用final方法的缘由有两个。第一个缘由是把方法锁定,以防任何继承类修改它的含义;第二个缘由是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。可是若是方法过于庞大,可能看不到内嵌调用带来的任何性能提高(如今的Java版本已经不须要使用final方法进行这些优化了)。类中全部的private方法都隐式地指定为fianl。

6.2 Java 集合框架

Arraylist 与 LinkedList 异同

  • 1. 是否保证线程安全: ArrayList 和 LinkedList 都是不一样步的,也就是不保证线程安全;
  • 2. 底层数据结构: Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向循环链表数据结构;
  • 3. 插入和删除是否受元素位置的影响:ArrayList 采用数组存储,因此插入和删除元素的时间复杂度受元素位置的影响。 好比:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种状况时间复杂度就是O(1)。可是若是要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。由于在进行上述操做的时候集合中第 i 和第 i 个元素以后的(n-i)个元素都要执行向后位/向前移一位的操做。 ② LinkedList 采用链表存储,因此插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。
  • 4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而ArrayList 实现了RandmoAccess 接口,因此有随机访问功能。快速随机访问就是经过元素的序号快速获取元素对象(对应于get(int index)方法)。
  • 5. 内存空间占用: ArrayList的空 间浪费主要体如今在list列表的结尾会预留必定的容量空间,而LinkedList的空间花费则体如今它的每个元素都须要消耗比ArrayList更多的空间(由于要存放直接后继和直接前驱以及数据)。

补充:数据结构基础之双向链表

双向链表也叫双链表,是链表的一种,它的每一个数据结点中都有两个指针,分别指向直接后继和直接前驱。因此,从双向链表中的任意一个结点开始,均可以很方便地访问它的前驱结点和后继结点。通常咱们都构造双向循环链表,以下图所示,同时下图也是LinkedList 底层使用的是双向循环链表数据结构。

ArrayList 与 Vector 区别

Vector类的全部方法都是同步的。能够由两个线程安全地访问一个Vector对象、可是一个线程访问Vector的话代码要在同步操做上耗费大量的时间。

Arraylist不是同步的,因此在不须要保证线程安全时时建议使用Arraylist。

HashMap的底层实现

①JDK1.8以前

JDK1.8 以前 HashMap 底层是 数组和链表 结合在一块儿使用也就是 链表散列HashMap 经过 key 的 hashCode 通过扰动函数处理事后获得 hash 值,而后经过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的时数组的长度),若是当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,若是相同的话,直接覆盖,不相同就经过拉链法解决冲突。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数以后能够减小碰撞。

JDK 1.8 HashMap 的 hash 方法源码:

JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,可是原理不变。

static final int hash(Object key) {
      int h;
      // key.hashCode():返回散列值也就是hashcode
      // ^ :按位异或
      // >>>:无符号右移,忽略符号位,空位都以0补齐
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }
复制代码

对比一下 JDK1.7的 HashMap 的 hash 方法源码.

static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).

    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
复制代码

相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,由于毕竟扰动了 4 次。

所谓 “拉链法” 就是:将链表和数组相结合。也就是说建立一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中便可。

jdk1.8以前的内部结构

②JDK1.8以后

相比于以前的版本, JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减小搜索时间。

JDK1.8以后的HashMap底层数据结构

TreeMap、TreeSet以及JDK1.8以后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,由于二叉查找树在某些状况下会退化成一个线性结构。

推荐阅读:

HashMap 和 Hashtable 的区别

  1. 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都通过 synchronized 修饰。(若是你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
  2. 效率: 由于线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
  3. 对Null key 和Null value的支持: HashMap 中,null 能够做为键,这样的键只有一个,能够有一个或多个键所对应的值为 null。。可是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
  4. 初始容量大小和每次扩充容量大小的不一样 : ①建立时若是不指定容量初始值,Hashtable 默认的初始大小为11,以后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。以后每次扩充,容量变为原来的2倍。②建立时若是给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 老是使用2的幂做为哈希表的大小,后面会介绍到为何是2的幂次方。
  5. 底层数据结构: JDK1.8 之后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减小搜索时间。Hashtable 没有这样的机制。

HashMap 的长度为何是2的幂次方

为了能让 HashMap 存取高效,尽可能较少碰撞,也就是要尽可能把数据分配均匀。咱们上面也讲到了过了,Hash 值的范围值-2147483648到2147483648,先后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,通常应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。因此这个散列值是不能直接拿来用的。用以前还要先作对数组的长度取模运算,获得的余数才能用来要存放的位置也就是对应的数组下标。

这个算法应该如何设计呢?

咱们首先可能会想到采用%取余的操做来实现。可是,重点来了:“取余(%)操做中若是除数是2的幂次则等价于与其除数减一的与(&)操做(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 而且 采用二进制位操做 &,相对于%可以提升运算效率,这就解释了 HashMap 的长度为何是2的幂次方。

HashMap 多线程操做致使死循环问题

在多线程下,进行 put 操做会致使 HashMap 死循环,缘由在于 HashMap 的扩容 resize()方法。因为扩容是新建一个数组,复制原数据到数组。因为数组下标挂有链表,因此须要复制链表,可是多线程操做有可能致使环形链表。复制链表过程以下:
如下模拟2个线程同时扩容。假设,当前 HashMap 的空间为2(临界值为1),hashcode 分别为 0 和 1,在散列地址 0 处有元素 A 和 B,这时候要添加元素 C,C 通过 hash 运算,获得散列地址为 1,这时候因为超过了临界值,空间不够,须要调用 resize 方法进行扩容,那么在多线程条件下,会出现条件竞争,模拟过程以下:

线程一:读取到当前的 HashMap 状况,在准备扩容时,线程二介入

线程二:读取 HashMap,进行扩容

线程一:继续执行

这个过程为,先将 A 复制到新的 hash 表中,而后接着复制 B 到链头(A 的前边:B.next=A),原本 B.next=null,到此也就结束了(跟线程二同样的过程),可是,因为线程二扩容的缘由,将 B.next=A,因此,这里继续复制A,让 A.next=B,由此,环形链表出现:B.next=A; A.next=B

HashSet 和 HashMap 区别

若是你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码很是很是少,由于除了 clone() 方法、writeObject()方法、readObject()方法是 HashSet 本身不得不实现以外,其余方法都是直接调用 HashMap 中的方法。)

HashSet 和 HashMap 区别

ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMap 和 Hashtable 的区别主要体如今实现线程安全的方式上不一样。

  • 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构同样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 以前的 HashMap 的底层数据结构相似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
  • 实现线程安全的方式(重要):在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不一样数据段的数据,就不会存在锁竞争,提升并发访问率。(默认分配16个Segment,比Hashtable效率提升16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操做。(JDK1.6之后 对 synchronized锁作了不少优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,可是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率很是低下。当一个线程访问同步方法时,其余线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另外一个线程不能使用 put 添加元素,也不能使用 get,竞争会愈来愈激烈效率越低。

二者的对比图:

图片来源:www.cnblogs.com/chengxiao/p…

HashTable:

JDK1.7的ConcurrentHashMap:

JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):

ConcurrentHashMap线程安全的具体实现方式/底层具体实现

①JDK1.7(上面有示意图)

首先将数据分为一段一段的存储,而后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其余段的数据也能被其余线程访问。

ConcurrentHashMap 是由 Segment 数组结构和 HahEntry 数组结构组成

Segment 实现了 ReentrantLock,因此 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。

static class Segment<K,V> extends ReentrantLock implements Serializable {
}
复制代码

一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap相似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每一个 HashEntry 是一个链表结构的元素,每一个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先得到对应的 Segment的锁。

②JDK1.8 (上面有示意图)

ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构相似,数组+链表/红黑二叉树。

synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提高N倍。

集合框架底层数据结构总结

Collection

1.List

  • Arraylist: Object数组

  • Vector: Object数组

  • LinkedList: 双向循环链表 2.Set

  • HashSet(无序,惟一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素

  • LinkedHashSet: LinkedHashSet 继承与 HashSet,而且其内部是经过 LinkedHashMap 来实现的。有点相似于咱们以前说的LinkedHashMap 其内部是基于 Hashmap 实现同样,不过仍是有一点点区别的。

  • TreeSet(有序,惟一): 红黑树(自平衡的排序二叉树。)

Map

  • HashMap: JDK1.8以前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减小搜索时间
  • LinkedHashMap: LinkedHashMap 继承自 HashMap,因此它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增长了一条双向链表,使得上面的结构能够保持键值对的插入顺序。同时经过对链表进行相应的操做,实现了访问顺序相关逻辑。详细能够查看:《LinkedHashMap 源码详细分析(JDK1.8)》
  • HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
  • TreeMap: 红黑树(自平衡的排序二叉树)

6.3 Java多线程

关于 Java多线程,在面试的时候,问的比较多的就是①悲观锁和乐观锁( 具体能够看个人这篇文章:面试必备之乐观锁与悲观锁)、②synchronized和lock区别以及volatile和synchronized的区别③可重入锁与非可重入锁的区别④多线程是解决什么问题的⑤线程池解决什么问题⑥线程池的原理⑦线程池使用时的注意事项⑧AQS原理、⑨ReentranLock源码,设计原理,总体过程 等等问题。

  面试官在多线程这一部分极可能会问你有没有在项目中实际使用多线程的经历。因此,若是你在你的项目中有实际使用Java多线程的经历 的话,会为你加分很多哦!

6.4 Java虚拟机

  关于Java虚拟机,在面试的时候通常会问的大多就是①Java内存区域②虚拟机垃圾算法③虚拟机垃圾收集器④JVM内存管理⑤JVM调优这些问题了。    具体能够查看个人这两篇文章:

6.5 设计模式

设计模式比较常见的就是让你手写一个单例模式(注意单例模式的几种不一样的实现方法)或者让你说一下某个常见的设计模式在你的项目中是如何使用的,另外面试官还有可能问你抽象工厂和工厂方法模式的区别工厂模式的思想这样的问题。

建议把代理模式观察者模式(抽象)工厂模式好好看一下,这三个设计模式也很重要。

七 数据结构

  数据结构比较常问的就是:二叉树红黑树(极可能让你手绘一个红黑树出来哦!)、二叉查找树(BST)平衡二叉树(Self-balancing binary search tree)B-树,B+树与B*树的优缺点比较LSM 树这些知识点。

  数据结构很重要,并且学起来也相对要难一些。建议学习数据结构必定要按部就班的来,一步一个脚印的走好。必定要搞懂原理,最好本身能用代码实现一遍。

八 算法

  常见的加密算法、排序算法都须要本身提早了解一下,排序算法最好本身可以独立手写出来。

  我以为面试中最刺激、最有压力或者说最有挑战的一个环节就是手撕算法了。面试中大部分算法题目都是来自于Leetcode、剑指offer上面,建议你们能够天天挤出一点时间刷一下算法题。

推荐两个刷题必备网站:

LeetCode:

牛客网:

8.1 举个栗子(手写快排)

面试官可能会问你,了解哪些排序方法啊?除了冒泡排序和选择排序能不能给我手写一个其余的排序算法。或者面试官可能会直接问你:“能不能给我手写一个快排出来?”。

快排的基本思想: 经过选择的参考值将待排序记录分割成独立的两部分,一部分全小于选取的参考值,另外一部分全大于选取的参考值。对分割以后的部分再进行一样的操做直到没法再进行该操做位置(能够使用递归)。

下面是我写的一个简单的快排算法,我选择的参考值是数组的第一个元素。

import java.util.Arrays;

public class QuickSort {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		int[] num = { 1, 3, 4, 8, 5, 10, 22, 15, 16 };
		QuickSort.quickSort(num, 0, num.length - 1);
		System.out.println(Arrays.toString(num));
	}

	public static void quickSort(int[] a, int start, int end) {
		// 该值定义了从哪一个位置开始分割数组
		int ref;
		if (start < end) {
			// 调用partition方法对数组进行排序
			ref = partition(a, start, end);
			// 对分割以后的两个数组继续进行排序
			quickSort(a, start, ref - 1);
			quickSort(a, ref + 1, end);
		}
	}

	/** * 选定参考值对给定数组进行一趟快速排序 * * @param a * 数组 * @param start * (切分)每一个数组的第一个的元素的位置 * @param end * (切分)每一个数组的最后一个的元素位置 * @return 下一次要切割数组的位置 */
	public static int partition(int[] a, int start, int end) {
		// 取数组的第一个值做为参考值(关键数据)
		int refvalue = a[start];
		// 从数组的右边开始往左遍历,直到找到小于参考值的元素
		while (start < end) {
			while (end > start && a[end] >= refvalue) {
				end--;
			}
			// 将元素直接赋予给左边第一个元素,即pivotkey所在的位置
			a[start] = a[end];

			// 从序列的左边边开始往右遍历,直到找到大于基准值的元素
			while (end > start && a[start] <= refvalue) {
				start++;
			}
			a[end] = a[start];
			return end;
		}
		// 最后的start是基准值所在的位置
		a[start] = refvalue;
		return start;
	}

}

复制代码

时间复杂度分析:

  • 在最优的状况下,Partition每次都划分得很均匀,快速排序算法的时间复杂度为O(nlogn)。
  • 最糟糕状况下的快排,当待排序的序列为正序或逆序排列时,时间复杂度为O(n^2)。

空间复杂度分析:

  • 最好状况,递归树的深度为log2n,其空间复杂度也就为O(logn)
  • 最坏状况,须要进行n‐1递归调用,其空间复杂度为O(n),平均状况,空间复杂度也为O(logn)。

一种简单优化的方式:

三向切分快速排序 :核心思想就是将待排序的数据分为三部分,左边都小于比较值,右边都大于比较值,中间的数和比较值相等.三向切分快速排序的特性就是遇到和比较值相同时,不进行数据交换, 这样对于有大量重复数据的排序时,三向切分快速排序算法就会优于普通快速排序算法,但因为它总体判断代码比普通快速排序多一点,因此对于常见的大量非重复数据,它并不能比普通快速排序多大多的优点 。

九 Spring

  Spring通常是不可避免的,若是你的简历上注明了你会Spring Boot或者Spring Cloud的话,那么面试官也可能会同时问你这两个技术,好比他可能会问你springboot和spring的区别。 因此,必定要谨慎对待写在简历上的东西,必定要对简历上的东西很是熟悉。

  另外,AOP实现原理动态代理静态代理Spring IOC的初始化过程IOC原理本身怎么实现一个IOC容器? 这些东西都是常常会被问到的。

9.1 Spring Bean 的做用域

9.2 Spring 事务中的隔离级别

TransactionDefinition 接口中定义了五个表示隔离级别的常量:

  • 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的隔离级别。全部的事务依次逐个执行,这样事务之间就彻底不可能产生干扰,也就是说,该级别能够防止脏读、不可重复读以及幻读。可是这将严重影响程序的性能。一般状况下也不会用到该级别。

9.3 Spring 事务中的事务传播行为

支持当前事务的状况:

  • TransactionDefinition.PROPAGATION_REQUIRED: 若是当前存在事务,则加入该事务;若是当前没有事务,则建立一个新的事务。
  • TransactionDefinition.PROPAGATION_SUPPORTS: 若是当前存在事务,则加入该事务;若是当前没有事务,则以非事务的方式继续运行。
  • TransactionDefinition.PROPAGATION_MANDATORY: 若是当前存在事务,则加入该事务;若是当前没有事务,则抛出异常。(mandatory:强制性)

不支持当前事务的状况:

  • TransactionDefinition.PROPAGATION_REQUIRES_NEW: 建立一个新的事务,若是当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,若是当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,若是当前存在事务,则抛出异常。

其余状况:

  • TransactionDefinition.PROPAGATION_NESTED: 若是当前存在事务,则建立一个事务做为当前事务的嵌套事务来运行;若是当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

9.4 AOP

AOP思想的实现通常都是基于 代理模式 ,在JAVA中通常采用JDK动态代理模式,可是咱们都知道,JDK动态代理模式只能代理接口而不能代理类。所以,Spring AOP 会这样子来进行切换,由于Spring AOP 同时支持 CGLIB、ASPECTJ、JDK动态代理。

  • 若是目标对象的实现类实现了接口,Spring AOP 将会采用 JDK 动态代理来生成 AOP 代理类;
  • 若是目标对象的实现类没有实现接口,Spring AOP 将会采用 CGLIB 来生成 AOP 代理类——不过这个选择过程对开发者彻底透明、开发者也无需关心。

这部份内容能够查看下面这几篇文章:

9.5 IOC

Spring IOC的初始化过程:

Spring IOC的初始化过程

IOC源码阅读

十 实际场景题

  我以为实际场景题就是对你的知识运用能力以及思惟能力的考察。建议你们在平时养成多思考问题的习惯,这样面试的时候碰到这样的问题就不至于慌了。另外,若是本身实在不会就给面试官委婉的说一下,面试官可能会给你提醒一下。切忌不懂装懂,乱答一气。      面试官可能会问你相似这样的问题:①假设你要作一个银行app,有可能碰到多我的同时向一个帐户打钱的状况,有可能碰到什么问题,如何解决(锁)②你是怎么保证你的代码质量和正确性的?③下单过程当中是下订单减库存仍是付款减库存,分析一下二者的优劣;④同时给10万我的发工资,怎么样设计并发方案,能确保在1分钟内所有发完。⑤若是让你设计xxx系统的话,你会如何设计。   

写在最后

最后,再强调几点:

  1. 必定要谨慎对待写在简历上的东西,必定要对简历上的东西很是熟悉。由于通常状况下,面试官都是会根据你的简从来问的;
  2. 能有一个上得了台面的项目也很是重要,这极可能是面试官会大量发问的地方,因此在面试以前好好回顾一下本身所作的项目;
  3. 和面试官聊基础知识好比设计模式的使用、多线程的使用等等,能够结合具体的项目场景或者是本身在平时是如何使用的;
  4. 注意本身开源的Github项目,面试官可能会挖你的Github项目提问;
  5. 建议提早了解一下本身想要面试的公司的价值观,判断一下本身到底是否适合这个公司。

另外,我我的以为面试也像是一场全新的征程,失败和胜利都是日常之事。因此,劝各位不要由于面试失败而灰心、丧失斗志。也不要由于面试经过而沾沾自喜,等待你的将是更美好的将来,继续加油!

初次以外,笔主也在这里给本身挖一个坑,关于 dubbo、zookeeper 等内容我会在后续作一个系统总结。保证你们看了以后,必定有收获!

最后,附上征文连接

还有三个本次活动的合做伙伴:

相关文章
相关标签/搜索