Java后端开发 面试知识点复习(二)
1.项目中有用到事务吗?
有。在我们回收业务中,比如用户提交回收订单这个操作:需要同时插入订单表、扣减用户积分/余额、更新物品状态、记录日志。删除店铺同时要删除电源。为了保证数据一致性,我在Service层方法上加了 @Transactional 注解。
项目中用的是Spring声明式事务,底层基于AOP,通过代理拦截方法,在方法执行前开启事务,正常结束提交,异常则回滚。
另外,我遇到过事务失效的情况,比如方法内部调用(this.method)、异常被catch没抛、非public方法等,后来都注意避开了。
我们用的MySQL InnoDB引擎,隔离级别默认是REPEATABLE_READ,没有遇到过大问题。
数据库的事务:多条sql语句,要么全部成功,要么全部失败。
事务的特性:原子性、一致性、隔离性、持久性。ACID
原子性: 组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有操作都成功,
整个事务才会提交。任何一个操作失败,已经执行的任何操作都必须撤销,让数据库返回初始
状态。
一致性:事务操作成功后,数据库所处的状态和它的业务规则是一致的。即数据不会被破坏。如A转账100元给B,不论操作是否成功,A和B的账户总额是不变的。
隔离性:在并发数据操作时,不同的事务拥有各自的数据空间,他们的操作不会对彼此产生干扰。
持久性:一旦事务提交成功,事务中的所有操作都必须持久化到数据库中。
并发事务带来的问题:(多个用户对同一数据进行操作)
脏读(Dirty read):当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这是另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据时还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”。例如:T1读取某表数据A=1000,修改A=600;T2读取A=600,使用A;但是T1回滚了,A还是1000,T2读取的是根本不存在的数据。(读到了还没确定的数据)
丢失修改(Lost of modify):在一个数据读取一个数据时,另外一个事务也访问了该数据。那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务的修改结果就被丢失。例如:T1读取某表数据A=20,T2读取A=20,T1修改A=A+2,T2修改A=A-1,最终A=19,T1的修改被丢失。(后写的覆盖先写的)
不可重复读(Unrepreatableread):指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的。例如:T1读取A=100;T2读取A=100,修改A=80提交;T1读取A=80。(两次读取,结果不一样 由于UPDATE修改)——行锁(如SELECT ... FOR UPDATE)
幻读(Phantom read):发生在一个事务T1读取了几行数据,接着另一个并发事务T2插入了一些数据。在随后的查询中,第一个事务T1就会发现多了一些原本不存在的记录。例如:T1查询不及格人数有3;T2插入一条不及格记录,提交;T1读取不及格人数有4。(两次查询,行数不一样 由于INSERT插入或DELETE删除)——间歇锁(MySQL的REPEATABLE READ级别下用Next-Key Lock)
事务的隔离级别:
读未提交(Read Uncommitted):允许读取尚未提交的数据变更,可能会导致脏读、幻读、不可重复读。
读已提交(Read Committed):只能读取到已经提交的数据。可阻止脏读,可能会导致幻读、不可重复读。Oracle等多数数据库默认都是该级别(不重复读)
可重复读(Repeatable Read):对同一字段的多次读取结果都是一致的,除非数据是被本身事务所修改,可阻止脏读和不可重复读,可能会导致幻读。MySQL InnoDB存储引擎默认支持(SELECT @@tx_isolation可查看)
可串行化(Serializable):所有的事务依次逐个执行,事务之间不干扰。可阻止脏读、不可重复读、幻读。
InnoDB存储引擎在分布式事务的情况下一般会用到可串行化隔离级别。
2.项目中有用到锁吗?死锁等等
用到了。
-
单体/单机环境:在后台管理员并发修改物品回收价的场景,用了
synchronized或ReentrantLock防止同时修改导致数据错乱。 -
分布式环境(多节点部署):
-
幂等控制:比如微信支付回调处理,用Redis分布式锁(Redisson)保证同一退款单号只能被处理一次。
-
定时任务:多节点同时执行任务时,也用Redis锁做选主,只有拿到锁的节点执行。
-
-
死锁经历:有一次对两张表(订单表、积分流水表)更新时,两个线程获取锁的顺序相反,导致了死锁。后来通过统一加锁顺序(比如先锁订单再锁积分)和使用
tryLock设置超时解决了。
另外,数据库层面也会产生死锁,我们通过SHOW ENGINE INNODB STATUS分析并优化了索引。
MySQL数据库的锁:
(1)共享锁(S锁):多个用户可同一时刻读取同一个资源。允许其他事务读,但禁止写。
(2)排它锁(X锁):一个写操作阻塞其他的读锁和写锁。只允许一个用户写入,防止其他用户读取正在写入的资源。禁止其他事务读或写。
(3)表锁:锁定整张表,系统开销最小。适合读多写少、批量操作。
(4)行锁:容易发生死锁,发生冲突概率低,并发高。适合高并发、频繁单行更新。InnoDB支持行锁(必须有索引WHERE 才能实现,否则会自动锁全表--实际是锁住所有聚簇索引记录)
--共享锁
SELECT ... LOCK IN SHARE MODE
--排它锁
SELECT ... FOR UPDATE/DELETE
-- 假设 id 是主键(聚簇索引)
UPDATE user SET name='A' WHERE id=10; -- 锁住 id=10 这一行
-- 假设 name 列没有索引
UPDATE user SET age=20 WHERE name='Tom'; -- 会锁全表(实际上扫描所有行并加锁)
锁升级:
- MySQL行锁只能加在索引上,如果操作不走索引,就会升级为表锁。InnoDB将primary key index和相关的行数据共同放在B+树的叶节点,通过对应的primary,找对应的数据行。
- 非唯一索引上记录数超过一定数量后,行锁也会升级为表锁。当非唯一索引相同的内容 大于等于 整个表记录的二分之一时会升级为表锁,因为此时索引需要的性能比全文检索还要大,查询语句优化时会选择不走索引,造成索引失效,行锁自然就会升级为表锁。
悲观锁和乐观锁:
悲观锁——数据库被外界修改保持保守状态,整个数据修改过程中,数据处于锁状态。依靠数据库锁机制,导致数据库性能的大量开销。悲观锁时,为保证事务的隔离性,需要一致性锁定读,读取数据时给加锁,其他事务无法修改这些数据;修改删除数据时要加锁,其他事务无法读取这些数据。
乐观锁——基于数据库版本(Version)记录机制实现,为数据增加一个版本标识,为数据库表增加一个version字段。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息比对,若提交的数据版本号大于数据库表当前版本号,则更新,否则认为是过期数据。
死锁:两个或以上事务,各自持有对方需要的锁,互相等待,导致都无法继续执行。
产生死锁的条件:互斥、请求与保持、不剥夺、循环等待。
避免死锁的出现:设置获取锁的超时时间,可退出程序而不是一直等待;设置按照同一顺序访问资源,类似串行执行;避免事务中的用户交叉,保持事务简短并在一个批处理中,使用低隔离级别,使用绑定链接。
3.对于AI有没有学习过?解释Agent,MCP,Skill
我有学习过AI的应用层知识,主要是如何调用大模型API来解决业务问题,比如在我的项目中集成了本地大模型,用来做图表代码生成和结论生成。之前也是在学校的课程中学过一些机器学习、深度学习的基础概念和应用。
-
Agent(智能体):一个能够自主感知环境、做出决策并执行动作的AI程序。在对话系统中,Agent可以调用工具、记忆上下文、规划步骤。
-
MCP(Model Context Protocol):Anthropic提出的一种协议,让模型能够与外部数据源和工具标准化交互。让AI更可控、可扩展。
-
Skill:在某些AI平台(如Dify、扣子Coze、Amazon Bedrock)中,Skill是封装好的功能模块,比如“查询天气”“发送邮件”,Agent可以调用Skill完成复杂任务
-
LangChain和AutoGPT
LangChain 是一个模块化的应用开发框架,把大模型、提示词模板、记忆、工具调用等组件组合起来。开发者可以精确编排固定的工作流(比如先检索知识库,再让模型回答——可从数据库或文档中检索信息后,再交给模型回答 RAG),适合构建稳定、可控的生产级应用。
AutoGPT 是一个自主智能体,你只需给它一个最终目标(如“调研AI趋势并写报告”),它会自动拆解成子任务、调用浏览器或代码解释器等工具、观察结果并调整计划,循环执行直到目标完成。它代表了AI自主探索的方向,但目前行为有一定不确定性,更适合实验性场景。
1.AOP原理,通知类型,怎么实现业务的?
原理:AOP基于动态代理。目标类有接口时用JDK动态代理,没有接口时用CGLIB生成子类。代理对象在调用目标方法前后,会执行切面中定义的通知逻辑。
通知类型(5种):@Before、@After、@AfterReturning、@AfterThrowing、@Around。我最常用@Around,因为它可以控制是否执行目标方法以及返回结果。
业务实现:在回收项目中,我写了一个日志切面:对所有Controller方法用@Around,记录请求参数、执行时间、响应结果。还写了一个权限切面:用@Before检查用户是否有操作某订单的权限。
具体代码:定义切面类加@Aspect和@Component,切点表达式@Pointcut("execution(* com..service.*.*(..))"),在通知方法里写业务逻辑。
2.分表策略
为什么分表:单表数据量过大(超过500万~1000万)时,索引效率下降,写入锁竞争严重。分表后要解决跨表查询、全局ID生成(雪花算法)等问题。
分表策略:
- 垂直分表:将大字段或访问频率低的列 拆分到另一张表。
- 水平分表:按某种规则 将行分散到多张结构相同的表。
常用策略:取模(user_id % 8)、范围(按时间/ID区间)、哈希(一致性哈希)。
选择依据:业务查询多数带哪个字段(如订单按user_id分)。
大表(单表记录数过大)如何优化:
(1)限定数据的范围,查询时加上数据范围条件。
(2)读/写分离,主库写,从库读。
(3)垂直分区:根据数据库里面数据表的相关性拆分。数据表列的拆分。用户表--> 用户登录表+用户基本信息表。列数据变小,查询时减少读取Block数,减少I/O次数,简化表的结构,便于维护;但主键冗余,需要join操作,事务复杂。
(4)水平分区:表数据结构不变,通过某种策略 存储数据分片。一张表的数据-->多张表来存放。可以支持非常大的数据量,应用端改造少;但分片事务难以解决,跨节点Join性能差,逻辑复杂。(客户端代理——分片逻辑在应用端,封装在jar包,通过修改或封装JDBC层实现;中间件代理——在应用和数据中间加一个代理层,分片逻辑统一维护在中间件服务中。)
分表仅仅是解决了单一表数据过大的问题,由于表的数据还是在同一台机器,对于提升Mysql并发能力没有意义,水平拆分最好分库。
尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度。实在要分片选择客户端分片架构,可以减少一次和中间件的网络I/O。
分库分表之后,id主键如何处理?
需要生成一个全局唯一的id。
- UUID:不适合作为主键,因为太长,无序不可读,查询效率低。比较适合用于生成唯一的名字的标示比如文件的名字。
- 数据库自增 id : 两台数据库分别设置不同步长,生成不重复ID的策略来实现高可用,生成的id 有序,但需要独立部署数据库实例,成本高,有性能瓶颈。
- 利用 redis 生成 id : 性能比较好,灵活方便,不依赖于数据库。但引入了新的组件造成系统更加复杂,可用性降低,编码更加复杂,增加了系统成本。
- Twitter的snowflake算法 :Github 地址:https://github.com/twitter-archive/snowflake。
- 美团的Leaf分布式ID生成系统 :Leaf 是美团开源的分布式ID生成器,能保证全局唯一性、趋 势递增、单调递增、信息安全,里面也提到了几种分布式方案的对比,但也需要依赖关系数据库、Zookeeper等中间件。美团技术团队的一篇文章:https://tech.meituan.com/2017/04/21/mt-leaf.html
3.IO和密集型线程池,为什么选择IO型,为什么核心线程数是CPU的2倍?
项目中我们有个调用AI接口解析生成结果的功能,需要读取Excel、调用AI解析、写入数据库,这些操作大量等待IO(网络/磁盘),属于IO密集型。
为什么核心线程数设为CPU核数的2倍(或更高):
-
IO密集型任务中,线程大部分时间处于阻塞等待状态(比如等待数据库返回、等待HTTP响应),此时CPU处于空闲。
-
让更多的线程可以交替使用CPU,提高CPU利用率。
-
经验公式:线程数 = CPU核心数 / (1 - 阻塞系数),阻塞系数通常在0.8~0.9,计算下来约CPU核心数 × (2~4)。我们取2倍是一个保守且有效的值。
对于CPU密集型任务(如复杂计算),线程数一般设为CPU核心数+1,避免过多线程带来的上下文切换开销。
我们在项目中实际配置:corePoolSize = Runtime.getRuntime().availableProcessors() * 2,最大线程数4倍,队列用LinkedBlockingQueue。
CPU密集型和IO密集型:
CPU——这种任务消耗的主要是CPU资源,可以将线程数设置为CPU核心数+1,比CPU核心数多出来的一个线程是为了防止线程偶发的缺页中断,或其他原因导致的任务暂停而带来的影响。一旦任务暂停,CPU处于空闲状态,多出来的一个线程就可以充分利用CPU的空闲时间。(线程暂停,操作系统不会给这个线程分配CPU时间片,CPU可能有一个核空闲了)(+1已经能应对绝大多数短暂暂停,再加更多线程会增加上下文切换开销,且CPU密接型任务中,多余线程大部分时间在等待CPU,反而浪费内存和调度成本)
IO——这种任务应用起来,系统会用大部分时间来处理I/O交互,而线程在处理I/O的时间段内不会占用CPU来处理,这是可以将CPU交出给其他线程使用,提高CPU利用率。线程数=CPU核心数x(2--4)。
4.大学学习计算机,有什么是让你比较兴奋的,举个实例,让你很有成就感。
之前和同学做一个关于资源导航的微信小程序的项目,第一次从单纯的算法题,独立的函数,变成一个完整的项目,变成一个我们可以去使用的项目。虽然只是简单的一些资源的展示,没有用到什么高并发,爬虫等,但是也是从0到1系统学习了一个项目从需求开发、代码实现、项目上线的团队协作的项目开发。
5.对Java有没有进行原理性的学习?
有。我不只满足于会用API,还系统学习了:
-
JVM原理:内存区域划分、类加载机制(双亲委派)、垃圾回收算法(可达性分析、标记清除/复制/整理),以及常见GC器(G1、ZGC)的区别。
-
并发编程:
synchronized的锁升级过程(无锁→偏向锁→轻量级锁→重量级锁)、volatile的内存语义(禁止重排序、可见性)、AQS(ReentrantLock、CountDownLatch的底层实现)。 -
集合框架:
HashMap的扩容机制、红黑树退化条件、ConcurrentHashMap的分段锁(JDK7)和CAS+synchronized(JDK8)。 -
我实践的方式:看《Java并发编程的艺术》《深入理解Java虚拟机》,然后自己写demo打断点、用
jstat/jstack分析,还在项目中优化过线程池参数。
6.AI玩不玩,除了找工作,每天会花多少时间学习技术
会去关注和使用。
-
本地部署:有系统学习过机器学习和深度学习的课程,之前比赛有自己学习、训练和部署过Yolo相关的模型,还有一些GAN生成的模型。项目中也会尝试调用本地大模型进行使用。
-
AI工具:日常用Trae、Cursor、Claude Code辅助写代码,用ChatGPT、Deepseek、豆包调试BUG或学新技术。
-
学习投入:平均每天2小时左右(不含上课/实习)。每天会在github或者b站等关注最新最热的技术项目,也会上手去做做,了解趋势。每月会坚持读一本书或做一个完整的项目进行学习。
我觉得技术更新太快,保持学习的习惯比突击更重要。
反问建议:多上手多实践,不止硬背。多想原理,为什么要这么做,不要只跟教程。
更多推荐




所有评论(0)