面试准备

STAR 法则。对于面试,你可以将这个法则用在自己的简历以及和面试官沟通交流的过程中。

STAR 法则由下面 4 个单词组成(STAR 法则的名字就是由它们的首字母组成):

  • Situation: 情景。 事情是在什么情况下发生的?
  • Task: 任务。你的任务是什么?
  • Action: 行动。你做了什么?
  • Result: 结果。最终的结果怎样?

面试总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
面试官您好,我叫方圆,目前是杭州电子科技大学通信工程专业研二的学生,
本科也是杭电通信工程,在学校的学习期间呢,对通信和计算机的相关专业课程都有所涉及,
熟悉Java语言,对spring boot进行web开发有一定的实践。

读研期间呢,参与了老师的一个横向课题,参与了数梦工厂消息中心项目,
负责开发其中的一个模块,我主要负责的是平台侧消息模板的创建和管理,
还有用户侧消息的管理,包括搜索,查看,标记已读删除等,消息推送等

后来项目结束后,为了丰富自己web开发的相关知识,去学习了Redis缓存的相关知识,
开发了一个类似大众点评的单体web应用,
并且在结束后用Docker部署在自己的虚拟机中,用nginx做了负载均衡的配置

同时呢,我也习惯于整理和分享自己的所学知识,为此我基于hexo 搭建了自己的网站,
主要分享技术学习经历,还有一些日常和遇到的bug问题。
目前记录字数已经超过20W字

因为直接在学习的过程中用到过的云业务比较多,
比如,域名服务,对象存储服务,还有短信服务,当然云端的业务还有很多,
还有我上届的师兄师姐也在华为云部门,我认为以后大家的使用的云端都很多,
会有很大的发展空间,期待能获得这次实习的机会。

为什么学通信的,却转转码开发?

1
2
3
4
5
6
7
首先呢,我高考报专业第一志愿就是计算机,但是差了一两分没去成
1.职业兴趣问题
在通信专业就读时候,发现自己对软件开发等互联网技术有着兴趣,
2.职业发展空间
而且计算机相较于传统通信有着较大的职业发展空间
3.技能转换的自然性质
通信也离不开计算机,同样也学习过很多相同的和专业课,在学习的过程中不会有什么阻碍

DTphere消息中心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
这个消息中心的项目,分为平台侧(管理的人去看的)和用户侧(用户去看的)
在平台侧呢
消息的管理

模版管理
--对不同的渠道,钉钉,微信,短信,邮件设计不同的消息模版
--设计实体关系图,一张表维护不同的消息模版Id
--每个Id作为外键关联一张模板表

渠道管理
--渠道管理负责去配置各个渠道的信息,短信,邮件,企业微信,钉钉机器人等
--引入钉钉,微信等相关依赖,配置用户名密码服务器去做测试链接
用户侧:
全部类型消息的查看
--搜索,查看,批量标记已读未读,删除消息,全部已读等
告警消息
--搜索,查看,标记已读未读,删除消息等
系统消息
--搜索,查看,标记已读未读,删除消息等
审批消息
--搜索,查看,标记已读未读,删除消息等
产品消息
--搜索,查看,标记已读未读,删除消息等

对需要删除的消息:用AOP日志记录
使用yapi.pro那个平台编写接口文档,做接口文档的开发

这个消息中心的项目,分为平台侧(管理的人去看的)和用户侧(用户去看的)

平台侧

  • 消息的管理
1
2
3
--绑定管理,将产品与消息模板绑定,当前产品对应的消息模板
--这里是新建一张绑定关系表,将产品Id和模板Id对应起来
查询模版、查询产品消息、绑定、回显
  • 模版管理
image-20240513091758796
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 --对不同的渠道,钉钉,微信,短信,邮件设计不同的消息模版
--设计实体关系图,一张表维护不同的消息模版Id,
--每个Id作为外键关联一张模板表
--我在开始数据库中创建了五张模板表,分别对应五种消息
--(后来就是五张表维护起来太麻烦了,每次增删改查还要在不同表里
还要创建一个模版表去记录创建好的所有Id)
--优化就是:因为模版表有很多相同的字段,创建一张表整合所有字段,
没有设置的字段,就默认为null
然后再新建五个vo类作为返回给前端的消息实体类,去封装对应模版的信息

不同的消息模板有不同的字段,我根据它们提供的图去在数据库中创建模板
这里是参照了它们官方提供的开发手册
定义字段的时候,选择合适的字段内容来建标
字符串就结合存储的内容来使用char 或者varchar
用户名称邮箱类型的可变,使用varchar
文章内容使用text
邮编长度使用char等等
模版类型这里,因为只有几个嘛,开始用的int,后来该改的tinyint

优化这里
1.分页和摘要
因为text字段过大,表里只存放指针,MySQL还需要二次查询
会影响查询性能,所以我们做了一个分表
将text列分离到单独的扩展表中,用ID字段做一个关联
然后,创建一个varchar(255)字段,来记录text的前255个字符作为摘要
这样在分页查询的时候,文章内容我们就只需要查询这个varchar字段就行
点进去具体查询的时候,就需要关联text表查询text字段了

2.联合索引
因为我们每次查询的未读消息的SQL语句,的where条件是
where user_id = ? and is_read = fasle
所以我们对这表中的两个字段添加复合索引
CREATE INDEX idx_user_id_is_read ON messages (user_id, is_read);
增加查询的效率

3.过期策略
用户去查找太久远的消息的几率不大
所以就去做一个分表,定期将历史消息归档在历史表中
可以按照季度或者月份归档
我们前端点击查看历史消息的时候,才会去访问这张表
  • 渠道管理

​ 下图只是举例,

​ 我们是:邮件、短信、企业微信、钉钉机器人、钉钉联系人等(字段也不同)

1
2
3
4
5
6
7
8
9
10
11
12
--渠道管理负责去配置各个渠道的信息,短信,邮件,企业微信,钉钉机器人等
--引入钉钉,微信等相关依赖,配置用户名密码服务器等

我们负责测试,前端同学将这些参数封装在json中传递
我们后端负责接受这些参数,然后在后台调用依赖
尝试去获取消息,获取成功就测试成功

邮件的话:我们是链接163邮箱,配置服务器,端口,这里是去163官网查看了相关demo
短信:引入它们自己的api,提供的了网关插件地址,去做测试链接
钉钉机器人、联系人、企业微信:都是去官方调用官方API
看它们的示例demo,去做配置
引入了第三方的依赖,去做测试链接
  • 消息分类统计展示
1
2
从不同维度统计及展示消息发送情况
例如根据消息的通知渠道、来源产品、消息分类(告警类型、工单提醒、license提醒)等。

用户侧

主要是对站内信的处理(从它们数据库中获取)

  • 全部类型消息– 告警消息、系统消息、审批消息、产品消息
  • 未读消息
  • 已读消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
这个就是对站内消息的处理
文档中给的需求是,查看,搜索,标记已读,删除,分页查找,批量删除等
并没有给我们接口文档。我们就先在实现的时候,根据功能编写接口文档
我们找了yapi.pro去编写

我们自己定义的规范就是,基于RESTful API定义的
用户侧就/user开头
说明请求参数,路径参数或者是实体参数开头的
定义Result返回值对象,返回标准的响应值(响应码,响应信息,响应数据data等)
数据库的实体参数作为DTO类,响应给前端的不一定是全部的字段
所以创建一个vo实体类,封装专门响应给前端的信息
(这里就用到了Hutul工具类的拷贝方法,不用一行一行复制很麻烦)

在删除的时候,因为涉及到数据库的操作,所以需要记录日志
(这里自定义了@logo注解和切面类@Aspect,用AOP的相关知识)
自定义注解@log和切面类@ASpect
注解表示在切面类中使用切面表达式,@Around,@Before前置循环后置,将注解引入进来
并写入插入数据库的逻辑
这样只需要在方法中加入自定义注解就可以实现记录日志操作的过程

难点:未读消息的推送:
我们一开始这里写的是,轮询
前端写一个定时器,定时的去请求后端未读消息的字段
去遍历数据库去查询,去查询未读的字段

轮询的话其实性能不是太好
后来考虑过其他消息推送的方法
websocket和SSE
websocket是双向通信,但我们不需要客户端向服务端通信
所以选用SSE轻量级的推送

具体的做法就是
客户端去建立连接
服务端创建一个seeEmitter对象
seeEmitter对象实现向客户端主动推送消息
把查询到的消息分页放在一个List集合去做的
客户端收到推送的消息后
刷新页面显示等
(后续详细写一遍博客吧)

Redis的数据类型

以及每种数据类型的使用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
String类型:Redis最常见的数据结构,可以存储任何数据,字符串,整形,浮点型都可以,
不管用哪儿种方式,底层都是以字节数组的形式存储的,适用于绝大多数场景

列表List:相当于Java中的linkedList,列表是链表结构,
可以做简单的消息队列功能,但一般不用Redis消息队列

集合分为集合和有序集合,

集合set:没有重复元素的集合嘛,一般用于点赞,我们维护一张点赞的表,
每次用户请求点赞的时候,我们先用StringRedisTemplete.opsForSet()
得到操作Redis中set集合的接口,
调用其中的isMember(),去判断点赞是否存在,
不存在就用add加入,再跟新到数据库,存在就返回已经点赞

共同好友也是,在关注的时候,把当前ID的关注列表以set形式存储到Redis中,
然后,用set求交集的interset()
就可以求得共同的好友交集,

有序集合sorted set:相对于集合set,是有顺序的,
每个元素都会关联一个权重,按照权重进行排序,
我主要用在点赞的排行傍等,使用的权重是时间,点赞时间早的会排序在前面,
用ZSET的range(key,0,4)方法得到排行前几的,
当然不同的排行傍会有不同的权重,可以是分数或者其他,使用权重排序的方式是不变的


哈希hash:键值对,适合缓存对象的存储,
因为我们Redis本来就是以键值对key-value的形式存储的,
一般hash存储用在value属性中,value属性存储的也是一对键值对

位图bigMap:用于存储二进制数据,可以位运算,统计用户签到比较方便

地理位置GEO:用于存储地理位置的数据结构,我们在数据库中将存放地理位置坐标的位置信息用stringRedisTemplete.opsForGeo接口的add方法存储到Redis中
,再调用Geodistance查找距离,GeoRadius查询一定范围内的餐厅
(这个需要先把商铺位置数据存储到Redis中去)

HyperLogLog:基于基数的数据结构,支持对大量元素的去重统计,占用和误差很小,
用于网站的访问统计等,底层是概率估算,
我们首先要在线程池里获得当前用户的唯一标识,
针对访客数,和访问量选取不同的标识
针对访客数,因为是不可重复的,可以选用唯一账户ID等
针对访问量,可以重复,当前线程中拿到的cookie即可


项目具体功能的实现逻辑

1.短信验证码的发送,登录、注册、校验

1
2
3
4
5
6
7
8
9
10
发送短信验证码:对手机号进行格式校验,然后随机生成验证码,
引用阿里云的依赖并做配置文件的配置,然后响应给前端

登录:得到手机号和验证码之后,去判断验证码是否正确,正确就登录成功,
再去数据库中判断是否存在,不存在就创建一个用户,保存到数据库中,
同时用uuid生成一个唯一的token作为key去在Redis中存放用户数据,设置一个过期时间

校验登录状态是:设置一个拦截器prehandle,去拦截登录请求,
获取token,去Redis中判断token是否存在,如果存在的话,就保存当前用户到线程池当中,
这样我们后续在其他地方获取当前用户信息就只需要在线程池中获取即可
1
2
3
4
5
6
7
cookie和session会话跟踪技术,去管理用户的状态,因为HTTP响应是无状态的,
每次请求都是独立的,下次的请求不会携带上次的数据,所以需要会话跟踪技术
cookie是客户端会话跟踪技术,存储数据在客户端浏览器中

session是服务端会话跟踪技术,数据存储在服务端中
底层通过cookie发送,只需要发送JessieId就可以了,在服务端就可以找到对应Id所在的当前数据
相比于cookie会更安全一些

2.Redis分布式锁的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
缓存穿透:就是大量请求的数据在缓存和数据库中都不存在,就会造成给数据库请求压力大的情况

解决方法有:
缓存空对象在缓存中
布隆过滤:就是在Redis和客户端之间再加了一层布隆过滤器,存在则通过,不存在则拒绝,
布隆过滤器是用hash思想实现的,它底层是一个庞大的二进制数组
来了一个key之后对这个key取模存入数组
再来就可以判断它key是否存在了
当然有hash的地方就不可避免有hash冲突,但因为它底层数组够大,冲突不超过5%

缓存击穿:缓存击穿就是,热点击穿嘛,热点key失效,很多线程来重建key,造成数据库的压力
解决方法就是
1.互斥锁,最先来的线程获取锁,去执行key的重建,其他的线程就会被阻塞
2.逻辑过期:就是存储一个过期的时间的字段到缓存中,但我们并不设置过期时间
当请求达到的时候,从缓存中获取数据,去判断一下这个过期字段,若过期则认为失效
失效的时候,就开启另外一个线程,去更新缓存的逻辑过期时间
当前线程就返回旧的数据
利弊:互斥锁保证了数据的一致性,但是会收到锁竞争的影响,考虑死锁的问题
逻辑过期保证了高的可用性,但是可能会出现数据不一致的问题

缓存雪崩:就是我们设置key的时候,设置了一样的过期时间,导致一起失效,数据直接打到数据库的时候
缓存雪崩与击穿不同的是,击穿是一个热点key失效,雪崩是很多key通用失效
解决方法就是:在原有的失效时间的基础上,随机增加几分钟的随机值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
分布式锁的实现:简单来说就是用setnx这个方法,去在Redis中设置锁,
如果插入key成功别人就获取不到,相当于加锁
setex获取锁的过期时间,最后在完成业务逻辑之后,再去删除释放锁

因为执行逻辑和删除是有时间差的,为了确保锁的一致性,防止极端宕机情况,
最好使用lua脚本去执行锁的创建和删除
lua脚本在Redis中执行是单线程的,所以一定会确保执行的时候的原子性

当然自己实现的还是会有很多考虑不到的地方,比如可重入啊,可靠啊等等
后面都是引入Redis依赖,用redisson的分布式锁实现的
底层会有看门口机制解决死锁的问题,死锁就是业务逻辑异常导致锁无法释放的问题
看门狗机制会在获取锁成功后启动任务,更新锁的过期时间,避免死锁的发生

乐观锁和悲观锁:
悲观锁:假定一定会发生多线程的问题,就只允许单线程执行,比如Synchronized关键字
乐观锁:允许多线程,但是会有一个版本号,只是在更新数据的时候,
判断是否其他线程对数据进行了修改,是的话重试即可

3.lua脚本实现高并发环境下的一人一单和线程安全问题

1
2
3
4
5
6
7
8
9
就是在lua脚本中去定义本地的处理逻辑
检查本地的数量是否足够
判断用户是否已经下单
再去减库存
返回操作结果

rabbitMQ消息队列:
生产者去创建一个交换机,交换机负责投递消息到哪儿一个队列,队列再把消息传给消费者处理
用注解@RabbitListener去配置

4.点赞,共同关注,排行榜

5.地理位置下的店铺查询

6.统计访问量和访客量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
集合分为集合和有序集合,

集合set:没有重复元素的集合嘛,一般用于点赞,我们维护一张点赞的表,
每次用户请求点赞的时候,我们先用StringRedisTemplete.opsForSet()
得到操作Redis中set集合的接口,调用其中的isMember(),
去判断点赞是否存在,不存在就用add加入,再跟新到数据库,存在就返回已经点赞

共同好友也是,在关注的时候,把当前ID的关注列表以set形式存储到Redis中,
然后,用set求交集的interset()
就可以求得共同的好友交集,

有序集合sorted set:相对于集合set,是有顺序的,
每个元素都会关联一个权重,按照权重进行排序,
我主要用在点赞的排行傍等,使用的权重是时间,
点赞时间早的会排序在前面,用ZSET的range(key,0,4)方法得到排行前几的,
当然不同的排行傍会有不同的权重,可以是分数或者其他,使用权重排序的方式是不变的


哈希hash:键值对,适合缓存对象的存储,
因为我们Redis本来就是以键值对key-value的形式存储的,
一般hash存储用在value属性中,value属性存储的也是一对键值对

位图bigMap:用于存储二进制数据,可以位运算,统计用户签到比较方便

地理位置GEO:用于存储地理位置的数据结构,
我们在数据库中将存放地理位置坐标的位置信息用
stringRedisTemplete.opsForGeo接口的add方法存储到Redis中,
再调用Geodistance查找距离,GeoRadius查询一定范围内的餐厅
(这个需要先把商铺位置数据存储到Redis中去)

HyperLogLog:基于基数的数据结构,支持对大量元素的去重统计,占用和误差很小,
用于网站的访问统计等,底层是概率估算,
我们首先要在线程池里获得当前用户的唯一标识,
针对访客数,和访问量选取不同的标识
针对访客数,因为是不可重复的,可以选用唯一账户ID等
针对访问量,可以重复,当前线程中拿到的cookie即可

1
2
3
开始使用了session做登录校验,从当前线程里获取到session但是session在分布式中不能共享,所以
集成阿里云的短信服务,就是引入了阿里云的依赖,先是对,做了配置文件的配置,

Redis的相关面试题

Redis和MySQL如何做读写一致性

1
2
3
1.缓存过期嘛,定期的从sql中加载最新的数据
2.做数据更改时候,不直接更新Redis而是删除缓存,下次访问时候未命中再更新
3.最终一致性:使用消息队列异步处理

Redis持久化是怎么做的呢

1
2
3
4
两种持久化方案:
1.快照文件,把Redis存储的数据保存到磁盘上,方便宕机的时候做数据恢复
2.追加文件,Redis操作写操作的时候,会存储到这个文件中去

Redis的数据过期策略

1
2
3
惰性删除:一般不管,我们使用到key后,如果判断过期再删除
定期删除:每隔一段时间对key进行检查,定期清理
一般Redis中的过期删除策略,是结合这两部分来的

Redis淘汰策略

1
很多,一把用LRU,对最近少使用的key淘汰

Redis集群

1
2
3
主从复制:数据从一个主节点复制到其他节点
哨兵模式:哨兵作为独立的节点监控集群,发生故障时候执行故障转移
分片模式:将数据分片到多个节点上

Linux常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
使用Redis和
文件操作:ls,ll,
进入目录:cd.. cd/ cd
显示路径:pwd
创建目录 mkdir
删除:rf
复制:cp
移动:mv

tar -zxvf解压
ping
ipconfig
sudo
等等

项目部署docker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
使用docker,我们需要将项目打个包为镜像,镜像包含应用的本身,也包含运行的环境依赖等等
docker在运行的时候会创建一个容器,运行我们的镜像文件

使用docker很方便
我用docker在我的虚拟机中创建运行了
MySQL
Redis
rabbitmq
openjdk
nginx
注意的是:Redis和MySQL需要进行目录的挂载
将容器内的目录映射到外边来
这样我们删除创建容器的时候,数据不会丢失

并且将自己的项目文件用Maven打包,构建dockerFile构建镜像
然后再执行构建镜像即可

nginx

1
2
3
4
启动nginx,在配置文件中修改参数
监听我程序运行的端口80
然后反向代理给启动的两个服务:8080和8081
选择负载均衡的方法有:轮询,最少连接,哈希等

数据库MySQL

数据库事务

1
2
3
4
5
6
7
8
9
10
11
一系列的数据库操作被当作一个单独的逻辑单元,要么全部执行,要么全部回滚
数据库事务是为了确保事务的完整性和一致性
数据库事务由四个属性组成ACID
原子性:要么全部执行,要么全部回滚
一致性:事务执行前后,数据库的数据处于一致状态
隔离性:每个事务独立于其他事务
持久性:事务执行成功,就是持久的

在sql中使用 start Transaction作为事务的开启,rollback回滚,commit提交
在spring框架中,在service层使用注解@Transactional开启事务

索引:

1
2
3
4
5
6
7
8
9
10
11
12
索引是一种特殊的数据结构,用于加快数据库中数据的查询速度
类似于书中的目录,可以帮助快速查找目录

使用create index ...去为字段创建索引
一般来讲是where子句中频繁出现的列,在表的特定列上创建索引

主键是会自动创建索引的
索引的底层是通过B+树来存放的,我们创建索引时候
会将当前字段插入B+树
B+树是一个多路平衡查找树,
所有叶子节点包含全部的键相互连接,方便查找
非叶子节点只做索引,不含实际数据

1
2
3
4
在并发访问时候,解决数据一致性和有效性的机制
全局锁:对整个数据库加锁,在备份的时候加锁 flush tables with readlock
表级锁:锁表 lock tables..
行级锁:锁行 lock

sql优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1.建标的时候选择合适的类型
2.使用索引
3.sql语句的编写
尽量避免select*的情况,查询具体的字段
尽量少用子查询,用join关联查询代替
尽量不使用or,导致索引失效,分开几条sql写
执行的时候,批量插入,而不是一条一条插入,分批删除
避免索引失效的写法
4.主从复制
5.读写分离

建标选择合适的类型
参考的是阿里云的开发手册,定义字段的时候,选择合适的字段内容来建标
字符串就结合存储的内容来使用char 或者varchar
用户名称邮箱类型的,使用varchar
文章内容使用text
邮编长度使用char等等

sql的执行顺序

1
2
3
4
5
6
7
8
9
10
select * from where groupby having orderby limit

先执行from语句,查询确定查的表
再是链接条件,join on
再是where 过滤符合条件的
再groupby分组
having分组后过滤
最后排序,limit输出

where是分组前过滤,having是分组后过滤

spring Web框架

spring框架

1
2
3
4
spring是Java开发的框架,有很多模块
spring框架的web开发模块,就是springMVC
springMVC + spring framework + Mybatis 就是俗称SSM
springboot整合了ssm,快速开发

IOC控制反转

1
2
3
4
5
是一种设计模式,将原本在程序中手动创建对象的控制权,交给spring框架来管理
IOC容器就是工厂一样,当我们需要创建一个对象的时候
只需要用注解@Autowired,而不需要考虑对象是如何创建出来的

Bean指的是被IOC容器所管理的对象

AOP面相切面编程

1
2
3
4
5
6
7
8
9
10
11
12
13
AOP是spring的核心框架之一,它在程序运行时动态将额外的行为
比如记录日志操作,事务处理,方法运行时间等功能,额外的插入到代码中

它是通过动态代理的方式实现的
实现proxy类的方法,生成代理对象,在其他地方通过去通过代理对象调用方法

我在项目中用AOP的实现过程,对数据库修改操作的日志记录的实现过程
创建操作日志的表格嘛,操作人Id,时间方法参数等
根据表格创建实体类
自定义注解@log和切面类@ASpect
注解表示在切面类中使用切面表达式,@Around,@Before前置循环后置,将注解引入进来
并写入插入数据库的逻辑
这样只需要在方法中加入自定义注解就可以实现记录日志操作的过程

spring框架用到了哪儿些设计模式

1
2
3
4
5
6
7
8
单例模式:spring中的bean是单例的,通过IOC容器对Bean管理,保证整个应用只有一个Bean实例
工厂模式:springIOC负责容器的创建和管理实例,可以将IOC看做是一个工厂
我们不需要知道对象是如何创建的
只需要用注解直接注入对象就行

代理模式:spring的AOP面相切面编程就用到了代理模式
实现proxy类的方法,生成代理对象,在其他地方通过代理对象去调用方法

spring事务

1
2
@Trancsacyional注解
事务的传播,如果当前有事务,就加入该事务,如果没有,就创建新的事务

spring的常用注解

1
2
3
4
5
6
7
8
9
10
11
12
13
最先就是启动注解@SpringBootApplication了,里面包含三个注解
然后就是三层架构的
@RestController @Service @MApper

在controller层
去处理请求HTTP相关的注解
@GetMapping @PostMapping @DeleteMapping等
传值相关的注解
@RequestBody @PathVariable @RequestParam等
mapper层的数据库注解
@Insert @Select @Delete 等等

还要通用的 @AutWired @configuration @Transcactional等等

mybatis 和mybatisPlus的联系区别

1
2
3
4
5
6
7
8
9
mybatis是用Java操作数据的的持久层框架,主要使用xml或者注解来配置sql语句
使用时,会创建数据库连接池,解决了资源重用,提示响应

MP内置了增删改查,通过扫描实体类,基于反射获取实体类的信息,作为数据库的信息表
一般来讲,单表的增删改查就有MP实现
多表的联合查询就没法用MP了,用mybatis去写

MP自带了分页查询插件,不用去引入第三方的依赖,很方便
创建一个Page<>对象,然后调用selectPage()方法

Java基础:

Java虚拟机

是运行Java字节码的运行环境,它使得Java具有跨平台的能力

Java从源代码到运行的过程如下:Java文件通过编译成.class 文件,然后交给JVM,字节码文件包含JVM的可运行指令集合,JVM在运行的时候,会判断是否是热点代码,是的话使用JIT编译器,JIT是运行时编译器,会将字节码对应的机器码保存起来,下次可以直接使用

Java程序转变为机器代码的过程

编译型编译型语言open in new window 会通过编译器open in new window将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。

解释型解释型语言open in new window会通过解释器open in new window一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。

Java和c,c++ 的区别

相同:都是面向对象的语言,封装继承多态

封装:将对象和属性都封装在一个类中,影藏内部复杂性,提供公共的get,set方法来设置,高内聚,低耦合,

继承:extends,子类可以继承父类的方法和属性,不修改父类方法的情况下,添加自己的新成员方法,子类可以通过super关键字调用父类的方法

多态:同一操作对不同的对象,有不同的解释,就是多态,编译型多态,重载,运行时多态,重写

不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
java 是解释性的语言,运行过程为:java 编译后生成字节码文件,然后在 java 虚拟机 JVM 中解释运行,

C++ 编译型语言,编译后直接生成二进制的字节码文件,所以 C++ 运行速度快,但是 java 可以移植

java 中没有指针,提供了数组和集合这样的类和方法去操作,使得程序更加安全

java 中没法实现多重继承,只能实现多个接口来达到与 C++ 中多重继承的作用

C++ 中,经常需要去 malloc 去分配和释放内存,java 中有垃圾回收机制,会自动释放内存

C++ 支持运算符的重载,java 不支持

C++ 更接近底层,允许更多的底层控制,java 隐藏了更多的底层细节,提供了丰富的库和内置功能

位移

1
2
3
4
5
`<<` :左移运算符,向左移若干位,高位丢弃,低位补零。`x << 1`,相当于 x 乘以 2(不溢出的情况下)。

`>>` :带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补 0,负数高位补 1。`x >> 1`,相当于 x 除以 2。

`>>>` :无符号右移,忽略符号位,空位都以 0 补齐。

包装类

1
使基本数据类型有类的特征,而构建的包装类,每个基本数据类型都有包装类,我们可以将基本数据类型转为对象使用,在Java中,自动装箱和拆箱机制使得基本数据类型和其对应的包装类可以自动转换,使得程序编写更加便捷。

浮点数

1
精度丢失:计算器底层是用二进制表示小数的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。

静态变量

1
2
3
4
5
就是被static修饰的变量,可以被所有的类所有的实例所共享,就是只会被分配一次内存,可以节省内存空间,

public修饰的静态变量可以通过类的名称.来访问,

静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。

可变长参数

1
2
3
调用方法可循允许在传入不定长度的参数,底层是通过数组来接受

方法重载的时候,会优先匹配固定参数的,再匹配可变参数的

抽象类和接口

1
2
3
4
5
抽象类是一个类,只能被继承,不能被实现,类似于is-a的关系

接口中的方法都是抽象的,abstract只不过被省略了,实现接口必须提供具体的实现方法,类似于has-a关系

抽象类中可以包含具体的方法,也可以包含抽象的方法

==和equals()

1
2
3
4
5
6
7
基本数据类型来说,==比较的是值

引用数据类型来说,==比较的是内存的地址

一般类会重写equals()方法,比较的是对象的属性是否相等,而不是地址值

`String` 中的 `equals` 方法是被重写过的,比较的是 String 字符串的值是否相等。 `Object` 的 `equals` 方法是比较的对象的内存地址。

hashcode()

1
2
3
4
5
为什么要有hashcode():我们把对象加入hashset或者HashMap,会先计算对象的hashcode值来判断插入的位置,一般来说是取模的操作,如果没有相同的哈希值,就直接插入,如果有相同的hash值,会调用equals去比较对象是否相等,如果相同,不插入,这样做的好处是,大大减少了equals比较的次数,提高效率



如果重写equals时候,没有重写hashcode,就会导致两个相等的对象,hash值不相等,造成set或者map中村子相等元素,

String,StringBuffer,StringBuilder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
String不可变字符串:底层被final修饰

StringBuffer:被synchronized修饰,线程安全

StringBuilder:线程不安全

**+**

底层是通过StringBuilder.append().toString()实现的

字符串常量池

是Java虚拟机JVM为了减少内存消耗针对String类开辟的区域,主要为了避免字符串重复创建

**唯一性**:字符串常量池中的字符串对象是唯一的,即相同内容的字符串在常量池中只有一个实例。

使用 `new` 关键字创建的字符串对象会被保存在堆内存中的普通对象区域,不会放入字符串常量池。

异常

1
2
3
4
5
6
7
Java类中,所有的异常都有一个祖先,Throwable类,有两个子类,Exception和Error类

常用异常:runtimeException等

全局异常处理器:在Springboot中,使用@ControllerAdvice表示是全局异常处理器

使用@ExceptionHandler,表示统一补货的异常

泛型

ArrayList list = new ArrayList<>();

表示ArrayList对象只能传入Integer类型的对象,传其他的会报错

泛型类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{

private T key;

public Generic(T key) {
this.key = key;
}

public T getKey(){
return key;
}
}

//实例化泛型类
Generic<Integer> g = new Generic<Integer>(123456);

泛型接口:

1
2
3
4
public interface Generator<T> {
public T method();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//实现泛型接口,不指定类型
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}

//实现泛型接口,指定类型
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method() {
return "hello";
}
}

泛型方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}

//使用

// 创建不同类型数组:Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray );
printArray( stringArray );

项目里哪儿里用到泛型

自定义返回结果Result 通过参数T,具体的返回指定的数据结构类型

反射

反射是指,在运行时,动态的获取类的信息以及操作类的属性,方法,和构造函数的能力

通过反射,我们可以在运行时检查类,实例化对象,调用方法,访问字段等,

而不需要在编译的时候,获取类的具体信息,

反射提供了一种很强大的机制,来操作类和对象

1
2
3
4
5
6
反射是Java中提供的一种强大的操控类和对象的机制
我们不需要再编译的时候去获取类的相关信息
而是在程序运行的时候
通过获取Class对象
动态的获取类的信息,比如他的属性,方法,成员变量,字段等等
我们可以去实例化对象,调用方法,访问字段等等
1
2
3
4
5
6
平时写业务代码一般用不到反射,但是Spring框架里大量用到反射机制

注解:用反射来读取类,方法,字段上面的**注解**,并且根据注解定义执行的逻辑等

依赖注入DI:通过注解@Autowired,使用反射,来创建并且实例化对象
AOP面向切面编程:使用反射来生成代理对象,并通过反射调用原始方法,实现比如,事务管理,日志记录,消耗时间检测等

具体怎样调用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
获取类的大Class对象,Class对象将一个类的全部方法,变量等信息告诉我,再调用Class的对象的方法来创建对象
想要使用反射,我先要得到class文件对象,其实也就是得到Class类的对象
Class类主要API:
成员变量 - Field
成员方法 - Constructor
构造方法 - Method
获取class文件对象的方式:
1:Object类的getClass()方法
2:数据类型的静态属性class
3:Class类中的静态方法:public static Class ForName(String className)
--------------------------------
获取成员变量并使用
1: 获取Class对象
2:通过Class对象获取Constructor对象
3:Object obj = Constructor.newInstance()创建对象
4:Field field = Class.getField("指定变量名")获取单个成员变量对象
5:field.set(obj,"") 为obj对象的field字段赋值
如果需要访问私有或者默认修饰的成员变量
1:Class.getDeclaredField()获取该成员变量对象
2:setAccessible() 暴力访问
---------------------------------
通过反射调用成员方法
1:获取Class对象
2:通过Class对象获取Constructor对象
3:Constructor.newInstance()创建对象
4:通过Class对象获取Method对象 ------getMethod("方法名");
5: Method对象调用invoke方法实现功能
如果调用的是私有方法那么需要暴力访问
1: getDeclaredMethod()
2: setAccessiable();

动态代理

简单来说就是:编写InvocationHandler实现类、调用Proxy.newProxyInstance()方法生成代理对象,代理对象在方法调用时会委托给InvocationHandler实现类中的invoke()方法处理额外逻辑。

再简单:编写实现类,new生成一个代理对象,调用调离对象的invoke()实现原有方法的逻辑

1
2
3
4
5
6
1.定义接口:首先需要定义一个接口,它定义了需要被代理类所实现的方法。
2.编写实际类:实现上述接口的一个或多个实际类。
3.编写InvocationHandler实现类:创建一个实现InvocationHandler接口的类,该类将负责拦截对代理对象方法的调用,并在必要时执行额外的逻辑。
4.调用Proxy.newProxyInstance()方法:使用Proxy.newProxyInstance()方法来创建动态代理对象。这个方法接受三个参数:ClassLoader,需要代理的接口数组以及一个InvocationHandler对象。
5.动态生成代理类:在调用Proxy.newProxyInstance()方法时,Java会动态生成一个代理类,并在运行时创建一个代理对象。这个代理类会实现所提供的接口,并在方法被调用时,将调用委托给InvocationHandler实现类中的invoke()方法。
6.调用代理对象方法:最后,通过调用代理对象的方法来触发InvocationHandler实现类中的invoke()方法,从而执行额外的逻辑。

序列化和反序列化

1
2
3
4
5
6
7
序列化:就是将数据结构对象-->转换成二进制的文件字节流的过程

返序列化:二进制字节流-->转换成数据结构对象的过程

Spring中使用的是Java原生的序列化和反序列化,

我们一般引入JSON依赖,试用的是JSON的序列化和反序列化

IO流

1
2
3
4
5
6
7
8
9
10
IO:输入、输出流

Java IO流的类都是从四个类派生出来的

- InputStream:字节输入流
- OutputStream:字节输出流
- Reader:字符输入流
- Writer:字符输出流

基本用到的Filereader和FileWriter
1
2
3
4
5
6
7
Reader 常用方法:
read() : 从输入流读取一个字符。
read(char[] cbuf) : 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中,等价于 read(cbuf, 0, cbuf.length) 。
read(char[] cbuf, int off, int len):在read(char[] cbuf) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字符数)。
skip(long n):忽略输入流中的 n 个字符 ,返回实际忽略的字符数。
close() : 关闭输入流并释放相关的系统资源。

1
2
3
4
5
6
7
8
9
Writer 常用方法:
write(int c) : 写入单个字符。
write(char[] cbuf):写入字符数组 cbuf,等价于write(cbuf, 0, cbuf.length)。
write(char[] cbuf, int off, int len):在write(char[] cbuf) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字符数)。
write(String str):写入字符串,等价于 write(str, 0, str.length()) 。
write(String str, int off, int len):在write(String str) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字符数)。
append(CharSequence csq):将指定的字符序列附加到指定的 Writer 对象并返回该 Writer 对象。append(char c):将指定的字符附加到指定的 Writer 对象并返回该 Writer 对象。flush():刷新此输出流并强制写出所有缓冲的输出字符。
close():关闭输出流释放相关的系统资源。

字节缓冲流

IO 操作是很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的 IO 操作,提高流的传输效率。

字节缓冲流这里采用了装饰器模式来增强 InputStreamOutputStream子类对象的功能。

举个例子,我们可以通过 BufferedInputStream(字节缓冲输入流)来增强 FileInputStream 的功能。

1
2
3
4
// 新建一个 BufferedInputStream 对象,字节缓冲环流
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt"));
//字符缓冲流
BufferedWriter writer = new BufferedWriter(new FileWriter("tokens.txt"));

由于字节缓冲流内部有缓冲区(字节数组),因此,字节缓冲流会先将读取到的字节存放在缓存区,大幅减少 IO 次数,提高读取效率。

随机访问流

支持随意跳转到文件的任意位置进行读写的 RandomAccessFile

RandomAccessFile 比较常见的一个应用就是实现大文件的 断点续传 。何谓断点续传?简单来说就是上传文件中途暂停或失败(比如遇到网络问题)之后,不需要重新上传,只需要上传那些未成功上传的文件分片即可。分片(先将文件切分成多个文件分片)上传是断点续传的基础。

JVM虚拟机

Java内存区域

java 虚拟机在在程序的运行过程中,会把它管理的内存划分为不同的区域,包括线程私有的和线程共享

Java 运行时数据区域(JDK1.8 )

线程共享的有:堆,字符串常量池,

线程私有的:程序计数器,虚拟机栈,本地方法栈等

程序计数器

1
2
3
4
5
6
7
8
9
可以看做是当前线程所执行的字节码的行号指令器,字节码解释器工作的时候需要改变这个计数器来执行下一条字节码的指令

作用:

字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。

在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

Java虚拟机栈

1
2
3
用于存储方法的局部变量,部分计算结果以及调用方法的状态,每个线程在运行的时候,都会创建一个独立的Java虚拟机栈

栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。
Java 虚拟机栈

  • 堆是Java虚拟机中掌管内存的最大一块,是所有线程都共享的一块内存区域,主要用来存放对象实例, 
    
    这些对象无需要手动释放内存,交给JVM的垃圾回收机制自动管理
    
    堆的结构:
    
    - 年轻代:新生成的对象,很快会消失
    
    - 老年代:在年轻代足够长后,会移动到老年代,存储长生命周期对象
    
    - 元空间:永久存储,使用本地内存,存储在本地
    
    1
    2
    3
    4
    5



    #### 方法区

当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据

1
2
3
4
5



#### 字符串常量池

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建

1
2
3
4
5



#### 对象创建过程

在Java虚拟机中会对类加载检查,确保所需要的类被夹在,然后,JVM为对象分配内存,初始化为0,设置对象头的信息,并且调用构造函数来初始化对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

**内存的分配机制有两种**

- ```
- 指针碰撞:用过的整合在一边,没用的在另一边,用分界指针,只需要向没用过的地方分配即可
- 空闲列表:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。

**分配内存的并发问题**

创建对象是很频繁的事情,所以的过程必须是线程安全的,

虚拟机采用两种方式保证线程安全:

CAS+失败重试:CAS是一种乐观锁的实现方式,所谓`乐观锁`就是假定没用冲突去完成,有冲突了就重试直到成功为止

TLAB:

为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配

JVM垃圾回收机制

1
2
3
4
5
6
7
8
9
10
11
Java 自动内存管理最核心的功能是 **堆** 内存中对象的分配与回收。

堆的内存空间被分为三个区域,新生代,老年代,永久代

**内存分配和回收原则**

一般对象优先在新生代分配

大对象直接进入老年代

长期存活的对象进入老年代
1
2
3
4
5
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。

大部分情况,对象都会首先在 Eden 区域分配。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间(s0 或者 s1)中,并将对象年龄设为 1(Eden 区->Survivor 区后对象的初始年龄变为 1)。

对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
1
2
3
4
5
6
7
部分收集 (Partial GC):

- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。

整堆收集 (Full GC):收集整个 Java 堆和方法区。

死亡对象的判断方法

引用计数法:

1
2
3
4
5
6
7
给对象中添加一个引用计数器:

- 每当有一个地方引用它,计数器就加 1;
- 当引用失效,计数器就减 1;
- 任何时候计数器为 0 的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间循环引用的问题。

可达性分析算法

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

哪些对象可以作为 GC Roots 呢?

  • - 虚拟机栈(栈帧中的局部变量表)中引用的对象
    - 本地方法栈(Native 方法)中引用的对象
    - 方法区中类静态属性引用的对象
    - 方法区中常量引用的对象
    - 所有被同步锁持有的对象
    - JNI(Java Native Interface)引用的对象
    
    1
    2
    3
    4
    5
    6
    7



    #### 引用类型总结

    将引用分为强引用、软引用、弱引用、虚引用四种

    1.强引用(StrongReference) 以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

2.软引用(SoftReference)
如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。

3.弱引用(WeakReference)
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

4.虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

### 垃圾收集算法

- ```
#### 标记清除算法

首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。

1. **效率问题**:标记和清除两个过程效率都不高。
2. **空间问题**:标记清除后会产生大量不连续的内存碎片

#### 复制算法

为了解决标记-清除算法的效率和内存碎片问题,复制(Copying)收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。在垃圾回收时,将活动对象复制到未使用的那块内存中,然后清理正在使用的内存块。这种方式适用于年轻代,因为它可以快速清理大量的短命对象。

- **可用内存变小**:可用内存缩小为原来的一半。
- **不适合老年代**:如果存活对象数量比较大,复制性能会变得很差。

#### 标记整理算法

在标记阶段后,不是简单地清除未标记对象,而是将所有存活的对象压缩到内存的一端,减少碎片并清理剩余空间。由于多了整理这一步,因此效率也不高,适合老年代这种垃圾回收频率不是很高的场景。

#### 分代收集算法

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

#### 垃圾收集器

**如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。**

Java提供了多种垃圾回收器,每种回收器都适用于不同类型和负载的应用场景:

- **串行回收器(Serial GC)**:单线程回收,适用于小型堆和单核处理器。
- **并行回收器(Parallel GC)**:多线程回收,适用于增加吞吐量的需求,如在多CPU环境下。
- **CMS回收器(Concurrent Mark Sweep)**:减少停顿时间,适用于需要低延迟的应用。
- **G1回收器(Garbage-First GC)**:一种服务器端垃圾回收器,旨在用于多处理器机器和大内存环境,能够更好地预测停顿时间。

设计模式

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。

1
2
3
4
5
6
设计模式是解决待定问题的成熟模版,Springboot中用到了多种模式

单例模式:在Spring中,Bean默认是单例的,提供了一个全局访问点来访问该实例,确保一致性和性能,避免对象重复创建的开销
工厂模式:
代理模式:aop
模版方法模式:

单例模式:它确保一个类只有一个实例,并提供了一个全局的访问点来访问该实例,这个模式特别适用于管理共享资源的情况,比如Spring框架中的Bean容器,和数据库连接池或配置管理器。使用单例模式可以避免对象的多次实例化,保持资源使用的一致性。

工厂模式:提供了一种创建对象的方式,而无需指定具体的实现类

“工厂模式主要分为两种:简单工厂模式和工厂方法模式。

1
2
3
4
5
6
7
8
9
10
11
### 简单工厂

它有一个中央的工厂类,负责创建所有类型的对象,通常通过接收一个参数来判断应该创建哪一种类型的对象。虽然简单工厂模式可以减少代码中的重复和提高内聚性,但它的主要缺点是,如果添加新的产品类型,就需要修改工厂类,这违反了开闭原则。

### 工厂方法模式

工厂方法模式在简单工厂的基础上进行了改进,定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。这样的设计让工厂方法在类的结构中创建所需的对象,它通过让子类实现一个工厂接口来移除了类中的硬编码依赖关系。每个生成的工厂都可以生成一种具体类型的对象,这种模式利于程序的扩展,并且遵守开闭原则。

### 抽象工厂模式

抽象工厂就是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂
1
2
3
4
5
6
7
8
9
10
11
准备换工作面试,来复习下。先看了单例和工厂。只是举些例子,方便记忆和理解。

1:单例,最常见就是spring中的bean都是单例的。比如你连接池datasource不采用单例模式,你每次使用连接池的地方都new个新对象,浪费资源。但是单例有时候会导致并发不安全,特别是一些喜欢在服务类里放成员变量的同志。

2.工厂模式的话,视频里很多都是啥if else,实际上有很多解决办法。比如spring自动注入个map,或者自己通过反射实现再或者写个枚举,通过enum.values(),放在map里面。

简单工厂,很多用的都是静态工厂方法,除非工厂依赖于其他对象,那就没办法静态了。说白了就是一个要有抽象的意识,不要只会CV。否则到时候改起需求来痛苦死你。

工厂方法的话,我自己代码里没有映像是否用过,我觉得在封装第三方包或者使用第三方依赖是可以用到。当你发现别人的类已经无法满足你的需求,你可以自己实现个factory然后注入到容器里,然后创建你自己需要的bean。比如说mybatis里的sqlSessionFactory?里面我没怎么细看。但应该是一种扩展第三方包功能的思路

抽象工厂,我本来想举个不同媒介的存储服务的例子,但我突然发现抽象工厂是一个很重的用法,因为抽象工厂更关注的是所创建的对象。但我们实际开发中突出的是服务的概念,也就是一种方法的实现,所以完全可以把对象中我们真正要用的方法抽成接口,然后用简单工厂就好了。
1
2
3
4
5
6
7
### 代理模式

代理模式是一种设计模式,属于结构型模式的一种。它的主要目的是通过引入一个代理对象来控制对另一个对象的访问。这个代理对象可以为被代理的对象提供额外的功能,比如访问控制、延迟初始化、日志记录、安全检查、缓存等,而不改变被代理对象的代码。

包括

静态代理:在编码的时候,就创建好了代理类和目标对象的关系
image-20240507164056743

动态代理:在运行的时候,动态代理,通过实现proxy类的方法,再调用invoke方法实现

1
2
3
4
5
6
7
### 策略模式

在策略模式定义了一系列算法或策略,并将每个算法封装在独立的类中,使得它们可以互相替换。通过使用策略模式,可以在运行时根据需要选择不同的算法,而不需要修改客户端代码。

创建一个定义活动的 *Strategy* 接口和实现了 *Strategy* 接口的实体策略类。*Context* 是一个使用了某种策略的类。

*StrategyPatternDemo*,我们的演示类使用 *Context* 和策略对象来演示 Context 在它所配置或使用的策略改变时的行为变化。

消息中心

其他消息中心的框架

img

img

img

image-20240428225844257

在校期间呢,参与了老师的一个校企合作项目—-数梦工厂的消息中心,学习了Java以及Spring,Mybatis的相关知识,消息中心主要基于Spring和mybatisPlus实现数据库的操作和Web接口服务,我主要负责了模版表的设计优化,以及用户侧代码的编写

面试总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
面试官您好,我叫方圆,目前是杭州电子科技大学通信工程专业研二的学生,
本科也是杭电通信工程,在学校的学习期间呢,对通信和计算机的相关专业课程都有所涉及,
熟悉Java语言,对spring boot进行web开发有一定的实践。

读研期间呢,参与了老师的一个横向课题,参与了数梦工厂消息中心项目,
负责开发其中的一个模块,我主要负责的是平台侧消息模板的创建和管理,
还有用户侧消息的管理,包括搜索,查看,标记已读删除等,消息推送等

后来项目结束后,为了丰富自己web开发的相关知识,去学习了Redis缓存的相关知识,
开发了一个类似大众点评的单体web应用,
并且在结束后用Docker部署在自己的虚拟机中,用nginx做了负载均衡的配置

同时呢,我也习惯于整理和分享自己的所学知识,为此我基于hexo 搭建了自己的网站,
主要分享技术学习经历,还有一些日常和遇到的bug问题。

因为直接在学习的过程中用到过的云业务比较多,
比如,域名服务,对象存储服务,还有短信服务,当然云端的业务还有很多,
还有我上届的师兄师姐也在华为云部门,我认为以后大家的使用的云端都很多,
会有很大的发展空间,期待能获得这次实习的机会。

DTphere消息中心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
这个消息中心的项目,分为平台侧(管理的人去看的)和用户侧(用户去看的)
在平台侧呢
消息的管理

模版管理
--对不同的渠道,钉钉,微信,短信,邮件设计不同的消息模版
--设计实体关系图,一张表维护不同的消息模版Id
--每个Id作为外键关联一张模板表

渠道管理
--渠道管理负责去配置各个渠道的信息,短信,邮件,企业微信,钉钉机器人等
--引入钉钉,微信等相关依赖,配置用户名密码服务器去做测试链接
用户侧:
全部类型消息的查看
--搜索,查看,标记已读未读,删除消息等
告警消息
--搜索,查看,标记已读未读,删除消息等
系统消息
--搜索,查看,标记已读未读,删除消息等
审批消息
--搜索,查看,标记已读未读,删除消息等
产品消息
--搜索,查看,标记已读未读,删除消息等

对需要删除的消息:用AOP日志记录
使用yapi.pro那个平台编写接口文档,做接口文档的开发

这个消息中心的项目,分为平台侧(管理的人去看的)和用户侧(用户去看的)

平台侧

  • 消息的管理
1
2
3
--绑定管理,将产品与消息模板绑定,当前产品对应的消息模板
--这里是新建一张绑定关系表,将产品Id和模板Id对应起来
查询模版、查询产品消息、绑定、回显
  • 模版管理
image-20240513091758796
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 --对不同的渠道,钉钉,微信,短信,邮件设计不同的消息模版
--设计实体关系图,一张表维护不同的消息模版Id,
--每个Id作为外键关联一张模板表
--我在开始数据库中创建了五张模板表,分别对应五种消息
--(后来就是五张表维护起来太麻烦了,每次增删改查还要在不同表里
还要创建一个模版表去记录创建好的所有Id)
--优化就是:因为模版表有很多相同的字段,创建一张表整合所有字段,
没有设置的字段,就默认为null
然后再新建五个vo类作为返回给前端的消息实体类,去封装对应模版的信息

不同的消息模板有不同的字段,我根据它们提供的图去在数据库中创建模板
这里是参照了它们官方提供的开发手册
定义字段的时候,选择合适的字段内容来建标
字符串就结合存储的内容来使用char 或者varchar
用户名称邮箱类型的可变,使用varchar
文章内容使用text
邮编长度使用char等等
模版类型这里,因为只有几个嘛,开始用的int,后来该改的tinyint

优化这里:针对test过大的问题,在数据库中做了垂直分割
就是将表中除了text的字段和其他字段分割,用
将text字段放在另一张表中,只需要在详细时访问
  • 渠道管理

​ 下图只是举例,

​ 我们是:邮件、短信、企业微信、钉钉机器人、钉钉联系人等(字段也不同)

1
2
3
4
5
6
7
8
9
10
11
12
--渠道管理负责去配置各个渠道的信息,短信,邮件,企业微信,钉钉机器人等
--引入钉钉,微信等相关依赖,配置用户名密码服务器等

我们负责测试,前端同学将这些参数封装在json中传递
我们后端负责接受这些参数,然后在后台调用依赖
尝试去获取消息,获取成功就测试成功

邮件的话:我们是链接163邮箱,配置服务器,端口,这里是去163官网查看了相关demo
短信:引入它们自己的api,提供的了网关插件地址,去做测试链接
钉钉机器人、联系人、企业微信:都是去官方调用官方API
看它们的示例demo,去做配置
引入了第三方的依赖,去做测试链接
  • 消息分类统计展示
1
2
从不同维度统计及展示消息发送情况
例如根据消息的通知渠道、来源产品、消息分类(告警类型、工单提醒、license提醒)等。

用户侧

主要是对站内信的处理(从它们数据库中获取)

  • 全部类型消息– 告警消息、系统消息、审批消息、产品消息
  • 未读消息
  • 已读消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
这个就是对站内消息的处理
文档中给的需求是,查看,搜索,标记已读,删除,分页查找,批量删除等
并没有给我们接口文档。我们就先在实现的时候,根据功能编写接口文档
我们找了yapi.pro去编写

我们自己定义的规范就是,基于RESTful API定义的
用户侧就/user开头
说明请求参数,路径参数或者是实体参数开头的
定义Result返回值对象,返回标准的响应值(响应码,响应信息,响应数据data等)
数据库的实体参数作为DTO类,响应给前端的不一定是全部的字段
所以创建一个vo实体类,封装专门响应给前端的信息
(这里就用到了Hutul工具类的拷贝方法,不用一行一行复制很麻烦)

在删除的时候,因为涉及到数据库的操作,所以需要记录日志
(这里自定义了@logo注解和切面类@Aspect,用AOP的相关知识)

难点:未读消息的推送:
我们一开始这里写的是,去遍历数据库去查询,去查询未读的字段
(这样做性能不是很高,但是先实现了功能,后来它们测试让我们修改)
后来我们对是否已读字段加了索引,提高查询的效率
用web socket去代替传统的轮询,实现主动的消息推送
引入了web socket依赖
创建消息处理器,来控制和发送接受消息

Redis的数据类型

以及每种数据类型的使用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
String类型:Redis最常见的数据结构,可以存储任何数据,字符串,整形,浮点型都可以,不管用哪儿种方式,底层都是以字节数组的形式存储的,适用于绝大多数场景

列表List:相当于Java中的linkedList,列表是链表结构,可以做简单的消息队列功能,但一般不用Redis消息队列

集合分为集合和有序集合,

集合set:没有重复元素的集合嘛,一般用于点赞,我们维护一张点赞的表,每次用户请求点赞的时候,我们先用StringRedisTemplete.opsForSet()得到操作Redis中set集合的接口,调用其中的isMember(),去判断点赞是否存在,不存在就用add加入,再跟新到数据库,存在就返回已经点赞

共同好友也是,在关注的时候,把当前ID的关注列表以set形式存储到Redis中,然后,用set求交集的interset()
就可以求得共同的好友交集,

有序集合sorted set:相对于集合set,是有顺序的,每个元素都会关联一个权重,按照权重进行排序,
我主要用在点赞的排行傍等,使用的权重是时间,点赞时间早的会排序在前面,用ZSET的range(key,0,4)方法得到排行前几的,
当然不同的排行傍会有不同的权重,可以是分数或者其他,使用权重排序的方式是不变的


哈希hash:键值对,适合缓存对象的存储,因为我们Redis本来就是以键值对key-value的形式存储的,一般hash存储用在value属性中,value属性存储的也是一对键值对

位图bigMap:用于存储二进制数据,可以位运算,统计用户签到比较方便

地理位置GEO:用于存储地理位置的数据结构,我们在数据库中将存放地理位置坐标的位置信息用stringRedisTemplete.opsForGeo接口的add方法存储到Redis中,再调用Geodistance查找距离,GeoRadius查询一定范围内的餐厅(这个需要先把商铺位置数据存储到Redis中去)

HyperLogLog:基于基数的数据结构,支持对大量元素的去重统计,占用和误差很小,
用于网站的访问统计等,底层是概率估算,
我们首先要在线程池里获得当前用户的唯一标识,
针对访客数,和访问量选取不同的标识
针对访客数,因为是不可重复的,可以选用唯一账户ID等
针对访问量,可以重复,当前线程中拿到的cookie即可


项目具体功能的实现逻辑

1.短信验证码的发送,登录、注册、校验

1
2
3
4
5
发送短信验证码:对手机号进行格式校验,然后随机生成验证码,引用阿里云的依赖并做配置文件的配置,然后响应给前端

登录:得到手机号和验证码之后,去判断验证码是否正确,正确就登录成功,再去数据库中判断是否存在,不存在就创建一个用户,保存到数据库中,同时用uuid生成一个唯一的token作为key去在Redis中存放用户数据,设置一个过期时间

校验登录状态是:设置一个拦截器prehandle,去拦截登录请求,获取token,去Redis中判断token是否存在,如果存在的话,就保存当前用户到线程池当中,这样我们后续在其他地方获取当前用户信息就只需要在线程池中获取即可
1
2
3
4
5
6
cookie和session会话跟踪技术,去管理用户的状态,因为HTTP响应是无状态的,每次请求都是独立的,下次的请求不会携带上次的数据,所以需要会话跟踪技术
cookie是客户端会话跟踪技术,存储数据在客户端浏览器中

session是服务端会话跟踪技术,数据存储在服务端中
底层通过cookie发送,只需要发送JessieId就可以了,在服务端就可以找到对应Id所在的当前数据
相比于cookie会更安全一些

2.Redis分布式锁的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
缓存穿透:就是大量请求的数据在缓存和数据库中都不存在,就会造成给数据库请求压力大的情况

解决方法有:
缓存空对象在缓存中
布隆过滤:就是在Redis和客户端之间再加了一层布隆过滤器,存在则通过,不存在则拒绝,
布隆过滤器是用hash思想实现的,它底层是一个庞大的二进制数组
来了一个key之后对这个key取模存入数组
再来就可以判断它key是否存在了
当然有hash的地方就不可避免有hash冲突,但因为它底层数组够大,冲突不超过5%

缓存击穿:缓存击穿就是,热点击穿嘛,热点key失效,很多线程来重建key,造成数据库的压力
解决方法就是
1.互斥锁,最先来的线程获取锁,去执行key的重建,其他的线程就会被阻塞
2.逻辑过期:就是存储一个过期的时间的字段到缓存中,但我们并不设置过期时间
当请求达到的时候,从缓存中获取数据,去判断一下这个过期字段,若过期则认为失效
失效的时候,就开启另外一个线程,去更新缓存的逻辑过期时间
当前线程就返回旧的数据
利弊:互斥锁保证了数据的一致性,但是会收到锁竞争的影响,考虑死锁的问题
逻辑过期保证了高的可用性,但是可能会出现数据不一致的问题

缓存雪崩:就是我们设置key的时候,设置了一样的过期时间,导致一起失效,数据直接打到数据库的时候
缓存雪崩与击穿不同的是,击穿是一个热点key失效,雪崩是很多key通用失效
解决方法就是:在原有的失效时间的基础上,随机增加几分钟的随机值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
分布式锁的实现:简单来说就是用setnx这个方法,去在Redis中设置锁,如果插入key成功别人就获取不到,相当于加锁
setex获取锁的过期时间,最后在完成业务逻辑之后,再去删除释放锁

因为执行逻辑和删除是有时间差的,为了确保锁的一致性,防止极端宕机情况,最好使用lua脚本去执行锁的创建和删除
lua脚本在Redis中执行是单线程的,所以一定会确保执行的时候的原子性

当然自己实现的还是会有很多考虑不到的地方,比如可重入啊,可靠啊等等
后面都是引入Redis依赖,用redisson的分布式锁实现的
底层会有看门口机制解决死锁的问题,死锁就是业务逻辑异常导致锁无法释放的问题
看门狗机制会在获取锁成功后启动任务,更新锁的过期时间,避免死锁的发生

乐观锁和悲观锁:
悲观锁:假定一定会发生多线程的问题,就只允许单线程执行,比如Synchronized关键字
乐观锁:允许多线程,但是会有一个版本号,只是在更新数据的时候,
判断是否其他线程对数据进行了修改,是的话重试即可

3.lua脚本实现高并发环境下的一人一单和线程安全问题

1
2
3
4
5
6
7
8
9
就是在lua脚本中去定义本地的处理逻辑
检查本地的数量是否足够
判断用户是否已经下单
再去减库存
返回操作结果

rabbitMQ消息队列:
生产者去创建一个交换机,交换机负责投递消息到哪儿一个队列,队列再把消息传给消费者处理
用注解@RabbitListener去配置

4.点赞,共同关注,排行榜

5.地理位置下的店铺查询

6.统计访问量和访客量

1
2
3
开始使用了session做登录校验,从当前线程里获取到session但是session在分布式中不能共享,所以
集成阿里云的短信服务,就是引入了阿里云的依赖,先是对,做了配置文件的配置,

Redis的相关面试题

Redis和MySQL如何做读写一致性

1
2
3
1.缓存过期嘛,定期的从sql中加载最新的数据
2.做数据更改时候,不直接更新Redis而是删除缓存,下次访问时候未命中再更新
3.最终一致性:使用消息队列异步处理

Redis持久化是怎么做的呢

1
2
3
4
两种持久化方案:
1.快照文件,把Redis存储的数据保存到磁盘上,方便宕机的时候做数据恢复
2.追加文件,Redis操作写操作的时候,会存储到这个文件中去

Redis的数据过期策略

1
2
3
惰性删除:一般不管,我们使用到key后,如果判断过期再删除
定期删除:每隔一段时间对key进行检查,定期清理
一般Redis中的过期删除策略,是结合这两部分来的

Redis淘汰策略

1
很多,一把用LRU,对最近少使用的key淘汰

Redis集群

1
2
3
主从复制:数据从一个主节点复制到其他节点
哨兵模式:哨兵作为独立的节点监控集群,发生故障时候执行故障转移
分片模式:将数据分片到多个节点上

Linux常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
使用Redis和
文件操作:ls,ll,
进入目录:cd.. cd/ cd
显示路径:pwd
创建目录 mkdir
删除:rf
复制:cp
移动:mv

tar -zxvf解压
ping
ipconfig
sudo
等等

项目部署docker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
使用docker,我们需要将项目打个包为镜像,镜像包含应用的本身,也包含运行的环境依赖等等
docker在运行的时候会创建一个容器,运行我们的镜像文件

使用docker很方便
我用docker在我的虚拟机中创建运行了
MySQL
Redis
rabbitmq
openjdk
nginx
注意的是:Redis和MySQL需要进行目录的挂载
将容器内的目录映射到外边来
这样我们删除创建容器的时候,数据不会丢失

并且将自己的项目文件用Maven打包,构建dockerFile构建镜像
然后再执行构建镜像即可

nginx

1
2
3
4
启动nginx,在配置文件中修改参数
监听我程序运行的端口80
然后反向代理给启动的两个服务:8080和8081
选择负载均衡的方法有:轮询,最少连接,哈希等

数据库MySQL

数据库事务

1
2
3
4
5
6
7
8
9
10
11
一系列的数据库操作被当作一个单独的逻辑单元,要么全部执行,要么全部回滚
数据库事务是为了确保事务的完整性和一致性
数据库事务由四个属性组成ACID
原子性:要么全部执行,要么全部回滚
一致性:事务执行前后,数据库的数据处于一致状态
隔离性:每个事务独立于其他事务
持久性:事务执行成功,就是持久的

在sql中使用 start Transaction作为事务的开启,rollback回滚,commit提交
在spring框架中,在service层使用注解@Transactional开启事务

索引:

1
2
3
4
5
6
7
8
9
10
11
12
索引是一种特殊的数据结构,用于加快数据库中数据的查询速度
类似于书中的目录,可以帮助快速查找目录

使用create index ...去为字段创建索引
一般来讲是where子句中频繁出现的列,在表的特定列上创建索引

主键是会自动创建索引的
索引的底层是通过B+树来存放的,我们创建索引时候
会将当前字段插入B+树
B+树是一个多路平衡查找树,
所有叶子节点包含全部的键相互连接,方便查找
非叶子节点只做索引,不含实际数据

1
2
3
4
在并发访问时候,解决数据一致性和有效性的机制
全局锁:对整个数据库加锁,在备份的时候加锁 flush tables with readlock
表级锁:锁表 lock tables..
行级锁:锁行 lock

sql优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1.建标的时候选择合适的类型
2.使用索引
3.sql语句的编写
尽量避免select*的情况,查询具体的字段
尽量少用子查询,用join关联查询代替
尽量不使用or,导致索引失效,分开几条sql写
执行的时候,批量插入,而不是一条一条插入,分批删除
避免索引失效的写法
4.主从复制
5.读写分离

建标选择合适的类型
参考的是阿里云的开发手册,定义字段的时候,选择合适的字段内容来建标
字符串就结合存储的内容来使用char 或者varchar
用户名称邮箱类型的,使用varchar
文章内容使用text
邮编长度使用char等等

sql的执行顺序

1
2
3
4
5
6
7
8
9
10
select * from where groupby having orderby limit

先执行from语句,查询确定查的表
再是链接条件,join on
再是where 过滤符合条件的
再groupby分组
having分组后过滤
最后排序,limit输出

where是分组前过滤,having是分组后过滤

spring Web框架

spring框架

1
2
3
4
spring是Java开发的框架,有很多模块
spring框架的web开发模块,就是springMVC
springMVC + spring framework + Mybatis 就是俗称SSM
springboot整合了ssm,快速开发

IOC控制反转

1
2
3
4
5
是一种设计模式,将原本在程序中手动创建对象的控制权,交给spring框架来管理
IOC容器就是工厂一样,当我们需要创建一个对象的时候
只需要用注解@Autowired,而不需要考虑对象是如何创建出来的

Bean指的是被IOC容器所管理的对象

AOP面相切面编程

1
2
3
4
5
6
7
8
9
10
11
12
13
AOP是spring的核心框架之一,它在程序运行时动态将额外的行为
比如记录日志操作,事务处理,方法运行时间等功能,额外的插入到代码中

它是通过动态代理的方式实现的
实现proxy类的方法,生成代理对象,在其他地方通过去通过代理对象调用方法

我在项目中用AOP的实现过程,对数据库修改操作的日志记录的实现过程
创建操作日志的表格嘛,操作人Id,时间方法参数等
根据表格创建实体类
自定义注解@log和切面类@ASpect
注解表示在切面类中使用切面表达式,@Around,@Before前置循环后置,将注解引入进来
并写入插入数据库的逻辑
这样只需要在方法中加入自定义注解就可以实现记录日志操作的过程

spring框架用到了哪儿些设计模式

1
2
3
4
5
6
7
8
单例模式:spring中的bean是单例的,通过IOC容器对Bean管理,保证整个应用只有一个Bean实例
工厂模式:springIOC负责容器的创建和管理实例,可以将IOC看做是一个工厂
我们不需要知道对象是如何创建的
只需要用注解直接注入对象就行

代理模式:spring的AOP面相切面编程就用到了代理模式
实现proxy类的方法,生成代理对象,在其他地方通过代理对象去调用方法

spring事务

1
2
@Trancsacyional注解
事务的传播,如果当前有事务,就加入该事务,如果没有,就创建新的事务

spring的常用注解

1
2
3
4
5
6
7
8
9
10
11
12
13
最先就是启动注解@SpringBootApplication了,里面包含三个注解
然后就是三层架构的
@RestController @Service @MApper

在controller层
去处理请求HTTP相关的注解
@GetMapping @PostMapping @DeleteMapping等
传值相关的注解
@RequestBody @PathVariable @RequestParam等
mapper层的数据库注解
@Insert @Select @Delete 等等

还要通用的 @AutWired @configuration @Transcactional等等

mybatis 和mybatisPlus的联系区别

1
2
3
4
5
6
7
8
9
mybatis是用Java操作数据的的持久层框架,主要使用xml或者注解来配置sql语句
使用时,会创建数据库连接池,解决了资源重用,提示响应

MP内置了增删改查,通过扫描实体类,基于反射获取实体类的信息,作为数据库的信息表
一般来讲,单表的增删改查就有MP实现
多表的联合查询就没法用MP了,用mybatis去写

MP自带了分页查询插件,不用去引入第三方的依赖,很方便
创建一个Page<>对象,然后调用selectPage()方法

JVM

img

汇总

1
2
3
4
5
6
7
其中内存模型、类加载机制、GC是重点方面,性能调优部分更偏向应用,重点突出实践能力,编译器优化和执行模式部分偏向于理论,重点掌握知识点
需了解内存模型各部分作用,保存哪些数据
类加载:双亲委派机加载机制,常用加载器分别加载哪种类型的类
GC分代回收的思想和依据,以及不同垃圾回收算法的回收思路和适合场景
性能调优:常用JVM优化参数作用、参数调优的依据、常用的JVM分析工具能分析哪些问题以及使用方法
执行模式:解释、编译、混合模式的优缺点,Java7提供的分层编译技术、JIT即时编译技术、OSR栈上替换、C1/C2编译器针对的场景
编译器优化:javac的编译过程、ast抽象语法树、编译器优化和运行期优化

详解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
JVM内存模型
线程独占:栈、本地方法栈、程序计数器
线程共享:堆、方法区

又称方法栈,线程私有的,线程执行方法是都会创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息。调用方法时执行入栈,方法返回时执行出栈
本地方法栈
与栈类似,也是用来保存执行方法的信息,执行Java方法是使用占,执行Native方法时是使用本地方法栈
程序计数器
保存当前线程执行的字节码位置,每个线程工作时都有独立的计数器,值为执行Java方法服务,执行Native方法时,程序计数器为空

JVM内存管理最大的一块,堆被线程共享,目的是存放对象的实例,几乎所有对象的实例都会放在这里。当堆没有可用空间时,会抛出OOM异常(Out of Menory内存溢出),根据对象的存活周期不同,JVM把对象进行分代管理,由垃圾回收器进行垃圾的回收管理
方法区
又称非堆区,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器优化后的代码等数据
1.7的永久代和1.8的源空间都是方法区的一种实现
JVM内存可见性
JMM是定义程序中变量的访问规则,线程对变量的操作只能在自己的工作内存中进行,而不能直接对主内存操作,由于指令重排,可能会导致读写的顺序被打乱,因此JMM需要提供原子性、可见性、有序性保证