常见面试题总结

1. 苍穹外卖的模块

苍穹外卖大方向上主要分为管理端和用户端

管理端使用vue开发,主要是商家来使用,提供餐品的管理功能,主要有下面几个模块:

员工模块,提供员工账号的登录功能和管理功能
分类、菜品、套餐模块,分别对分类、菜品和套餐进行增删改查和启用禁用
订单模块,可以搜索和查看订单,变更订单状态
统计模块,统计营业额、用户、订单和销量排名,还有Excel报表导出功能
工作台模块,提供今日运营数据数据以及订单、菜品、套餐的总览
用户端使用微信小程序开发,主要是给用户提供点餐功能

登录模块,调用微信小程序登录接口实现登录功能

菜品、套餐模块,用于查询菜品、套餐的信息

购物车模块,在购物车中添加或删除套餐菜品

订单模块,提供下单、微信支付、查询订单、取消订单、再来一单、催单功能。

我完成了一个外卖项目,名叫苍穹外卖,是跟着黑马程序员的课程来自己动手写的。

项目基本实现了外卖客户端、商家端的后端完整业务。

商家端分为员工管理、文件上传、菜品管理、分类管理、套餐管理、店铺营业状态、订单下单派送等的管理、数据统计等,用时7天半;用户端用微信小程序实现的,分为用户登陆退出、套餐菜品浏览、购物车、订单管理、地址薄管理等,用时4天半。

项目使用了Nginx、Swagger、Redis、MySQL、SpringBoot、AOP、HTTPClient、SpringCache、SpringTask、WebSocket、POI等技术。

项目使用了流行的微信小程序实现用户端,保证了技术赶上应用的潮流。

项目难点在于各种第三方调用的实现,比如调用微信进行登录、付款,调用百度地图进行位置查询等,需要自信了解第三方接口的细节,需要有基本的编程语言理解与代码学习能力。解决方案就是自己学习、搜索学习和向其他人、高手学习。

经过了这个项目,我的代码能力提升了,对Spring三层架构的理解又深了一步,离当上一个合格的程序员又近了一步。

业务逻辑分析,请问面试官有感兴趣的地方吗。

假设的业务进行分析,我尽力而为。

2. 管理端账号密码的登录流程

登录的本质就是对员工表进行查询操作

首先要接收前端发送的账号和密码
然后根据账号在数据库员工表进行查询,如果没有查到,说明此账号不存在,可以直接给前端返回一个提示,如果查询到了说明这个账号存在
接下来就要进行前端传入密码和数据库查询到的密码比对,由于数据库的密码是加密保存的,所以在比对之前,还需要对前端传入的秘密进行加密;如果比对失败,说明用户提供的密码不对,可以直接给前端返回一个提示;如果比对没有问题,说明用户提供的账户和密码是正确的,此时登录成功
登录成功之后,需要生成一个标识用户身份的token,返回给前端,前端会将token保存起来
用户后面访问系统的时候,需要携带着这个token,而我们后端需要编写一个拦截器,用于拦截请求,校验token
校验通过,则放行请求,正常访问;校验失败,则禁止通行,返回提示

为什么需要JWT?


答:HTTP协议是无状态的,也就是说,如果我们已经认证了一个用户,那么他下一次请求的时候,服务器不知道我是谁,我们必须再次认证

JWT和Session有什么区别?


答:相同点是,它们都是存储用户信息;然而,Session是在服务器端的,而JWT是在客户端的。Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。

JWT是如何工作的?


用户携带用户名和密码请求访问登录接口,如果没有任何问题,我使用用户名和密码生成token。然后每次浏览器访问网站的请求,都会带上token。

注意:每一次请求中的token都会放在请求头中。

JWT使用步骤?

引入依赖

1. 自定义一个JWT工具类  

里面有两个方法,一个是创建token,一个是解析token

用户登录先判断,然后返回一个token

拦截器

2. 在登录接口处,创建token并返回


3. 前端小程序的微信登录流程


微信登录的核心是通过微信小程序提供的临时凭证code换取永久凭证openid的过程

首先微信小程序会向微信官方申请一个临时登录code
然后,小程序带着code向后台服务发送请求
后台接收到code后,会调用微信官方接口验证code是否合法,如果合法,官方会返回一个openid;这个openid就是此用户在我们系统中的唯一标识,同时也代表用户身份合法
后台服务接收到来着微信的openid之后,会去数据库查询一下是否存在此账户;如果存在,代表这是一个老用户,如果不存在,则代表这是一个新用户首次使用我们的系统,我们需要将其信息保存到用户表中
登录成功之后,需要生成一个标识用户身份的token,返回给前端,前端会将token保存起来
用户后面访问系统的时候,需要携带着这个token,而我们后端需要编写一个拦截器,用于拦截请求,校验token
校验通过,则放行请求,正常访问;校验失败,则禁止通行,返回提示


4. ThreadLocal 在项目中的应用


ThreadLocal 称为线程局部变量,可以为每个线程单独提供一份存储空间,它的特点是:线程之内,数据共享;线程之间,数据隔离。

在我们的项目中经常使用ThreadLocal来存储用户的登录信息,具体的做法是:

每次用户访问后台都需要经过拦截器鉴权,如果鉴权通过,我们就将登录用户的信息保存到ThreadLocal中

接下来,在项目的各个代码中就可以轻松的从ThreadLocal中获取用户信息了

最后,当请求访问完服务离开的时候,还会再次经过拦截器,这个时候就可以清理掉ThreadLocal中的内容了

5. 项目是如何进行异常处理的


在我们的项目中,异常处理都是通过spring的全局异常处理器来实现的,核心是两个注解:

一个是@RestControllerAdvice,标注在类上,可以定义全局异常处理类对异常进行拦截
一个是@ExceptionHandler,标注在异常处理类中的方法上,可以声明每个方法能够处理的异常类型
在我们的项目中,将异常分为了三大类:

在苍穹外卖项目的全局异常处理器中一般定义三种异常:

第一类是指定异常,指定异常指的是用户操作产生的与程序设计相关的异常,比如说字段重复异常、Validation校验异常等等,这类异常捕获之后,我们会根据异常的消息提示,给前端一个确定的返回结果
第二类是业务异常处理,业务异常是由于用户不正当操作产生的与业务相关的的异常,这种异常往往需要我们自定义,然后在程序的相关位置手动抛出,在抛出的时候还会指定异常提示信息。然后异常处理器捕获之后,直接将异常提示消息返回给前端
第三种异常时兜底异常,此处主要捕获的是不属于上面两种异常的异常,一般是一些程序员代码不够严谨引发的运行时异常,对于这些异常,我们处理方案是首先要把错误记录到日志系统中,然后给前端一个类似于服务器开小差了之类的统一提示


6. 项目是如何存储文件的


在我使用过的文件存储中,主要有三类存储方式

直接将文件保存到服务到硬盘,这种方式操作方便,但是扩容困难,而且安全保障不高,现在基本不再使用
使用一些付费的第三方存储服务,比如阿里云或者七牛云,这种方式无需自己公司提供服务器和相关软件,并且安全性和扩展性都不需要自己再进行考虑,但是不适合存储一些敏感文件
将文件保存在公司自己搭建的一些分布式系统中,比如我们公司用过MinIO和FastDFS,它需要我们自己准备服务器,安装维护软件,好处是文件都存放在自己的服务器上,隐私性比较好
至于如何选择服务器,我认为目前主要考虑的就是分布式文件存储系统和第三方服务

如果文件是隐私性比较高,建议使用自己搭建的分布式文件存储系统
如果文件隐私性不高,可以考虑使用第三方服务
我们项目中主要存储的文件是一些菜品或者套餐的图片,不涉及什么隐私问题,所以选择了阿里云服务

7. 项目是如何进行参数校验的


我们项目中的校验是使用validation技术实现的,它的核心是一些具有特定意义的注解

这些注解主要标注在请求参数或者是参数对象对应类的属性上,每个注解都有自己的校验规则。

如果我们输入的请求参数不符合对应的校验规则,系统就会抛出异常,此时我们只需要在全局异常处理器中捕获异常,然后给前端提示即可。

我们常用的注解有下面这些:

@Null 可以标注在任意类型元素上,被标注的元素必须为null

@NotEmpty 可以标注 在字符串,集合,数组,map上,被标注的元素必须不能为 null ,也不能是空串

@Range 标注在字符串和数值的大小必须在指定的范围内,对于null无效

@Digits(integer(数值的位数) =3 , fraction(小数的位数)=2)

@size(min=,max=) 可以标注在字符串,数组,集合,map用于控制长度

@Email 邮箱

@URL 合法的地址

8. 如何理解分组校验


很多情况下,我们会将校验规则写到实体类中的属性上,而这个实体类有可能作为不同功能方法的参数使用,而不同的功能对象参数对象中属性的要求是不一样的。比如我们在新增和修改一个用户对象时,都会接收User对象作为请求参数,但是新增要求对象的id为空,而修改则要求id字段不能为空。这个时候就需要使用到分组校验了
分组校验其实就是定义多套校验规则,对于指定的功能,我们按照要求指定它使用哪套规则即可

9. Redis中有哪些数据类型


Redis是一个基于内存的键值对数据库,它的键都是字符串类型,而值的部分支持5种数据类型,每种类型特点不一样

string:字符串类型,可以存储普通字符串、JSON字符串,也可以存储对象系列化之后的字符串
hash:哈希类型,类似于Java中的HashMap,比较适合存储对象
list:列表类型,底层是一个顺序链表,可以从两端添加或移除元素,元素是有序的,可重复的
set:无序集合,没有重复元素
zset:有序集合,没有重复元素,且集合中每个元素关联一个分数,可以根据分数进行排序


10. Redis在项目中的应用


我们项目中有两处地方用到了Redis,分别是:店铺营业状态标识和小程序端的套餐、菜品列表数据

店铺营业状态标识,仅仅需要在redis中保存一个0|1值即可。这里之所以选择redis,有两个原因

而没有采用数据库来存储,就是因为这个字段太简单了,没有必要在数据库中新建一张表

这个状态访问比较频繁,放在redis中,提高了查询速度的同时,可以减轻数据库的访问压力

小程序端的套餐、菜品列表数据,由于小程序端以后的访问量比较大,所以采用Redis提高访问速度

具体的操作步骤就是:在查询列表的时候,先判断Redis缓存中是否有数据,如果有,直接返回给前端

如果没有,再去查询数据库,并将查询结果保存到redis中的同时,再返回给前端

为了保证Redis和数据库中数据的实时一致性,在对数据库相关数据进行增删改操作时,需要同时清理Redis中数据

Redis常用命令

3.1 字符串操作命令

Redis 中字符串类型常用命令:

SET key value 设置指定key的值

GET key 获取指定key的值

SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒

SETNX key value 只有在 key 不存在时设置 key 的值

3.2 哈希操作命令


Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象,常用命令:

HSET key field value 将哈希表 key 中的字段 field 的值设为 value

HGET key field 获取存储在哈希表中指定字段的值

HDEL key field 删除存储在哈希表中的指定字段

HKEYS key 获取哈希表中所有字段

HVALS key 获取哈希表中所有值

3.3 列表操作命令


Redis 列表是简单的字符串列表,按照插入顺序排序,常用命令:

LPUSH key value1 [value2] 将一个或多个值插入到列表头部

LRANGE key start stop 获取列表指定范围内的元素

RPOP key 移除并获取列表最后一个元素

LLEN key 获取列表长度

BRPOP key1 [key2 ] timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超 时或发现可弹出元素为止

3.4 集合操作命令


Redis set 是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据,常用命令:

SADD key member1 [member2] 向集合添加一个或多个成员

SMEMBERS key 返回集合中的所有成员

SCARD key 获取集合的成员数

SINTER key1 [key2] 返回给定所有集合的交集

SUNION key1 [key2] 返回所有给定集合的并集

SREM key member1 [member2] 移除集合中一个或多个成员

3.5 有序集合操作命令


Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。常用命令:

常用命令:

ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员

ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员

ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment

ZREM key member [member ...] 移除有序集合中的一个或多个成员

3.6 通用命令


Redis的通用命令是不分数据类型的,都可以使用的命令:

KEYS pattern 查找所有符合给定模式( pattern)的 key

EXISTS key 检查给定 key 是否存在

TYPE key 返回 key 所储存的值的类型

DEL key 该命令用于在 key 存在是删除 key

11. SpringCache在项目中的应用


SpringCache是Spring提供的一个缓存框架,它可以通过简单的注解实现缓存的操作,我们常用的注解有下面几个:

@EnableCaching: 开启基于注解的缓存

@CachePut: 一般用在查询方法上,表示将方法的返回值放到缓存中

@Cacheable: 一般用在查询方法上,表示在方法执行前先查看缓存中是否有数据,如果有直接返回;如果没有,再调用方法体查询数据并将返回结果放到缓存中;他有两个关键属性:

value: 缓存的名称,每个缓存名称下面可以有多个key

key: 缓存的key,支持Spring的表达式语言SPEL语法

@CacheEvict: 一般用在增删改方法上 ,用于清理指定缓存,可以根据key清理,也可以清理整个value下的缓存

SpringCache还有一个有点,就是可以随意切换底层的缓存软件,比如:Redis、内存等等

本项目中菜品和套餐列表的缓存用到了SpringCache

12. 微信支付流程


整个微信支付流程涉及到三个角色:微信小程序、服务端、微信平台

首先,由小程序发起下单请求到服务端,服务端生成订单保存到数据库后,将订单号返给前端
然后,小程序会向服务端发起支付请求,这个请求中会携带着订单号
服务端根据订单号查询到订单信息后,开始调用微信下单接口从微信平台获取预支付交易标识
服务端需要将预支付交易标识进行签名之后组装成支付参数,回传给小程序,小程序就会弹出支付窗口
用户通过小程序向微信平台付款,并可以获取到支付结果,进行显示
微信平台还会将订单支付结果推送给我们的后台程序,后台程序需要修改订单状态

HTTPClient的作用

  • 发送HTTP请求

  • 接收响应数据


13. SpringTask在项目中的应用


SpringTask是Spring框架提供的一种任务调度工具,用来按照定义的时间格式执行某段代码。

它的一个关键注解是@Scheduled,此注解标注在方法上,用于设置方法的调用时机,它支持下面一些属性:

fixedDelay:上一次任务完成后多久执行下一次任务
fixedRate:上一次任务开始后多久执行下一次任务(注意:任务不能有重叠)
initialDelay:第一次任务延迟多久执行
cron:通过cron表达式控制任务执行时间
在我们的项目中,超时订单的状态改变用到了SpringTask,比如:

每隔1分钟检查是否有超过15分钟未支付的订单,如果有就将订单取消
每天凌晨1点检查前一天是否有派送中的订单,如果有将订单状态改成已完成

spring task 处理定时任务
Spring Task(Spring 任务调度)是 Spring 框架提供的一种任务调度框架,用于执行定时任务、异步任务、任务监听、任务调度等。

在苍穹外卖项目中使用 Spring task

通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”
通过定时任务每天凌晨1点检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成”


14. cron表达式


cron表达式其实就是一个字符串,通过cron表达式可以定义任务的触发时间

SpringTask支持的cron表达式分为6个域,由空格分隔开,每个域代表一个含义:秒 分 时 日 月 周

每个域都支持精准数值的写法,也支持一些具有特殊意义的字符,主要的有下面这些:

*:表示任意

?:表示忽略,只能用在日和周两个域

-:表示区间,

/:表示起始时间开始触发,然后每隔固定时间触发一次

,:表示列出枚举值,例如在分域使用5,20则意味着在5和20分触发一次

#: 用于确定每个月第几个星期几

15. WebSocket对比HTTP


HTTP的通信是单向的,要先请求后响应,类似于对讲机

WebSocket的通信双向的、实时的,客户端和服务端可以同时发消息,类似于手机通话

我们在项目中大部分场景下都是使用HTTP协议,只有在高实时场景下,建议使用WebSocket

项目在向商家提醒接单时,用户催单发送提醒时使用了webSocket

使用 Websocket 来实现用户端和商家端通信:


WebSocket 是一种在 Web 应用程序中实现双向通信的协议。它允许客户端和服务器之间建立持久的、双向的通信通道,使得服务器可以主动向客户端推送消息,而无需客户端发送请求。
客户端和服务器之间可以实时地发送消息和接收消息,不需要频繁地发起请求。这样可以减少网络流量和延迟,并提供更好的用户体验。

通过WebSocket实现管理端页面和服务端保持长连接状态
用户下单并且支付成功后,需要第一时间通知外卖商家。
来单提醒

当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息
客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
type 为消息类型,1为来单提醒 2为客户催单
orderId 为订单id
content 为消息内容
用户在小程序中点击催单按钮后,需要第一时间通知外卖商家。
*客户催单

通过WebSocket实现管理端页面和服务端保持长连接状态
当用户点击催单按钮后,调用WebSocket的相关API实现服务端向客户端推送消息
客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
type 为消息类型,1为来单提醒 2为客户催单
orderId 为订单id
content 为消息内容

nginx反向代理和负载均衡


反向代理
就是将前端发送的动态请求由 nginx 转发到后端服务器

nginx 反向代理的好处:

提高访问速度

因为nginx本身可以进行缓存,如果访问的同一接口,并且做了数据缓存,nginx就直接可把数据返回,不需要真正地访问服务端,从而提高访问速度。

进行负载均衡

所谓负载均衡,就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器。

保证后端服务安全

因为一般后台服务地址不会暴露,所以使用浏览器不能直接访问,可以把nginx作为请求访问的入口,请求到达nginx后转发到具体的服务中,从而保证后端服务的安全。

16. Excel有哪些技术方案


Excel技术方案有:ApachePOI和EasyExcel等,EasyExcel是在POI的基础上进行二次开发的

POI的封装度较低,使用时需要写大量的代码,并且性能也比较低,同时它是对文档一次性导入,容易导致内存溢出

EasyExcel的封装度比较高,使用起来比较方便,而且它是一条条导入数据,不会导致内存溢出

在实际开发中,更倾向于使用稳定性更好的方案,所以一般选择EasyExcel作为Excel的导入导出技术方案

17. 项目参与人员角色


项目经理(1人):对整个项目负责,任务分配、把控进度

产品经理(1人):进行需求调研,输出需求调研文档、产品原型等

UI设计师(1人):根据产品原型输出界面效果图

架构师(0人):项目整体架构设计、技术选型等

开发工程师(3+2):功能代码实现

测试工程师(2人):编写测试用例,输出测试报告

运维工程师(1人):软件环境搭建、项目上线

18. git在工作中的使用流程


我在工作中对于git的使用,可分为以下几个步骤:

首先,每天上班之后,我会从公司远程仓库中进行拉取(pull),以保证本地项目和远程仓库项目进度一致

然后,在本地的开发分支上新建一个当天的分支,进行代码开发

开发过程中一般在完成某功能或某一模块时,进行本地提交(commit)

当我们完成一个完整模块的开发后,会将本地新分支的代码合并到开发分支

最后,提交(push)到远程仓库前,先进行拉取(pull),如果有冲突,就先进行冲突解决,解决完毕之后,再push

19. 介绍一下苍穹外卖项目


本项目是专门为一家餐厅定制的一款软件产品,主要包含包括 系统管理端和 小程序端 两部分

系统管理端提供给餐饮企业内部员工使用,可以对餐厅的分类、菜品、套餐、订单、员工等进行管理维护

小程序端提供给消费者使用,可以在线浏览菜品、添加购物车、下单、支付、催单等操作

我在这个项目中主要负责后端分类、套餐、菜品模块和小程序端的所有功能

20. 苍穹外卖核心功能


菜品新增:对菜品表和口味表进行新增操作

首先将前端传过来的菜品信息保存到菜品表并主键返回,然后遍历前端传过来的口味集合,
为每个口味设置刚才返回来的主键并保存到口味表
菜品修改:对菜品表进行更新,对菜品详情表进行增删操作

首先根据前端传过来的菜品信息对菜品表进行修改
然后根据菜品id删除对应的口味列表集合
最后再把前端传过来的口味集合重新加入到口味表中
菜品删除:对菜品表和口味表进行删除操作

遍历前端传过来的菜品id集合得到每个菜品的信息
如果当前菜品是启售状态或者被套餐关联那么就不能被删除
否则就可以通过菜品id对菜品表和口味表中的数据进行删除
套餐新增:对套餐表和套餐菜品关系表进行新增操作

首先将前端传过来的套餐基本信息保存到套餐表中,并返回主键的id
然后为前端传过来的套餐菜品设置套餐id
最后将套餐包含的菜品添加到套餐菜品关系表中
套餐修改:对套餐表进行修改,在对套餐菜品关系表进行增删操作

首先根据前端传过来的套餐基本信息更新到套餐表中
然后根据套餐的id删除所有套餐菜品关系表中包含的菜品信息
最后遍历前端传过来的菜品的列表,设置好套餐的id后重新保存到套餐菜品关系表中
套餐删除:对套餐表和套餐菜品关系表进行删除操作

首先遍历前端传过来套餐id的集合得到每一个套餐的信息
然后根据id查询套餐,判断套餐的状态是否为启售状态,如果是启售状态,则不能删除
如果是在禁售状态,就可以通过套餐的id进行套餐菜品关系表的删除操作
分类删除

分类删除的核心逻辑就是根据前端传过来的分类id去分类表进行一个删除操作
但是要对这个分类里面是否有菜品和套餐做一个判断,拿着这个id去菜品表和套餐表做一个统计查询
如果查出来数量大于0,就不能删除,如果为0,直接删除
添加购物车:将用户选择的商品基本数据信息添加到数据库表中进行保存

利用到的数据库表(本次项目):购物车表,菜品表、套餐表,保存的信息就是从表中查到的

首先根据id查询购物车中是否有相同商品

有:则不用添加,只修改查询到的商品number属性+1并重新赋值即可,执行mapper更新。

无:则判断是菜品还是套餐,查询对应商品的数据库得到基本信息,补全购物车需要的参数执行保存。

21.session和cookie


二者区别

①Cookie可以存储在浏览器或者本地,Session只能存在服务器
②session 能够存储任意的 java 对象,cookie 只能存储 String 类型的对象
③Session比Cookie更具有安全性(Cookie有安全隐患,通过拦截或本地文件找得到你的cookie后可以进行攻击)
④Session占用服务器性能,Session过多,增加服务器压力
⑤单个Cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个Cookie,Session是没有大小限制和服务器的内存大小有关。

22.Swagger 作用


定义:Swagger 是一个用于设计、构建和文档化 RESTful Web 服务的工具集。
作用:用来在后端生成接口文档,辅助前端测试。使用 Swagger 只需要按照它的规范定义接口以及接口的相关信息,就可以做到生成接口文档,以及在线接口调试页面。


23.Knife 4 j


Knife 4 j 是 Swagger 的一个增强工具,是基于 Swagger 构建的一款功能强大的文档工具。它提供了一系列注解,用于增强对 API 文档的描述和可视化展示。如在苍穹外卖项目中常用的 Knife 4 j 注解介绍:
@Api :用于对 Controller 类进行说明和描述,可以指定 Controller 的名称、描述、标签等信息。
@ApiOperation:用于对 Controller 中的方法进行说明和描述,可以指定方法的名称、描述、请求方法(GET、POST 等)等信息。


24.Pagehelper (Mybatis提供的插件)如何实现分页查询?


PageHelper是MyBatis的一个插件,内部实现了一个PageInterceptor拦截器。Mybatis会加载这个拦截器到拦截器链中。在我们使用过程中先使用PageHelper.startPage这样的语句在当前线程上下文中设置一个ThreadLocal变量,再利用PageInterceptor这个分页拦截器拦截,从ThreadLocal中拿到分页的信息,如果有分页信息拼装分页SQL(limit语句等)进行分页查询,最后再把ThreadLocal中的东西清除掉。

设置分页参数:在执行查询之前,首先通过 PageHelper.startPage(int pageNum, int pageSize) 方法设置分页的参数,调用该方法时,通过 ThreadLocal 存储分页信息。
拦截查询语句:PageHelper 利用 MyBatis 提供的插件 API(Interceptor 接口)来拦截原始的查询语句。MyBatis 执行任何 SQL 语句前,都会先通过其插件体系中的拦截器链,PageHelper 正是在这个环节介入的。
修改原始 SQL 语句:在拦截原始查询语句后,PageHelper 会根据分页参数动态地重写或添加 SQL 语句,使其成为一个分页查询。
执行分页查询:修改后的 SQL 语句被执行,返回当前页的数据。
查询总记录数(可选):如果需要获取总记录数,PageHelper 会自动执行一个派生的查询,以计算原始查询(不包含分页参数)的总记录数。这通常通过移除原始 SQL 的排序(ORDER BY)和分页(LIMIT、OFFSET 等)条件,加上 COUNT(*) 的包装来实现。
返回分页信息:查询结果被封装在 PageInfo 对象中(或其他形式的分页结果对象),这个对象除了包含当前页的数据列表外,还提供了总记录数、总页数、当前页码等分页相关的信息,方便在应用程序中使用。


25.什么是反射?


反射是一种在程序运行时检查和操作类的机制,通过获取类的信息并动态调用方法、创建对象等。这种机制让程序能够在运行时根据需要动态地获取和操作类的结构和成员。

获取 Class 对象: 程序通过类的全限定名、对象的 getClass ()方法或. Class 语法来获取对应的 Class 对象。
查询类信息: 通过 Class 对象可以获取类的信息,包括类名、包名、父类、实现的接口、构造函数、方法、字段等。
动态创建对象: 通过 Class 对象的 newInstance ()方法调用类的默认构造函数来创建对象,或者通过 Constructor 对象调用类的其他构造函数来创建对象。
动态调用方法: 通过 Method 对象调用类的方法,传递参数并获取返回值。
动态访问字段: 通过 Field 对象获取和设置类的字段值。
整个流程就是通过获取 Class 对象,然后根据需要动态地调用类的方法、创建对象、访问字段等操作,实现了对类的动态操作和调用。

26. 使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能。

1.2 实现思路
在实现公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。在上述的问题分析中,我们提到有四个公共字段,需要在新增/更新中进行赋值操作, 具体情况如下:

插入数据时,需要设置的字段值,如下表格
更新数据时,需要设置的字段值,如下表格


实现步骤:

1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法

2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值

3). 在 Mapper 的方法上加入 AutoFill 注解

若要实现上述步骤,需掌握以下知识(之前课程内容都学过)

技术点:

枚举:因为枚举可以标识我们当前这个操作的类型,比如当前操作是insert操作还是update操作。因为你不同的操作,最终操作的字段是不一样的。
注解
AOP
反射:为公共字段赋值
1.3 代码开发(通过AOP实现)
按照上一小节分析的实现步骤依次实现,共三步。

1.3.1 步骤一

自定义注解 AutoFill

1.3.2 步骤二

自定义切面 AutoFillAspect

1.3.3 步骤三

在Mapper接口的方法上加入 AutoFill 注解

 java公共问题


在这个项目中主要负责的模块是什么?
项目中哪些表是你负责设计的? 它们之间的关系是什么?

1、JDK和JRE的区别是什么


JDK是Java开发工具包,JRE是Java运行时环境,二者的区别在于

JRE是Java程序运行所必须的,它包含jvm和一些Java的基础类库

JDK是Java程序开发所必须的,它包含JRE和一些开发工具

总结一下就是:JDK包含JRE,如果仅仅是运行Java程序,只要有JRE即可;如果是要开发Java程序,则必须要有JDK

2、&和&&的区别是什么


&和&&都是逻辑运算符,都可以实现取并的效果,即符号两边的表达式都为true,结果才是true

不一样的是&&有短路的功能,即当符号前面的表达式为false时,后面的表达式将不再执行,而&没有这个功能

另外,&还可以用作位运算符,当&操作符两边的表达式不是boolean类型时,&表示按位与操作

3、final finally finalize区别是什么


这是Java提供的三个关键字,虽然长的差不多,但是其实没什么联系,使用场景也完全不同

​ final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、 修饰变量表示该变量是一个常量不能被重新赋值。

​ finally一般作用在try-catch代码块中,在处理异常的时候,无论程序是否出现异常,写在finally中的代码都会被执行,一般用来释放一些资源

​ finalize是Object类的一个方法,它会在一个对象被垃圾回收的时候自动被垃圾回收器来调用

4、下面几个字符串类的区别是什么


String、StringBuffer、 StringBuilder 、StringJoiner的区别

这几个都是关于字符串的类,他们的区别点有下面几个

可变性:String是不可变对象,其它的都是可变对象
线程安全:String和StringBuffer是线程安全的,StringBuilder和StringJoiner是线程不安全的
效率:StringBuilder和StringJoiner效率最高,StringBuffer居中,String效率最低
使用场景:少量字符串的操作使用String,大量字符串的频繁操作在多线程下使用StringBuffer,单线程下可以使用StringBuilder、StringJoiner


5、使用=和new创建字符串的区别是什么


两种方式都可以创建出字符串,但是在内存分配上却是不一样的

等号方式:JVM会在常量池中创建一个字符串对象

new方式:JVM会先判断常量池中是否有此字符串,如果没有,它就会在常量池中创建一个

而且无论常量池中是否有,它都会在堆内存中重新创建一个

6、float f=3.4 是否正确

这种写法是不正确的。

因为直接写出的字面量3.4是double类型的,将double赋值给float属于向下转型

这种情况下,Java是不允许直接赋值的,如果一定要赋值,则需要强制类型转换 float f =(float)3.4

或者是在声明字面量3.4的时候,直接声明成float类型,即写成 float f =3.4F

7、重写和重载的区别是什么


重载和重写都是用于描述方法间的关系的,但是他们的含义和场景确大不相同

重写是存在于子父类之间的,一般用在父类的方法无法满足子类需求时,子类重写方法来自定义方法功能

​ 它要求子类定义的方法与父类中的方法具有相同的方法名字,相同的参数表和相同的返回类型

重载是存在于同一个类中的,一般用在功能相似的方法需要接收不同的参数时,

它要求多个方法具有相同的名字,但方法具有不同的参数列表

8、this和super的应用场景是什么

this和supper都是Java提供的关键字

this代表的是当前对象,一般用于在一个方法中调用本对象的成员变量或其它方法

supper代表是父类对象,一般在本对象和父对象出现成员名称冲突时,强行调用父对象的成员,也经常用于调用父类的构造方法

9、throw和throws的区别是什么


throws:用在方法的声明上,声明当前方法可能抛出的异常

throw:写在方法里,真正的抛出一个异常,抛出自定义异常。创建对象自定义抛出异常

10、应该使用什么数据类型来计算价格


如果不是特别关心内存和性能的话,使用 BigDecimal

否则使用预定义精度的 double 类型

11、== 与 equals 的区别


==是一个运算符,equals 是 Object 类的方法
用于基本类型的变量比较时: ==比较的是值是否相等,equals不能直接用于基本数据类型的比较,需要转换为其对应的包装类型。
用于引用类型的比较时。==和 equals 都是比较栈内存中的地址是否相等。但是通常会重写 equals 方法去实现对象内容的比较。


12、接口和抽象类的区别


它们的共同点是:都不能实例化对象

它们的不同点是:

1. 抽象类一般用于抽取子类中的共同方法和属性,接口一般用于指定实现类的规范
2. 抽象类可以有构造方法,作用是用给抽象父类中中的属性赋值;接口中不能有构造方法
3. 接口中不能含有静态代码块,而抽象类可以有静态代码块
4. 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
1
2
3
4
当然我这里对于接口的描述是基于JDK1.8之后的

13、说出几个常见的异常


Java中的异常分为运行时异常和编译时异常两大类:

运行时异常都是 RuntimeException 类及其子类异常,这类异常的特点是不强行要求程序员进行处理,常见的有

NullPointerException 空指针异常,调用了未经初始化的对象或者是不存在的对象

IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生

ClassCastException 数据类型转换异常

NoSuchMethodException 方法不存在异常

非运行时异常,也叫编译异常,是 Exception 的子类但不是 RuntimeException的子类,类型上都属于及其子类

它要求程序员在编写代码的过程中提供异常处理方案,否则编译不通过,常见的有:IOException和SQLException等

14、Java 反射有了解吗


反射是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法,并且可以调用它的任意一个方法

它主要应用于大量的框架底层,比如 Spring/Spring Boot、MyBatis 等等

15、浅拷贝和深拷贝区别


浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。即对象的浅拷贝会对“主”对象进行拷贝,但不会复制主对象里面的对象。”里面的对象“会在原来的对象和它的副本之间共享。

深拷贝:深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。简而言之,深拷贝把要复制的对象所引用的对象都复制了一遍

16.TCP四次挥手

四次挥手的过程如下:

第一次挥手:

客户端向服务器发送一个 FIN 数据包(FIN = 1,seq = u)主动断开连接,报文中会指定一个序列号。
告诉服务器:我要跟你断开连接了,不会再给你发数据了;
客户端此时还是可以接收数据的,如果一直没有收到被动连接方的确认包,则可以重新发送这个包。
此时客户端处于 FIN_WAIT1 状态。
第二次挥手:

服务器收到 FIN 数据包之后,向客户端发送确认包(ACK = 1,ack = u + 1),把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了
这是服务器在告诉客户端:我知道你要断开了,但是我还有数据没有发送完,等发送完了所有的数据就进行第三次挥手
此时服务端处于 CLOSE_WAIT 状态,客户端处于 FIN_WAIT2 状态
第三次挥手:

服务器向客户端发送FIN 数据包(FIN=1,seq = w),且指定一个序列号,以及确认包(ACK = 1, ack = u + 1),用来停止向客户端发送数据
这个动作是告诉客户端:我的数据也发送完了,不再给你发数据了
此时服务端处于LAST_ACK状态,客户端处于TIME_WAIT状态
第四次挥手:

客户端收到 FIN数据包 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值
此时客户端处于 TIME_WAIT 状态。
需要过一了一定时间(2MSL)之后,客户端发送确认包(ACK = 1, ack = w + 1),此时客户端才会进入 CLOSED 状态,以确保发送方的ACK可以到达接收方,防止已失效连接请求报文段出现在此连接中。
至此,完成四次挥手。

17.排序算法

1.什么是算法时间复杂度
时间复杂度表示了算法的执行时间与数据规模之间的增长关系

3.什么是算法的空间复杂度?
表示算法占用的额外存储空间与数据规模之间的增长关系

常见的空间复杂度:O(1),O(n),O(n^2)

稳定性:

它说的是,当一个排序算法对一个数组排序的时候,两个相同的数,在排序后相对位置有没有发生变化。没有则稳定

冒泡排序算法的原理如下:

比较相邻的元素。如果第一个比第二个大,就交换他们两个。

对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。

针对所有的元素重复以上的步骤,除了最后一个。

持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

快速排序算法的原理如下:

​ 1.从数列中挑出一个元素,称为 “基准”(pivot);

​ 2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

​ 3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

桶排序算法的原理如下:

​ 1.设置一个定量的数组当作空桶;

​ 2.遍历输入数据,并且把数据一个一个放到对应的桶里去;

​ 3.对每个不是空的桶进行排序;

​ 4.从不是空的桶里把排好序的数据拼接起来。

集合问题

1、说一说集合类的体系结构


我们常见的集合主要有两大类,分别是单列集合和双列集合

单列集合的顶级接口是Collection,它下面有两个主要的子接口分别是List和Set

List的特点是元素有序的,可以重复的;Set的特点是元素无序的,不可重复的

List下我们常用的类有ArrayList、LinkedList等,Set下我们常用的类有HashSet、LinkedHashSet、TreeSet等

双列集合的顶级接口是Map,它的特点是每个元素都有键和值两部分组成,而且键不能重复

Map接口下我们常用的类有:HashMap、LinkedHashMap、TreeMap等

2、聊聊集合类的底层数据结构


集合主要分为双列集合和双列集合

双列集合都是Map的实现类,主要有HashMap、LinkedHashMap和TreeMap

HashMap: JDK1.8之前是由数组+链表组成的,JDK1.8之后,为了提升效率,在当链表的长度>8,并且数组长度>=64的时候,链表就会转换为红黑树
LinkedHashMap:继承自HashMap,在HashMap的基础上增加了一条双向链表,来保持键值对的插入顺序。
TreeMap:底层是红黑树
单列集合主要是List和Set

List有ArrayList和LinkedList,ArrayList底层是数组,查询快,增删慢;LinkedList底层是双向链表,查询慢,增删快

Set有HashSet、LinkedHashSet和TreeSet,它的实现原理和对应的Map是一样的,底层都是用的对应Map的key实现

3、ArrayList和LinkedList的区别


ArrayList和LinkedList都是Java中的单列结合,都是有序的,可重复的

不同点有下面几个:

底层数据结构不同:ArrayList 底层是动态数组,而LinkedList底层是双向链表
使用场景不同:ArrayList查询快,增删慢,适合查询场景;LinkedList查询慢,增删快,适合频繁修改的场景
占用内存空间不同:LinkedList比ArrayList更占内存,这是因为它的每个节点除了存储数据,还存储了前后节点的引用两个引用


4、HashMap和HashTable区别


HashMap和HashTable都是Map的子类,都可以存储键值对的数据,区别点在于HashTable是线程安全,HashMap则不是

HashTable的线程安全是通过底层在每个方法上添加synchronized 实现的,因此它的效率要比HashMap低

HashTable在我们公司中已经不再使用,我对它的了解也不是特别多,我们公司一般都是采用的HashMap

如果碰到需要线程安全的场景,我们则会使用ConcurrentHashMap,而不用HashTable,所以我对它的了解也就这些

5、HashMap的底层原理


HashMap底层数据结构是哈希表,哈希表在JDK1.8之前是数组+链表实现,在JDK1.8之后是数组+链表+红黑树实现的

下面我以map中存储对象的流程给您说一下它的实现原理把

当我们创建一个HashMap的时候,JDK就会在内存中创建一个长度为16的数组

当我们调用put方法像HashMap中保存一个元素的时候,它会先调用key的hashCode方法计算出key的hash值

然后使用得到hash值对数组长度取余,找出当前对象的元素在数组中的位置

接下来,它会判断算出的位置上是否有元素,如果没有,就会将此元素直接存储到当前位置上

如果算出的位置上有元素或者是有链表,它会再调用key的equals方法跟存在元素的key做比较

如果有一个比较得到的结果为true,则会进行值的覆盖,如果都为false,则会将元素追加在链表的末尾

当然,为了降低Hash冲突和链表长度,HashMap还做了一些优化

当元素的数量超过数组大小与加载因子的乘积的时候,就会执行扩容,扩容为原来的2倍,并将原来数组中的键重新进行hash运算,然后分配到新数组中

当链表的长度>8,并且数组长度>=64的时候,链表就会转换为红黑树,当红黑树结点数小于6时将再次转回为链表。

6、HashMap是怎么解决哈希冲突的


​ 首先,HashMap的底层有一个数组,它在保存元素的时候,会对元素的key进行hash运算,得到hash值,然后再使用hash值对数组长度取余,得到元素在数组中的位置,这样的话,不同的元素计算完毕之后,就可能会被分配到数组中的同一个位置上,这就是所谓的哈希冲突。

​ 解决hash冲突最常用的方式有链表法和开放地址法,而HashMap就是采用了链表法。具体做法就是当哈希冲突出现之后,HashMap会在发生冲突的位置上创建一个链表来保存元素,当然在JDK1.8之后,又对此做出了改进,那就是当链表的长度>8,并且数组长度>=64的时候,链表就会转换为红黑树,使得效率更高。

7、HashMap的扩容机制是怎样的


​ HashMap的扩容机制是指当HashMap中的元素个数超过数组长度乘以负载因子时,就会重新分配一个更大的数组,并将原来的元素重新计算哈希值并插入到新的数组中。

​ 在JDK1.8中,底层是调用resize方法实现扩容的,它的默认做法是:当元素个数超过数组长度的0.75倍时触发扩容,每次扩容的时候,都是扩容为原来的2倍, 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。

8、为何HashMap的数组长度一定是2的次幂?


计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
为了能让 HashMap 存数据和取数据的效率高,尽可能地减少 hash 值的碰撞,也就是说尽量把数据能均匀的分配,每个链表或者红黑树长度尽量相等。我们首先可能会想到%取模的操作来实现。


9、说一下HashSet的实现原理?


​ HashSet是基于HashMap实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此 HashSet的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,由于HashMap的键是不能重复的,所有HashSet 不允许重复的值。

10、HashSet如何检查重复?


​ HashSet是一个不允许存储重复元素的集合,它通过哈希表来实现。在HashSet中,每个元素都是唯一的,如果尝试添加一个已经存在的元素,HashSet会拒绝并保留原有的元素。

​ 具体做法是在向HashSet中保存元素的时候,会先计算该元素的哈希值,然后确定这元素在数组中的位置,如果该位置上没有元素,则保存成功,如果有元素,则调用equals方法去跟存在的每个值进行比较,表较结果有一个相等,则直接丢弃;不相等,就会被挂在老元素的后面。

线程问题

1、创建线程有几种方式


我知道的创建线程的方式大体上可以分为四种:

继承Thread类并重写run方法创建线程,这种方式实现简单但线程类不可以再继承其他类
实现Runnable接口并重写run方法,这种方式避免了单继承局限性,编程更加灵活,实现解耦
实现Callable 接口并重写call方法,这种方式可以获取线程执行结果的返回值,并且可以抛出异常
使用线程池创建


2、runnable和callable的区别


这两个接口都是线程任务类的接口,区别点在于

Runnable接口run方法无返回值;Callable接口call方法有返回值,也就是说如果需要获取线程类的执行结果,必须要使用Callable,如果不需要返回结果,则使用Runnable更简单一些
Runnable接口run方法只能抛出运行时异常,且无法捕获处理;Callable接口call方法允许抛出异常,可以获取异常信息


3、start和run的区别


run(): 封装了要被线程执行的代码,本质上就是一个普通方法,可以被调用多次

start(): 用来启动线程,底层会自动去执行run方法中的代码,start方法只能被调用一次

也就是启动线程的时候,只能调用start方法,如果调用的run方法,不会启动新线程,而是当普通方法调用执行

4、notify和 notifyAll的区别


这两个方法都是用户唤醒被wait方法休眠的线程的,区别点在于:

notifyAll:唤醒所有wait的线程
notify:随机唤醒一个 wait 线程


5、sleep 和 wait 的区别


sleep和wait都是Java中用来让线程暂时放弃CPU使用权,进入阻塞状态的方法。他们的主要区别点有下面几个:

方法归属不同:sleep是Thread 的静态方法,而wait是Object的成员方法
醒来时机不同: sleep会在指定的时间后自动苏醒,而wait需要其他线程的唤醒
锁特性不同:sleep不会释放锁,而wait会释放锁
使用限制不同:wait必须用在synchronized代码块中,而sleep无此限制


6、说一下线程的状态及转换


在我的理解中,线程共分为7种状态,分别是:新建、就绪、运行、终止以及阻塞、等待、计时等待

它们之间的转换关系是这样的:

当线程new出来之后,没有start之前就会处于新建状态
当线程执行start方法之后,就进入就绪状态
当就绪的线程一旦获取到了cpu的执行权,就可以进入运行状态
当线程执行完了run方法之后,就进入了死亡状态
这是一条正常的流程,但是代码在运行状态下可以因为一些原因进入到其它状态,比如说:

当进行抢锁操作时,抢锁失败就会进入到阻塞状态
当代码调用了wait方法时,就会进入等待状态
当代码调用了sleep方法时,就会进入计时等待状态

这是我对线程状态及其转换的理解


7、现在有T1,T2,T3三个线程,如何保证它们按顺序执行?


在多线程中有多种方法让线程按特定顺序执行,最简单的方式就是使用线程类的join方法实现

join方法是Thread类中的一个方法,它的作用是将当前线程挂起,等待其他线程结束后再执行当前线程

具体来说就是:可以在t2之前调用t1.join(),在t3之前调用t2.join()

8、synchronized的实现原理是怎样的


在Java中,每个对象都隐式包含一个 monitor(监视器)对象,加锁的过程其实就是竞争 monitor 的过程,

当线程进入字节码monitorEnter指令之后,线程将持有monitor对象,执行monitorExit时释放 monitor 对象

如果在这个过程中,其它线程就会阻塞等待获取该monitor对象

mybatis问题

1、Mybatis中#{}和${}的区别


在Mybatis中#{}和${}都可以用于在sql语句中拼接参数,但是在使用方面有很多的区别

1、处理方式不同:${}表示的是字符串拼接,Mybatis在处理它时,会直接将${}替换成变量的值

​ 而#{}是预编译处理,Mybatis在处理它时,会将sql中的#{}替换为?号,然后底层使用JDBC的预编译对象来赋值

2、安全性不同:${}存在SQL注入问题,#{}可以有效的防止SQL注入

3、效率不同:${}处理的sql到数据库每次都要重新编译,而#{}处理的sql只需要编译一次

总之,在实际使用过程中尽量使用#{},而避免使用${},当然这也不是说${}就没有使用场景

比如:如果sql中需要动态传递表名或者字段名,那就只能使用${}了

2、当实体类中的属性名和表中的字段名不一样 ,怎么办


是这样的,当实体类中的属性名和表中的字段名一样的时候,Mybatis会自动完成查询结果的映射

但是如果不一样,Mybatis默认无法完成结果映射,此时我们可以使用下面这几种方案:

1、开启驼峰映射:这种方式可以处理掉字段和属性满足驼峰转换规则的那部分

2、字段起别名:可以在查询的SQL语句中定义字段名的别名,让字段名的别名和实体类的属性名一致

3、手动映射:mybatis提供了resultMap标签,它可以通过来自定义映射关系来保证字段和属性的映射

3、MyBatis动态SQL了解吗


动态SQL是为了解决SQL语句灵活性不足的问题而提出的一种技术,它可以根据条件拼接SQL语句以不同的查询需求

MyBatis常用的动态SQL标签有:

条件判断标签:if、choose、when、otherwise 当条件成立时才执行其中的 SQL 语句
格式整理标签:trim、where、set 它可以在生成的SQL语句中调整格式,去除多余的关键字和符号
循环遍历标签:foreach 它用于遍历一个集合并将集合中的元素添加到 SQL 语句中
动态 SQL 的执行原理是,当 MyBatis 执行动态 SQL 语句时,会将 SQL 语句和参数传递给 SQL 解析器进行解析

SQL 解析器会根据 SQL 语句中的动态标签和参数的值,生成一个完整的 SQL 语句

然后,MyBatis将生成的SQL语句和参数传递给 JDBC 驱动程序进行执行

springboot问题

1、项目中为什么选择SpringBoot


SpringBoot简化了Spring,可以快速搭建企业级项目,而且开发起来效率也会更高,它的主要优点如下:

版本锁定:SpringBoot在父工程中进行了大量常见依赖的版本锁定,省去了我们查找依赖版本和解决版本冲突的麻烦
起步依赖:SpringBoot以功能化的方式将需要的依赖进行组装,并且允许程序员以starter的方式进行引入
默认配置:SpringBoot实现了大量依赖框架的默认配置项,程序员无须再进行自己配置
内置Tomcat:SpringBoot内置了一个tomcat,使用它开发的程序无需再进行tomcat部署,可直接运行


2、SpringBoot的自动装配原理


Springboot自动装配主要是基于注解编程和约定优于配置的思想来进行设计的

自动装配就是自动地把其他组件中的Bean装载到IOC容器中,不需要开发人员再去配置文件中添加大量的配置

我们只需要在SpringBoot的启动类上添加一个@SpringBootApplication的注解,就可以开启自动装配

SpringBootApplication底层最重要的一部分是@EnableAutoConfiguration这个注解来实现的,它作用是:

读取所有jar包中两个指定配置文件中的所有自动配置类(xxxxAutoConfiguration)
这些值必须声明为Spring的配置类,也就是在类中需要向Spring容器放入对象
为了防止非当前所需的组件进入到容器,配置类中需要使用@Conditional注解来声明配置成立的必要条件


3、SpringBoot的核心注解是哪个


SpringBoot的核心注解在启动类上,叫@SpringBootApplication,主要组合包含了以下3个注解:

@SpringBootConfiguration:组合了@Configuration注解,实现配置文件的功能
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项
@ComponentScan:Spring组件扫描,默认会扫描启动类所在的包


4、SpringBoot中的starter是干什么的


当项目足够复杂时,因为涉及的组件太多了,就需要引入很多的依赖,此时管理依赖就边的很麻烦

此时SpringBoot的starter就派上用场了,每个starter都可以为我们提供某个服务场景所需要的一系列依赖

在导入starter之后,SpringBoot主要帮我们完成了两件事情:

相关组件的自动导入
相关组件的自动配置


5、SpringBoot可以有哪些方式加载配置


SpringBoot支持很多种方式加载配置,常见有

配置文件,直接在项目中提供SpringBoot支持的配置文件,比如properties、yaml 、yml

系统环境变量,SpringBoot是可以读取系统环境变量中的配置信息的,但不推荐这么做

命令行参数,SpringBoot在项目启动的时候运行通过命令行直接传递参数,一般用于临时修改配置的情况

6、bootstrap.yml和application.yml有何区别


这是SpringBoot支持的两个核心配置文件,区别点在于

boostrap比applicaton优先加载,在应用程序上下文的引导阶段生效,且里面的属性不能被覆盖

一般来说我们在SpringCloud Config或者Nacos中会用到它

application用于SpringBoot项目的自动化配置,一般来说我们会将自己项目的业务配置项写在这里面

7、SpringBoot读取配置的方式有几种


SpringBoot常见的读取配置信息的方式有两种:

使用@Value配合EL表达式(@Value(“${name}”))注解直接注入对应的值
使用@ConfigurationProperties注解把对应的值绑定到一个配置对象,然后将配置对象注入到需要的地方
推荐使用使用第二种方式,在配置比较多的情况下,操作简单,可读性好

8、SpringBoot项目如何热部署


Spring Boot有一个开发工具(DevTools)模块,通过它可以实现SpringBoot项目的热部署

也就是开发人员将文件更改后,它会自动部署到服务器并自动重启服务器。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-devtools</artifactId>
</dependency>
1
2
3
4


9、SpringBoot项目如何实现方法的异步调用


异步调用指的是a方法在调用b方法的时候,无需等待b方法执行完毕,就可以继续向下执行

一般用在a方法无需使用b方法返回结果的场景下,可以在一定程度上提高运行效率

在SpringBoot中使用异步调用是很简单的,只需要做两个操作就可以了

在启动类上添加@EnableAsync注解,开启异步调用支持
在被调用的方法上添加@Async注解
controller-a(){
    代码1 
    service-a()
    代码3 
}

@Async
service-a(){}
1
2
3
4
5
6
7
8


10、SpringBoot中如何实现定时任务


在SpringBoot中使用定时任务主要有两种方式,一个就是使用SpringTask,另一个则是使用第三方框架Quartz

SpringTask主要是通过@Scheduled注解来实现定时任务触发的,格式如下

@Scheduled(fixedRate = 5000)
public void printTime() {
    System.out.println(new Date().toLocaleString());
}
1
2
3
4
主要属性如下:

fixedRate:按一定的频率执行任务,参数类型为long,单位 ms
fixedDelay:上一次任务执行完后多久再执行,参数类型为long,单位 ms
initialDelay:延迟多久再第一次执行任务,参数类型为 long,单位 ms
cron:使用cron表达式指定任务在特定时间执行


11、SpringBoot中如何解决跨域问题


跨域是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制

当浏览器从一个域名的网页去请求另一个域名的资源时,出现域名、端口、协议任一不同,都属于跨域


SpringBoot解决跨域很简单,执行添加一个配置类实现WebMvcConfigurer接口然后重写addCorsMappings方法即可

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
        .allowedOrigins("*")//允许跨域访问的路径
        .allowCredentials(true)
        .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")//运行跨越的请求方式
        .maxAge(3600);
    }
}
1
2
3
4
5
6
7
8
9
10
11


12、如何理解拦截器


拦截器是Spring提供的一种拦截机制,目的是实现对指定请求路径进行拦截,然后做成指定的处理

自定义一个拦截器需要实现HandlerInterceptor,并重写接口中定义的3个方法

preHandle: 这个方法在Controller处理请求之前被调用,通过方法的返回值可以确定是否放行请求
postHandle:这个方法在Controller处理请求之后被调用
afterCompletion:这个方法将在整个请求结束之后被调用,此方法主要用于进行资源清理


13、拦截器和过滤器的区别是什么


拦截器和过滤器都可以实现请求的拦截处理,不同点有下面几个:

技术栈所属不同:过滤器属于JavaWeb技术,依赖Servlet容器;而拦截器是属于Spring的技术
实现原理不同:拦截器是基于Java的反射机制,而过滤器是基于函数回调
拦截范围不同:过滤器可以拦截所有请求,而拦截器主要是针对发往controller请求
拦截位置不同:过滤器在前端控制器前拦截行,而拦截器在前端控制器后拦截

14.boot和mvc的区别

Spring Boot和Spring MVC都是Spring框架的一部分,但它们有一些不同之处:
目的不同:Spring Boot旨在简化Spring应用程序的开发和部署,提供了自动配置、快速启动、内嵌服务器等功能,可以让开发人员更加专注于业务逻辑的实现。而Spring MVC则是一个Web框架,用于构建Web应用程序。

配置方式不同:Spring Boot提供了自动配置功能,可以根据应用程序的依赖关系自动配置应用程序。而Spring MVC需要手动配置,需要编写XML或Java配置文件来配置应用程序。

依赖不同:Spring Boot包含了Spring MVC,因此在使用Spring Boot时不需要显式地引入Spring MVC的依赖。而在使用Spring MVC时,需要显式地引入Spring MVC的依赖。

启动方式不同:Spring Boot提供了快速启动功能,可以通过命令行或者Maven插件来启动应用程序。而Spring MVC需要部署到Web服务器中才能运行。

总之,Spring Boot和Spring MVC都是Spring框架的一部分,但它们的目的、配置方式、依赖和启动方式都有所不同。选择使用哪个框架,需要根据具体的需求和场景来决定。

springcloud问题

1、SpringCloud组件有哪些


SpringCloud 是一系列框架的有序集合。它利用 SpringBoot 的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 SpringBoot 的开发风格做到一键启动和部署。

早期我们一般认为的Spring Cloud五大组件是

Eureka : 注册中心
Ribbon : 负载均衡
Feign : 远程调用
Hystrix : 服务熔断
Zuul/Gateway : 网关
随着SpringCloudAlibba在国内兴起 , 我们项目中使用了一些阿里巴巴的组件

注册中心/配置中心 Nacos

负载均衡 Ribbon

服务调用 Feign

服务保护 sentinel

服务网关 Gateway

2、Feign工作原理


Feign是SpringCloud技术栈中用于远程调用的一个HTTP客户端,主要作用是将远程服务调用格式本地方法调用格式统一成一致的

Feign的工作步骤如下:

首先需要在SpringBoot的启动类上添加@EnableFeignClients 注解开启对Feign的支持

当程序启动时,会扫描所有标有@FeignClient的注解的类,并且将这些信息注入Spring IOC 容器中

当定义的 Feign 接口中的方法被调用时,通过JDK的代理方式,来生成具体的 RequestTemplate

RequestTemplate对象封装了 HTTP 请求需要的全部信息,如请求参数名,请求方法等信息

然后RequestTemplate生成 Request,并将Request交给Client去处理,这里的 Client 可以是 JDK 原生的 URLConnection、Apache 的 HttpClient等

最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发起服务之间的调用

3、什么是Hystrix


在微服务架构中,我们会拆分出很多的服务,服务之间存在复杂的调用关系,那么这些服务一旦会出现失败的情况,就会导致服务雪崩

Hystrix就是来防止服务雪崩的工具,它具有服务降级,服务熔断,服务隔离,监控等一些防止雪崩的技术。 它有四种防雪崩手段:

服务隔离:隔离服务之间相互影响
服务监控:在服务发生调用时,会将每秒请求数、成功请求数等运行指标记录下来
服务熔断:接口调用失败就会进入调用接口提前定义好的一个熔断的方法,返回错误信息
服务降级:接口调用失败就调用本地的方法返回一个空


4、Hystrix断路器状态有哪些


断路器状态机包括三个状态:

closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后(默认值)会进入half-open状态
half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作
请求成功:则切换到closed状态
请求失败:则切换到open状态

5、Ribbon的工作原理


我们项目中使用的是Feign来进行远程微服务的调用,Feign的底层是集成了Ribbon的,大体使用流程如下:

当请求发出的时候,会被Ribbon的负载均衡拦截器所有拦截
Ribbon会提取请求路径中微服务的名称,然后去服务治理中心中查找微服务的对应的服务地址
Ribbon会使用配置的负载均衡策略从众多地址中选择一个,进行调用


6、Ribbon的负载均衡策略有哪些


Ribbon官方提供了7种负载均衡策略

轮询策略:按照一定的顺序依次调用服务实例
权重策略:根据每个服务提供者的响应时间分配一个权重,响应时间越长,权重越小,被选中的可能性也就越低。它的实现原理是,刚开始使用轮询策略并开启一个计时器,每一段时间收集一次所有服务提供者的平均响应时间,然后再给每个服务提供者附上一个权重,权重越高被选中的概率也越大。
随机策略:从服务提供者的列表中随机选择一个服务实例
最小连接数策略:遍历服务提供者列表,选取连接数最小的一个服务实例。如果有相同的最小连接数,那么会调用轮询策略进行选取。
重试策略:按照轮询策略来获取服务,如果获取的服务实例为 null 或已经失效,则在指定的时间之内不断地进行重试来获取服务,如果超过指定时间依然没获取到服务实例则返回 null。
可用性敏感策略:先过滤掉非健康的服务实例,然后再选择连接数较小的服务实例
区域敏感策略:根据服务所在区域(zone)的性能和服务的可用性来选择服务实例,在没有区域的环境下,该策略和轮询策略类似。


7、Nacos的工作原理


Nacos是SpringCloudAlibaba技术栈的一项技术,在项目中主要用作服务注册中心和服务配置中心

Nacos做服务注册中心主要具备下面这些能力

服务注册:服务提供者会将自己的地址信息注册到Nacos中,在nacos中形成一张服务清单

服务发现:服务消费者会从Nacos中查询服务提供者的信息,并且缓存到本地,并且每隔30s更新一次

当服务提供者的地址发生变化之后,Nacos也会主动推送最新的地址信息给消费者

服务续约:服务提供者会间隔一定时间就给Nacos发送心跳,表明自己在线

服务剔除:当nacos一段时间内接收不到服务微服务的续约请求时或者收到微服务的下线请求时,就会将服务地址从服务清单中删除

Nacos做服务配置中心的原理
Nacos允许微服务将经常改动的一些配置项保存到Nacos中,然后在本地的bootstrap.yml中指定远程配置的位置信息
一旦Nacos的配置发生变化之后,会主动推送给微服务,微服务进行热更新
Nacos还支持使用多环境、命名空间的方式实现多套配置文件的共存

rabbitMQ问题

1、你们项目中哪里用到了RabbitMQ


RabbitMQ是我们项目中服务通信的主要方式之一 , 我们项目中服务通信主要有二种方式实现 :

通过Feign实现服务的同步调用
通过MQ实现服务的异步通信
下面要结合自己的项目中功能来说两个地方

xxx

xxx

2、为什么会选择使用RabbitMQ


我们项目中之所以选择使用RabbitMQ,是因为它的功能比较丰富 , 支持各种消息收发模式, 支持延迟队列 , 惰性队列

而且天然支持集群, 保证服务的高可用, 同时性能非常不错 , 社区也比较活跃, 文档资料非常丰富

使用MQ有很多好处,简单跟您说几个:

吞吐量提升:无需等待订阅者处理完成,响应更快速

故障隔离:服务没有直接调用,不存在级联失败问题

调用间没有阻塞,不会造成无效的资源占用

耦合度极低,每个服务都可以灵活插拔,可替换

流量削峰:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件

当然使用使用MQ也有一些缺点

架构复杂了,业务没有明显的流程线,不好管理
需要依赖于Broker的可靠、安全、性能
总之,瑕不掩瑜,使用了RabbitMQ之后可以大大提供程序的效率

3、使用RabbitMQ如何保证消息不丢失


消息从生产者发送到消费者接收,会经历多个过程 , 其中的每一步都可能导致消息丢失

大体可以分为这样几种情况:

消息发送到交换机丢失
消息从交换机路由到队列丢失
消息保存到队列中丢失
消费者消费消息丢失
针对每一步,RabbitMQ分别给出了解决方案:

消息发送到交换机丢失:发布者确认机制

消息发送到交换机失败会向生产者返回失败原因,生产者通过回调接收发送结果,如果发送失败,重新发送,或者记录日志人工介入

消息从交换机路由到队列丢失:发布者回执机制

消息从交换机路由到队列失败会向生产者返回失败原因 ,生产者通过回调接收回调结果,如果发送失败,重新发送,或者记录日志人工介入

消息保存到队列中丢失:MQ持久化

RabbitMQ运行开启交换机持久化、队列持久化、消息持久化,以保证消息在传输过程中不会丢失

消费者消费消息丢失:消费者确认机制

消费者确认机制指的是只有消费者一方确认消息消费成功了,mq才删除消息,否则就会重新发送消息给消费者

通过RabbitMQ本身所提供的机制基本上已经可以保证消息不丢失, 但是因为一些特殊的原因还是会发送消息丢失问题 ,

例如 : 回调丢失 , 系统宕机, 磁盘损坏等 , 这种概率很小 , 但是如果想规避这些问题 , 进一步提高消息发送的成功率, 也可以通过程序自己进行控制


设计一个消息状态表 , 主要包含 : 消息id , 消息内容 , 交换机 , 消息路由key , 发送时间, 签收状态等字段 , 发送方业务执行完毕之后 , 向消息状态表保存一条消息记录, 消息状态为未签收 , 之后再向MQ发送消息 , 消费方接收消息消费完毕之后 , 向发送方发送一条签收消息 , 发送方接收到签收消息之后 , 修改消息状态表中的消息状态为已签收 ! 之后通过定时任务扫描消息状态表中这些未签收的消息 , 重新发送消息, 直到成功为止 , 对于已经完成消费的消息定时清理即可 !

4、消息的重复消费问题如何解决的


在使用RabbitMQ进行消息收发的时候,如果发送失败或者消费失败会自动进行重试,那么就有可能会导致消息的重复消费

解决方案:

每条消息设置一个唯一的标识id
幂等方案
token+redis
分布式锁
数据库锁(悲观锁、乐观锁)


5、如何解决消息堆积在MQ的问题


解决消息堆积有几种种思路:

提高消费者的消费能力,例如使用多线程消费
增加消费者数量,提高消费速度,可以使用ork队列模式,设置多个消费者消费消费同一个队列中的消息
扩大队列容积,提高堆积上限
使用RabbitMQ惰性队列,接收到消息后直接存入磁盘而非内存,消费者要消费消息时才会从磁盘中读取并加载到内存


6、RabbitMQ如何保证消费的顺序性


一个队列只设置一个消费者消费即可 , 多个消费者之间是无法保证消息消费顺序性的

7、RabbitMQ的延迟队列有了解过嘛


RabbitMQ的延迟队列有两种实现方案 :

使用消息过期TTL + 死信交换机
使用延迟交换机插件


8、RabbitMQ如何设置消息过期


RabbitMQ设置消息过期的方式有两种 :

在队列上设置过期时间,所有进到这个队列的消息就会具有统一的过期时间

为消息单独设置过期时间

注意 :

队列过期和消息过期同时存在 , 会以时间短的时间为准
RabbitMQ队列消息过期的机制是判断队列头部元素是否过期 , 如果队里头部消息没有到过期时间 , 中间消息到了过期时间, 这个消息也不会被自动剔除


9、什么是死信交换机


死信交换机和正常的交换机没有什么不同,当一个包含死信的队列使用dead-letter-exchange属性,指定了一个交换机,这个交换机称为死信交换机

也就是说只有队列中的死信才会流转到死信交换机,而当一个队列中的消息满足下列情况之一时,就会成为死信:

消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
消息是一个过期消息,超时无人消费
要投递的队列消息满了,无法投递
一般的死信交换机还会再跟着一个专门的队列,用来专门存储所有的死信,以方便后期的人工干预

10、RabbitMQ的集群有哪些


RabbitMQ天然支持集群模式,它的集群有两种模式:

普通集群:是一种分布式集群,将队列分散到集群的各个节点,从而提高整个集群的并发能力

这种集群会在集群的各个节点间共享部分数据,包括:交换机、队列元信息。不包含队列中的消息。

当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回

如果队列所在节点宕机,队列中的消息就会丢失

镜像集群:是一种主从集群,普通集群的基础上,添加了主从备份功能,提高集群的数据可用性。

这种集群模式下,交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份

创建队列的节点被称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点。

一个队列的主节点可能是另一个队列的镜像节点

所有操作都是主节点完成,然后同步给镜像节点

主宕机后,镜像节点会替代成新的主

分布式服务问题

1、什么是分布式事务


在分布式系统中,一个业务因为跨越不同数据库或者跨越不同微服务而包含多个子事务,要求所有子事务同时成功或失败,这就是分布式事务。

比如一个电商系统的下单操作需要请求三个服务来完成,这三个服务分别是:订单服务,账户服务,库存服务。

当订单生成完毕以后,就需要分别请求账户服务和库存服务进行进行账户余额的扣减和库存扣减。

假设都扣减成功了,此时在执行下单的后续操作时出现了问题,那么订单数据库就进行事务回滚,订单生成失败,而账户余额和扣减则都扣减成功了。

这就出现了问题,而分布式事务就是解决上述这种不一致问题的。

产生分布式事务的原因主要有下面几种:

跨库事务:一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据
跨服务事务:一个应用某个功能需要调用多个微服务进行实现,不同的微服务操作的是不同的数据库


2、什么是CAP理论


在分布式系统有三个指标,分别是一致性、可用性、分区容错性

一致性(Consistency) : 分布式系统中的更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,不能存在中间状态

可用性(Availability) : 分布式系统中提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果

分区容错性(Partition tolerance) : 分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务

CAP定理是指这个三个指标最多可以同时满足两个

3、为什么分布式系统中无法同时AC


对于分布式系统而言,各节点之间一定会存在网络交互,首先网络存在延迟,其次无法100%确保网络的可用,因此可以认为分区网络故障不可避免。

在此条件下,如果要保证各节点的一致性,就必须在一个节点数据变更后同步给其他节点前,让客户等待,这就无法满足可用性

如果要保证各节点的可用性,就必须让各节点在接收到请求立即返回响应,那这个时候各节点可能还没有完成数据的统一,所以就违背了一致性

所以,在存在系统分区的场景下,可用性和一致性无法同时满足

4、什么是BASE理论


BASE是CAP理论的延伸,核心思想是即使无法做到强一致性,但应用可以采用适合的方式达到最终一致性。它的思想包含三方面:

Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。
Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。


5、分布式事务的解决方案有哪些


分布式事物的解决方案有很多,常见的有2PC、TCC,还有可以使用MQ来做

方案一:2PC

2PC即两阶段提交,它是一种保证强一致性的处理方式。 主要将事务分为两个阶段:

阶段一: 表决阶段,所有参与者都将本事务执行预提交,并将能否成功的信息反馈发给协调者。
阶段二: 执行阶段,协调者根据所有参与者的反馈,通知所有参与者,步调一致地执行提交或者回滚。
方案二:TCC

TCC又称补偿事务,它是一种保证最终一致性的处理方式,一共有三个步骤:

Try:做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confirm一起才能真正构成一个完整的业务逻辑
Confirm:做确认提交,Try阶段所有分支事务执行成功后开始执行Confirm
Cancel:在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放
方案三:MQ分布式事务

如果数据强一致性要求没那么高,可以采用消息中间件(MQ)实现事务最终一致。

在支付系统中,常常使用的分布式事务解决方案就是基于MQ实现的,它对数据强一致性要求没那么高,但要求数据最终一致即可。

例如:向借呗申请借钱,借呗审核通过后支付宝的余额才会增加,但借呗和支付宝有可能不是同一个系统,这时候就可以借助MQ完成分布式事务

具体流程如下所示:

1、找借呗借钱

2、借呗借钱审核通过,同步生成借款单

3、借款单生成后,向MQ发送消息,通知支付宝转账

4、支付宝读取MQ消息,并增加账户余额

上图最复杂的其实是如何保障2、3在同一个事务中执行(本地事务和MQ消息发送在同一个事务执行),借款结束后,借呗数据处理就完成了,接下来支付宝才能读到消息,然后执行余额增加,这才完成整个操作。如果中途操作发生异常,例如支付宝余额增加发生问题怎么办?此时需要人工解决,没有特别好的办法,但这种事故概率极低。

6、Seata的架构是什么


Seata事务管理中有三个重要的角色:

1、TC (Transaction Coordinator) -事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。

2、TM (Transaction Manager) -事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。

3、RM (Resource Manager) -资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

如下所示:

7、XA模式的工作流程是什么


xa模式整个工作流程图如下所示:

分为两个阶段:

1、RM一阶段的工作:① 注册分支事务到TC ② 执行分支业务sql但不提交 ③ 报告执行状态到TC

2、TC二阶段的工作:TC检测各分支事务执行状态 ①如果都成功,通知所有RM提交事务 ②如果有失败,通知所有RM回滚事务

3、RM二阶段的工作:接收TC指令,提交或回滚事务

xa模式牺牲了可用性,保证了强一致性

8、AT模型的工作原理是什么


at模式的整个工作流程图如下所示:


1、阶段一RM的工作:① 注册分支事务 ② 记录undo-log(数据快照)③ 执行业务sql并提交 ④报告事务状态

2、阶段二提交时RM的工作:删除undo-log即可

3、阶段二回滚时RM的工作:根据undo-log恢复数据到更新前

at模式牺牲了一致性,保证了可用性

9、TCC模型的工作原理是什么


TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:

1、Try:资源的检测和预留;

2、Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。

3、Cancel:预留资源释放,可以理解为try的反向操作。

Seata中的tcc模型的执行流程如下所示:


1、阶段一RM的工作:① 注册分支事务 ② 执行try操作预留资源 ④报告事务状态

2、阶段二提交时RM的工作:根据各分支事务的状态执行confirm或者cancel

jvm问题

1、JVM的主要组成部分有哪些


JVM主要分为下面几部分

类加载器:负责将字节码文件加载到内存中

运行时数据区:用于保存java程序运行过程中需要用到的数据和相关信息

执行引擎:字节码文件并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎将字节码翻译成底层系统指令

本地库接口:会被执行引擎调用参与字节码的翻译

在这里面最主要的部分是运行时数据区,它又由五部分构成,分别是:堆、方法区、栈、本地方法栈、程序计数器

堆是对象实例存储的主要区域
方法区可以认为是堆的一部分,用于存储已被虚拟机加载的信息,比如常量、静态变量等等
栈是程序方法运行的主要区域,栈里面存的是栈帧,栈帧里面存的是局部变量表、操作数栈、动态链接、方法出口等信息
本地方法栈与栈功能相同,区别在于本地方法栈执行的是本地方法,即一个Java调用非Java代码的接口
程序计数器主要存放的是当前线程所执行的字节码的行号,用于记录正在执行的字节码指令的地址


2、堆栈的区别是什么


堆和栈都是JVM的主要组成部分,不同点在于:

栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的
堆会GC垃圾回收,而栈不会
栈内存是线程私有的,而堆内存是线程共有的
两者异常错误不同,栈空间不足:java.lang.StackOverFlowError,堆空间不足:java.lang.OutOfMemoryError


3、JVM的类加载器有哪些


类加载器的主要作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。根据各自加载范围的不同,主要划分为四种类加载器:

启动类加载器(BootStrap ClassLoader):用于加载JAVA_HOME/jre/lib目录下的类库

扩展类加载器(ExtClassLoader):用于加载JAVA_HOME/jre/lib/ext目录中的类库

应用类加载器(AppClassLoader):用于加载classPath下的类,也就是加载开发者自己编写的Java类

自定义类加载器:开发者自定义类继承ClassLoader,实现自定义类加载规则

4、什么是双亲委派模型


双亲委派模型是Java中的一种类加载机制。

在双亲委派模型中,类加载器之间形成了一种层次继承关系,从顶端开始依次是:启动类加载器->扩展类加载器->应用类加载器->自定义类加载器

当一个类加载器需要加载某个类时,它首先会委派给其上层类加载器去尝试加载该类。如果父类加载器无法加载该类,子类加载器才会尝试加载。

这种层次关系形成了一个从上到下的委派链。

双亲委派模型的主要目的是保证Java类的安全性和避免类的重复加载。当一个类加载器收到加载请求时,它会首先检查自己是否已经加载了该类。

如果已经加载,则直接返回该类的Class对象;如果未加载,则将加载请求委派给父类加载器。

父类加载器也会按照同样的方式进行检查,直到顶层的启动类加载器。如果顶层的启动类加载器无法加载该类,那么子类加载器会尝试自己加载。

这样可以避免同一个类被不同的类加载器加载多次,确保类的唯一性。

双亲委派模型的优势在于能够保证类的一致性和安全性。

通过委派链的机制,可以避免恶意代码通过自定义的类加载器加载替换系统核心类,从而提高了Java程序的安全性。

此外,通过双亲委派模型,可以实现类的共享和重用,减少内存占用和加载时间,提高了系统的性能。

5、说一下类加载器的执行过程


类从被加载到虚拟机内存中开始,直到卸载出内存为止,整个生命周期包括了7个阶段:加载、验证、准备、解析、初始化、使用、卸载

加载: 这个阶段会在内存中生成一个代表这个类的java.lang.Class对象
验证: 这个阶段的主要目的是为了确保Class文件包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
准备: 这个阶段正式为类变量分配内存并设置类变量的初始值,注意这里的初始值指的是默认值,而不是代码=后的实际值
解析: 这个阶段将符号引用替换为直接引用,比如方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接引用方法
初始化: 这个阶段是执行类构造器方法的过程,是类加载的最后一步,到了这一步Java虚拟机才开始真正执行类中定义的Java程序代码(字节码)
使用: 这个节点程序在运行
卸载: 这个阶段类Class对象被GC


6、怎么判断对象是否可以被回收


在堆中存放着几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事就是要确定哪些对象是要回收的

JVM认为不被引用的对象就是可以被回收的对象,而它确认对象是否还在被引用的算法主要有两种:引用计数法和可达性分析算法

引用计数法

在对象头处维护一个counter,每增加一次对该对象的引用,计数器自加,如果对该对象的引用失联,则计数器自减

当counter为0时,表明该对象已经被废弃,不处于存活状态,

但是此方法存在问题,假设两个对象相互引用始终无法释放counter,则永远不能GC

可达性分析算法

通过一系列为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链

当一个对象到GC Roots没有任何引用链相连时,则证明该对象是不可用的

可以作为GC Roots的对象一般有栈中引用的对象 、方法区中类静态属性引用的对象以及

7、JVM的垃圾回收算法有哪些


目前JVM中的垃圾回收算法主要有四个,分别是:标记清除算法、标记-整理算法、复制算法和分代收集算法

标记清除算法是将垃圾回收分为2个阶段,分别是标记和清除

它会先使用根据可达性分析算法找到垃圾资源进行标记,然后对这些标记为可回收的内容进行垃圾回收

这种算法的主要不足有两个:

效率问题,标记和清除阶段都要遍历多有对象,并且在GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的

空间问题,对象被回收之后会产生大量不连续的内存碎片,当需要分配较大对象时,由于找不到合适的空闲内存而不得不再次触发垃圾回收动作

标记整理算法也是将垃圾回收分为2个阶段,分别是标记和整理清除

它的第一阶段也是会先将存活的对象先标记出来

不一样的地方在于第二阶段,它会将所有存活的对象向前移动放在一起,然后将无用空间回收,这样就会出现连续的可用空间了

所以它解决了空间碎片问题,但是效率低的问题依旧存在

复制算法,将原有的内存空间一分为二,每次只用其中的一半

在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将当前内存空间清空,交换两个内存的角色,完成垃圾的回收。

这种算法的缺点在于分配2块内存空间,在同一个时刻,只能使用一半,内存使用率较低

分代收集算法,它会将整个堆内存分成几部分空间,每个空间中放入不同类型的对象,然后各自适合的算法回收

在JDK8时,堆被分为了两份:新生代和老年代,默认空间比例为1:2

对于新生代,内部又被分为了三个区域:Eden区,S0区,S1区,,默认空间比例为8:1:1

它的基本工作机制是:

当创建一个对象的时候,这个对象会被分配在新生代的Eden区,当Eden区要满了时候,触发MinorGC

当进行MinorGC后,此时在Eden区存活的对象被移动到S0区,并且当前对象的年龄会加1,清空Eden区

当再一次触发MinorGC的时候,会把Eden区中存活下来的对象和S0中的对象,移动到S1区中,这些对象的年龄会加1,清空Eden区和S0区

当再一次触发YoungGC的时候,会把Eden区中存活下来的对象和S1中的对象,移动到S0区中,这些对象的年龄会加1,清空Eden区和S1区

对象的年龄达到了某一个限定的值(默认15岁),那么这个对象就会进入到老年代中,除此之外,大对象也会直接放入老年代空间

当老年代满了之后,触发FullGC**。**FullGC同时回收新生代和老年代

在上述过程中,新生代中的对象存活率比较低,所以选用复制算法;老年代中对象存活率高,所以使用标记-整理算法

小细节:

当对新生代产生GC:MinorGC,老年代代产生GC:Major GC ,新生代和老年代产生FullGC

Minor GC非常频繁,一般回收速度也很快,Major GC一般会伴随一次Minor GC,Major GC的速度要慢很多,一般要比Minor GC慢10倍

占用内存较大的对象,对于虚拟机内存分配是一个坏消息,虚拟机提供了一个-XX:PretenureSizeThreshold让大于这个设置的对象直接存入老年代

虚拟机给每个对象定义了一个Age年龄计数器,对象在Eden中出生并经过第一次Minor GC后仍然存活,年龄+1,此后每熬过一次Minor GC则年龄+1,

当年龄增加到一定程度(默认15岁),就会晋升到老年代。可通过参数设置晋升年龄 -XX:MaxTenuringThreshold

8、JVM的垃圾回收器都有哪些


JVM中常见的一些垃圾回收器有:

新生代回收器:Serial、ParNew、Parallel Scavenge

老年代回收器:Serial Old、Parallel Old、CMS

整堆回收器:G1

新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低

老年代回收器一般采用的是标记-整理的算法进行垃圾回收

redis问题

1、项目中为什么用Redis


我们项目中之所以选择Redis,主要是因为Redis有下面这些优点:

操作速度快:Redis的数据都保存在内存中,相比于其它硬盘类的存储,速度要快很多
数据类型丰富:Redis支持 string,list,set,Zset,hash 等数据类型,基本满足我们开发中的各种使用场景
使用场景丰富:Redis可用于缓存,消息队列,按 key 设置过期时间,过期后将会自动删除


2、Redis的数据类型有哪些


Redis最常见的数据类型有5种,分别是String、List、Hash、Set、ZSet,下面给您详细介绍一下:

String:简单的 key-value 类型,最大能存储512MB数据。场景:计数、缓存文章标题、微博内容等

List:底层是链表,特点是:增删容易,随机访问困难。场景:发布与订阅或者说消息队列

Hash:类似于Java中的HashMap,适合存储对象。场景:系统中对象数据的存储

Set:是一种无序集合,可以方便的求交、并、差集。 场景:共同关注、共同粉丝、共同喜好等功能

ZSet:相比于set来讲,多了1个权重参数 score,元素会按照score进行排序。场景:各种排行榜,弹幕消息

3、Redis为什么这么快


Redis之所以运行速度比较快,主要是由于这样一些原因:

纯内存操作:Redis的绝大部分请求是纯粹的内存操作,非常快速

单线程:Redis的核心部分是单线程运行的,避免了不必要的上下文切换,也不存在线程切换导致的 CPU消耗

使用 I/O 多路复用模型和非阻塞 IO

什么是 I/O 多路复用

I/O多路复用是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源
目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能
其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器
在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程
1
2
3
4


4、Redis的过期删除策略有哪些


Redis的过期删除策略指的是当Redis中的key过期之后在什么时候进行删除的处理方案,常用的删除策略就两个:

惰性删除:只会在取出 key 的时候才对数据进行过期检查,过期了就删除
定期删除:每隔一段时间抽取一批 key执行删除过期 key 操作
两者相比,定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是定期删除+惰性/懒汉式删除。

5、Redis的内存淘汰策略有哪些


Redis的内存淘汰策略指的是当Redis的内存已经存满,又有新的数据需要保存时的处理方案,官方提供了8种淘汰策略:

no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。
volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
volatile-lfu:从已设置过期时间的数据集中挑选最不经常使用的数据淘汰
allkeys-lru:在所有的数据集中选择最近最少使用的数据淘汰
allkeys-random:在所有的数据集中任意选择数据淘汰
allkeys-lfu:在所有的数据集中选择最不经常使用的数据淘汰


6、Redis的RDB和AOF区别


Redis是一个基于内存的数据存储,为了保证数据安全,需要将内存中的数据备份到磁盘上,官方提供了两种数据持久化的方式,分别是RDB和AOF

RDB采用的是定期更新的方式,它会定期将Redis中的数据生成的快照同步到磁盘上,磁盘上保存的就是Redis的内存快照

优点是数据文件的大小相比于AOF较小,数据恢复速度较快

缺点是比较耗时,存在丢失数据的风险

AOF是将Redis所执行过的所有写指令都记录到磁盘上,在下次Redis重启时,只需要将指令重写一遍就可以了

优点是数据丢失的风险大大降低了

缺点是数据文件的大小相比于rdb较大,而且数据恢复的时候速度较慢

在我们公司是同时开启RDB和AOF 持久化机制的,这样做的好处是:

在Redis重启时先使用AOF日志进行恢复,然后再使用RDB快照进行备份
而且将AOF的appendfsync 参数为 everysec,保证每秒将AOF缓冲区中的写操作同步到 AOF 文件中,提高数据的持久化能力
定期进行RDB快照的备份,以便在需要时进行全量数据的恢复
这样的配置可以充分利用RDB和AOF两种持久化机制的优势,提高数据的可靠性和恢复能力

7、RDB期间可以同时处理写请求吗


Redis在进行RDB期间是可以同时处理写请求的,这得益于Redis使用操作系统的多进程写时复制技术来实现快照持久化

具体来说,就是Redis在持久化时会产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求

当主线程执行写指令修改数据的时候,这个数据就会复制一份副本, 然后修改副本中的数据,RDB结束后,主进程读取这个副本数据写到 RDB 文件

这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响

8、Redis集群有哪些方案


在Redis中提供的集群主要有三种,分别是主从、哨兵和分片集群

主从集群主要用来解决Redis的并发问题,一般是一个主节点负责数据写入,多个从节点负责数据读取,主节点的数据会实时同步给从节点

哨兵集群主要用来解决Redis的高可用问题,哨兵会监控集群中节点的状态,并在主节点出现问题时进行重新选主

分片集群主要用来解决Redis的海量数据存储问题,它要求有多个主节点,然后数据写入的数据会经过计算落到其中一个上

在这个计算的过程中Redis引入了哈希槽的概念,Redis集群有16384个哈希槽,每个 key通过CRC16校验后对16384取模来决定放置哪个槽

而分片集群的每个节点负责一部分 hash 槽,这样就可以计算出一个key会出现在哪个节点上了,查询的时候也是同时的方式来定位即可

9、如何保存Redis数据与MySQL一致


保证Redis和MySQL数据一致性的方案有很多,最常见的有三种

同步双写,即在程序更新完MySQL之后后立即同步更新redis
异步监听,即通过Canal监听MySQL的binlog日志变化,然后再通过程序将变化的数据更新数据到 Redis
MQ异步,即程序在更新完MySQL后,发送一条消息到MQ中,然后在通过一个程序监听MQ,获取到消息,然后更新Redis


10、什么是缓存预热


缓存预热是指系统上线后,提前将相关的缓存数据加载到缓存系统。

避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据。

如果不进行预热,那么 Redis 初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中,对数据库造成流量的压力。

缓存预热解决方案主要有下面几个:

数据量不大的时候,工程启动的时候进行加载缓存动作
数据量大的时候,设置一个定时任务脚本,进行缓存的刷新
数据量太大的时候,优先保证热点数据进行提前加载到缓存


11、什么是缓存穿透, 怎么解决


在我们的项目中会将缓存放到数据库前面,查询的时候先查缓存,缓存有了就不用再去查数据库了,这样可以大大减轻数据库的访问压力

而缓存穿透指的是请求一直在查询一个数据库中不存在的数据,这样缓存中没有,请求就会到达数据库,而数据库也没有,也就没法缓存

所以每一次请求都会直接到数据库中查询,这就极有可能导致数据库被压垮

常用的解决方案有两个:

查询返回的数据为空,仍把这个空结果进行缓存,但过期时间尽量设置稍短一些

使用布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定存在的数据会被这个 bitmap 拦截掉,从而避免了对DB的查询

12、什么是缓存击穿,怎么解决


在我们的项目中会将缓存放到数据库前面,查询的时候先查缓存,缓存有了就不用再去查数据库了,这样可以大大减轻数据库的访问压力

缓存击穿指的是对于一个设置了过期时间的key,在其缓存失效的瞬间,有大量的请求访问这个它,这些请求在缓存找不到就会直接到数据,导致数据库被压垮

常用的解决方案有两个:

使用互斥锁:当缓存失效时,不立即去数据库查询,而是先去获取一把全局锁,那个线程获取到了,就去数据库查询,获取不到的就等待重试查询缓存

修改设置key有效期的逻辑,大体如下:

在设置key的时候,不给它设置过期时间,而是单独设置一个过期时间字段一块存入缓存中

当查询的时候,从redis取出数据后判断时间是否过期,如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据


两种方案对比:

解决方案    优点    缺点
互斥锁    没有额外的内存消耗 ,保证一致性    线程需要等待,性能受影响
逻辑过期    线程无需等待,性能较好    不保证一致性,有额外内存消耗


13、什么是缓存雪崩,怎么解决


在我们的项目中会将缓存放到数据库前面,查询的时候先查缓存,缓存有了就不用再去查数据库了,这样可以大大减轻数据库的访问压力

缓存雪崩指的是大量的key在某一时刻同时失效,这样大量的请求全部转发到DB,DB 瞬时压力过重雪崩

解决方案也很简单,就是在设置key的过期时间的时候,尽量加一些随机值,这样缓存过期时间的重复率就会降低

14、用过Redis的事务吗


Redis中本身是没有事务的概念的,但是他有几个命令组合起来能实现类似于事务的效果。也就是说,Redis事务的本质是一组命令的集合。

这里用到的命令主要有5个,分别是:

MULTI:用来组装一个事务
EXEC:执行一个事物
DISCARD:取消一个事务
WATCH:用来监视一些key,一旦这些key在事务执行之前被改变,则取消事务的执行
UNWATCH:取消 WATCH 命令对所有key的监视


总结说:Redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。Reids中,单条命令式原子性执行的,但事务不保证原子性,且没有回滚。

mysql问题

1、内连接和外连接的区别


内连接和外连接都是数据库进行多表联查时使用的连接方式,区别在于二者获取的数据集不同

内连接指的是使用左表中的每一条数据分别去连接右表中的每一条数据,仅仅显示出匹配成功的那部分

外连接有分为左外连接和右外连接

左外连接: 首先要显示出左表的全部,然后使用连接条件匹配右表,能匹配中的就显示,匹配不中的显示为null
右外连接: 首先要显示出右表的全部,然后使用连接条件匹配左表,能匹配中的就显示,匹配不中的显示为null


2、drop、delete与truncate区别


这个关键字都是MySQL中用于删除的关键字,区别在于:

delete语句执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存以便进行进行回滚操作
drop 主要用于删除数据表、表中的列、索引等结构
truncate 是直接把表删除,然后再重建表结构
这三种方式在效率方面drop 最高、truncate 其次、delete最低,但是drop和truncate 都不记录日志,无法回滚

3、union与union all的区别


union和union all都是MySQL中用于合并多条select语句结果的关键字,它会将前后两条select语句的结果组合到一个结果集合中

区别在于UNION ALL会返回所有结果,UNION会去掉重复的记录

4、char和varchar的区别


char和varchar是MySQL中的字符串类型,区别在于下面几方面:

最大长度:char最大长度是255字符,varchar最大长度是65535个字节
占用长度:char是定长的,不足的部分用隐藏空格填充,varchar是不定长的
空间使用:char会浪费空间,varchar会更加节省空间
查找效率:char查找效率会很高,varchar查找效率会更低
因此我们如果存储固定长度的列,例如身份证号、手机号建议使用char

其它不定长度的建议使用varchar,使用varchar的时候也要尽量让声明长度贴近实际长度

注意:varchar(50)中50的涵义是最多存放50个字符,varchar(50)和varchar(200)存储hello所占空间一样

5、事务的四大特性


事务的四大特性指的是原子性、一致性、隔离性、持久性

原子性:事务是最小的执行单位,不允许分割,同一个事务中的所有命令要么全部执行,要么全部不执行
一致性:事务执行前后,数据的状态要保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的
隔离性:并发访问数据库时,一个事务不被其他事务所干扰,各并发事务是独立执行的
持久性:一个事务一旦提交,对数据库的改变应该是永久的,即使系统发生故障也不能丢失


6、并发事务带来的问题


并发事务下,可能会产生如下的问题:

脏读:一个事务读取到了另外一个事务没有提交的数据
不可重复读:一个事务读取到了另外一个事务修改的数据
幻读(虚读):一个事务读取到了另外一个事务新增的数据


7、事务隔离级别


事务隔离级别是用来解决并发事务问题的方案,不同的隔离级别可以解决的事务问题不一样

读未提交: 允许读取尚未提交的数据,可能会导致脏读、幻读或不可重复读
读已提交: 允许读取并发事务已提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
可重复读: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
可串行化: 所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,该级别可以防止脏读、不可重复读以及幻读。
上面的这些事务隔离级别效率依次降低,安全性依次升高,如果不单独设置,MySQL默认的隔离级别是可重复读

8、数据库三大范式


三大范式是指导设计数据库的原则

第一范式:表中的每一列不能再进行拆分,也就是每一列都应该是原子的
第二范式:一张表只做一件事,不要将多个层次的数据列保存到一张表中
第三范式:数据不能存在传递关系,也就是说可以通过其它字段推出来的字段没必要再存储
在现有的程序设计中认为第三范式是可以不遵守的,也就是通过添加冗余字段,来减少多表联查或计算,我们称为反三范式

9、索引的分类


索引是数据库中用于提供查询效率的一种手段

从物理存储角度上分为聚集索引和非聚集索引

聚集索引指的是数据和索引存储在同一个文件中,InnoDB存储引擎使用的是此类索引存储方式

非聚集索引指的是数据和索引存储在不同的文件中,MyISAM存储引擎使用的是此类索引存储方式

从逻辑角度上分为普通、唯一、主键和联合索引,它们都可以用来提高查询效率,区别点在于

唯一索引可以限制某列数据不出现重复,主键索引能够限制字段唯一、非空

联合索引指的是对多个字段建立一个索引,一般是当经常使用某几个字段查询时才会使用,它比对这几个列单独建立索引效率要高

10、索引的创建原则


索引可以大幅度提高查询的效率,但不是所有的字段都要加,也不是加的越多越好,因为索引会占据磁盘空间,也会影响增删改的效率

我们在建立索引的时候应该遵循下面这些原则:

主键字段、外键字段应该添加索引
经常作为查询条件、排序条件或分组条件的字段需要建立索引
经常使用聚合函数进行统计的列可以建立索引
经常使用多个条件查询时建议使用组合索引代替多个单列索引
除此之外,下面这些情况,不应该建立索引

数据量小的表不建议添加索引
数据类型的字段是TEXT、BLOB、BIT等数据类型的字段不建议建索引
不要在区分度低的字段建立索引,比如性别字段、年龄字段等


11、索引失效的情况


索引失效指的是虽然在查询的列上添加了索引,但是某些情况下,查询的时候依旧没有用到索引,常见的情况有

使用like关键字时,模糊匹配使用%开头将导致索引失效
使用连接条件时,如果条件中存在没有索引的列会导致索引失效
在索引列上进行计算、函数运算、类型转换将导致索引失效
使用 !=、not in、is null、is not null时会导致索引失效
使用联合索引时,没有遵循最左匹配原则会导致索引失效


12、如何知道索引是否失效


MySQL中自带了一个关键字叫explain,它可以加在一个sql的前面来查看这条sql的执行计划

在执行计划中,我们主要观察两列的结果,一列是type,一列是extra

第一个type是重要的列,显示本次查询使用了何种类型,常见的值从坏到好依次为:all、index、range、ref、eq_ref 、const、system

all表示全表扫描数据文件返回符合要求的记录
index表示全表扫描索引文件返回符合要求的记录
range表示检索指定范围的行,常见于使用>,<,between,in,like等运算符的查询中
ref表示两表查询时,驱动表可能返回多行数据,也就是查询条件在主表中是加了一个普通索引
eq_ref表示两表查询时,驱动表只返回一行数据,也就是查询条件在主表中是唯一的
const表示索引一次就能得到结果,一般是使用唯一索引或者主键作为查询条件
system表示表中仅有一行数据,很少见到
我们在优化的时候尽量优化到range级别以上

除了type之外我们需要关注一下extra列,它表示执行状态说明

要保证此列不要出现using filesort、using temporary等使用临时表或外部文件的情况

如果出现using index最好了,它表示列数据仅仅使用了索引中的信息而没有回表查询

13、MyISAM和InnoDB的区别


MyISAM和InnoDB是目前MySQL中最为流行的两种存储引擎,它们的区别有这几方面:

MyISAM不支持事务,每次操作都是原子的;InnoDB支持事务,支持事务的四种隔离级别
MyISAM不支持外键,InnoDB支持外键
MyISAM仅仅支持表级锁,即每次操作是对整个表加锁;InnoDB支持行级锁,因此可以支持写并发
MyISAM属于非聚集性索引,它的数据和索引不在同一个文件中;InnoDB属于聚集性索引,它的数据和索引在同一个文件中
MyISAM中主键和非主键索引的数据部分都是存储的文件的指针;InnoDB主键索引的数据部分存储的是表记录,非主键索引的数据部分存储的是主键值


14、查询语句执行流程


一条查询语句到达MySQL数据库之后,数据库中的各个组件会按照顺序执行自己的任务

首先是连接器,他会负责建立连接、检查权限等操作
连接成功之后,会查询缓存,如果缓存中有结果会直接返回;如果缓存中没有结果,会将sql交给分析器处理
分析器负责检查sql的词法、语法,如果没有问题,再将sql交给优化器处理
优化器会决定用哪个索引,决定表的连接顺序等,然后将优化之后的sql交给执行器
执行器根据存储引擎类型,调用存储引擎接口
存储引擎负责最后数据的读写


15、索引的数据结构是什么


在MySQL中索引使用的数据结构是B+Tree,B+树是基于B树的变种,它具有B树的平衡性,而且树的高度更低

B+树非叶子节点不存在数据只存索引,因此其内部节点相对B树更小,树的高度更小,查询产生的I/O更少
B+树查询效率更高,B+树使用双向链表串连所有叶子节点,区间查询效率更高
B+树查询效率更稳定,B+树每次都必须查询到叶子节点才能找到数据,而B树查询的数据可能不在叶子节点,也可能在,这样就会造成查询的效率的不稳定


16、数据库中的锁有哪些


MySQL中的锁从不同维度可以分为不同的种类

从锁的粒度上可以分为表锁和行锁

表锁指的是会锁定修改数据所在的整个表,开销小,加锁快,锁定粒度大,发生锁冲突概率高

行锁指的是会锁定修改数据所在的行记录,开销大,加锁慢,锁定粒度小,发生锁冲突概率低

从锁的排他性上分为共享锁和排他锁

共享锁指的是当一个事务针对同一份数据加上共享锁之后,另一个事务也可以再往上加一把共享锁,也可以读数据,但是不能改

对索引列加共享锁,锁定的是一行数据;对非索引列加共享锁,锁定的是整表数据

排他锁指的的是当一个事务针对同一份数据加上排他锁之后,另一个事务只能读数据,不能改,也不能再上其它任务锁

还有两种概念上的锁是悲观锁和乐观锁

悲观锁是指一个事务在修改数据的时候,总是认为别人也会修改此数据,所以强制要使用锁来保证数据安全

乐观锁是指一个事务在修改数据的时候,总是认为别人不会修改此数据,因为不加任何锁

这种情况下万一在当前事务修改的时候,数据被其它事务也修改了,机会出现问题,此时常用的方案是:

给数据表中添加一个version列,每次更新后都将这个列的值加1,读取数据时,将版本号读取出来

在执行更新的时候,会先比较版本号,如果相同则执行更新,如果不相同,说明此条数据已经发生了变化,就放弃更新或重试

17、MySQL日志类型


MySQL的很多功能都是依靠日志来实现的,比如事务回滚,数据备份,主从复制等等,常见的日志有下面几个

binlog归档日志

负责记录对数据库的写操作,一般用在主从复制过程中记录日志,从库拷贝此日志做重放实现数据同步

redolog重做日志

用于确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘

在重启 mysql 服务的时候,根据 redo log 进行重做,从而达到事务的持久性这一特性

undo log 回滚日志

保存了事务发生之前的数据的一个版本,可以用于回滚

18、MySQL主从复制的流程


主从复制用于MySQL主从集群的主节点向从节点同步数据,主要是依靠MySQL的binLog实现的,大体流程分为三步:

Master 主库在事务提交时,会把数据变更记录在二进制日志文件 BinLog中
从库读取主库的二进制日志文件 Binlog ,写入到从库的中继日志 RelayLog
slave重做中继日志中的事件,将改变反映它自己的数据


19、谈谈你对sql的优化的经验


我在企业中优化Sql大体分为三步:

查找问题sql,主要手段是开启mysql的慢查询日志,它会将执行时间较长的sql记录记录下来
找到sql之后,我会分析出现问题的原因,原因很多,主要字段类型选择错误、sql语句效率低、索引失效等等
根据问题不同,我会再去定具体的解决方案
简单给您说几个常见的把

确定选择的引擎是否合适

myisam适合于查询为主,增删较少,无事务要求的数据表

Innodb适用于有事务处理,或者包括很多的更新和删除的数据表

表设计是否合理

单表不要有太多字段,建议在20以内

合理的加入冗余字段可以提高查询速度

确定字段的数据类型是否合适

数值型字段的比较比字符串的比较效率高得多,字段类型尽量使用最小、最简单的数据类型

设置合适的字符串类型(char和varchar)char定长效率高,varchar可变长度,效率稍低,varchar的长度只分配真正需要的空间

尽量使用TIMESTAMP而非DATETIME,尽量设计所有字段都得有默认值,尽量避免null

确定sql的书写是否有的题

SELECT语句务必指明字段名称,避免直接使用select *

SQL语句中IN包含的值不应过多

可以用内连接,就尽量不要使用外连接

使用连接连接查询来代替子查询

适用联合(UNION)来代替手动创建的临时表

表数据比较多的时候是否添加了合适的索引

表的主键、外键必须有索引

经常出现在where子句中的字段,特别是大表的字段,应该建立索引

经常用于排序、分组的字段,应当建立索引

加上索引之后,还应该使用Explain来确认索引是否生效

如果上面的几项都没有问题,那可能就是因为服务器性能或者数据量过大导致的查询慢,此时可以考虑读写分离

也就是我们搭建一个MySQL的主从集群,让1个主节点负责写入数据,多个从节点负责查询数据,已分摊查询压力

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值