数据库事务面试题
1. 什么是数据库事务?
一句话:事务是一组要么全成功、要么全失败的操作。
展开:比如转账------A扣钱和B加钱必须同时完成,不能只扣不加。
加分点:事务保证了数据不会停留在中间状态。
2. 事务的 ACID 四大特性是什么?
A(原子性) :操作不可拆分,全做或全不做。
C(一致性) :事务前后数据约束不变(如总金额守恒)。
I(隔离性) :多个事务互不干扰。
D(持久性):提交后数据永久保存,宕机也不丢。
加分点:一致性是最终目的,AID 是为 C 服务的。
3. 什么是脏读?
一句话 :读到了别的事务还没提交的数据。
例子 :
事务A把余额从100改成200(未提交),事务B读到200。
如果A回滚,B读到的200就是脏数据。
加分点 :脏读是隔离级别最低时才出现的问题。
4. 什么是不可重复读?
一句话 :同一事务内,同一行数据前后读的结果不一样。
例子 :
事务A第一次查余额100,事务B改成200并提交,事务A再查变成200。
加分点 :侧重修改(UPDATE) ,幻读侧重新增/删除。
5. 什么是幻读?
一句话 :同一事务内,两次查询返回的记录条数不一样。
例子 :
事务A查用户列表共10条,事务B插入一条新用户并提交,事务A再查变成11条。
加分点 :MySQL的RR级别通过间隙锁基本解决了幻读,但严格来说只在串行化下完全避免。
6. 数据库有哪几种隔离级别?
| 级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | ✔ | ✔ | ✔ |
| 读已提交 | ✘ | ✔ | ✔ |
| 可重复读 | ✘ | ✘ | MySQL基本避免 |
| 串行化 | ✘ | ✘ | ✘ |
加分点:隔离级别越强,并发性能越差。
7. MySQL 默认隔离级别是什么?解决了哪些问题?
一句话:默认可重复读(RR)。
解决了 :脏读、不可重复读。
幻读 :通过 MVCC + 间隙锁,大部分场景避免,但严格完全避免需要串行化。
加分点:面试官问"RR怎么解决幻读"时,可以说"快照读用MVCC,当前读用间隙锁"。
8. 什么是 MVCC?
一句话:多版本并发控制,让读不阻塞写,写不阻塞读。
原理:每条数据有隐藏的版本字段 + undo log 记录历史版本。
好处 :在 RR 和 RC 级别下,读操作读的是事务开始时的快照,不用加锁。
加分点:MVCC 是 MySQL InnoDB 高并发的核心机制。
9. Spring 事务管理有哪两种方式?
| 方式 | 说明 | 适用场景 |
|---|---|---|
| 编程式 | 手写 begin/commit/rollback | 精细控制、复杂逻辑 |
| 声明式 | @Transactional | 简单易用,项目主流 |
加分点:实际开发几乎都用声明式,编程式很少用。
10. Spring 事务的 7 种传播机制,常用哪几个?
常用3个:
| 机制 | 行为 |
|---|---|
| REQUIRED | 有事务就用,没有就新建(默认) |
| REQUIRES_NEW | 挂起当前事务,新建一个独立事务 |
| NESTED | 嵌套事务,外层回滚内层也回滚,内层异常外层可选回滚 |
加分点:REQUIRES_NEW 和 NESTED 区别常考------前者独立提交/回滚,后者依赖于外层。
11. @Transactional 注解常用属性有哪些?
| 属性 | 作用 |
|---|---|
| propagation | 传播机制 |
| isolation | 隔离级别 |
| rollbackFor | 指定哪些异常回滚 |
| noRollbackFor | 指定哪些异常不回滚 |
| timeout | 超时时间(秒) |
| readOnly | 是否只读事务(优化性能) |
加分点:readOnly = true 可以走只读优化,适合查询场景。
12. @Transactional 默认对哪些异常回滚?
一句话 :只对 RuntimeException 和 Error 回滚。
受检异常 (如 IOException、SQLException)默认不回滚。
解决方案 :手动指定 @Transactional(rollbackFor = Exception.class)
加分点 :这是一个高频踩坑点,面试时主动说出来很加分。
13. @Transactional 失效场景有哪些?(高频)
| 场景 | 原因 |
|---|---|
| 方法不是 public | Spring 代理只能拦截 public |
| 类没被 Spring 管理 | 没加 @Service 等 |
| 自身调用(this.方法) | 没走代理 |
| 异常被 try-catch 吞掉 | 没抛出异常 |
| 数据库引擎不支持事务 | 如 MyISAM |
| 传播机制设置错误 | 如改成 NOT_SUPPORTED |
加分点:可以说"我在项目里遇到过自身调用失效,后来通过注入自身或放到另一个Service解决"。
14. 编程式事务怎么用?
两种方式:
Java
// 方式1:TransactionTemplate
transactionTemplate.execute(status -> {
// 业务代码
return result;
});
// 方式2:PlatformTransactionManager
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 业务代码
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
加分点 :适合一个方法中有多个事务边界 或复杂条件回滚的场景。
15. 分布式事务了解吗?简单说下
一句话:跨多个数据库/服务的事务,不能靠本地事务解决。
常见方案:
| 方案 | 特点 |
|---|---|
| 2PC | 强一致,但性能差、有阻塞风险 |
| TCC | 业务侵入强,性能较好 |
| 可靠消息最终一致性 | 实用主流,如 RocketMQ 事务消息 |
| Seata | 框架简化开发,AT模式无侵入 |
加分点:小厂问到这个,能说出"最终一致性 + 补偿机制"就足够,不用太深。
场景题
场景题1:下单扣库存,事务怎么加?
题目
用户下单:需要「扣库存 + 创建订单 + 扣余额」,三个操作。请问 @Transactional 应该加在哪里?有什么注意事项?
追问
- 如果库存够、余额够,但创建订单失败了,库存和余额会回滚吗?
- 如果库存扣成功,余额扣失败了,怎么保证一致性?
标准答案
1. 事务加在Service层方法上:
java
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
// 1. 扣库存
stockService.deductStock(dto.getProductId(), dto.getQuantity());
// 2. 扣余额
accountService.deductBalance(dto.getUserId(), dto.getAmount());
// 3. 创建订单
orderMapper.insert(order);
}
}
2. 回答追问:
- 只要三个操作在同一个事务方法里,任何一个失败,全部回滚
- 余额扣失败 → 库存回滚、订单不回创建 → 保证最终一致
3. 注意事项(加分点):
- 必须指定
rollbackFor = Exception.class,防止受检异常不回滚 - 扣库存和扣余额操作要先扣后创建订单(防止超卖)
- 高并发场景:扣库存要用乐观锁 或Redis预扣 ,不能用简单
update stock set num = num - 1(会超卖)
面试官想听:
- 知道事务加在Service层
- 知道要指定rollbackFor
- 能说出高并发下事务的局限性(锁时间长、需要结合其他方案)
场景题2:批量导入Excel,一条失败全部回滚吗?
题目
需要批量导入1万条用户数据,要求:要么全部成功,要么全部失败。@Transactional 直接加在循环方法上可以吗?有什么问题?
追问
- 如果改成「一条失败只跳过继续执行下一条」,怎么做?
- 怎么避免大事务问题?
标准答案
1. 直接加@Transactional的问题:
java
java
// ❌ 错误写法
@Transactional
public void importUsers(List<User> list) {
for (User user : list) {
userMapper.insert(user); // 1万次插入都在一个事务里
}
}
问题:
- 事务持续时间太长(可能几十秒甚至几分钟)
- 数据库连接一直被占用
- 锁持有时间长,容易死锁
- 回滚日志巨大,回滚慢
2. 全部成功或全部失败的方案:
- 可以先校验所有数据(格式、重复等)
- 校验通过后再开启事务批量插入(用
batch insert)
java
@Transactional(rollbackFor = Exception.class)
public void importUsers(List<User> list) {
// 前置校验全部通过后
userMapper.batchInsert(list); // 一次插入,不是循环
}
3. 一条失败只跳过继续执行的方案:
- 不能在一个事务里
- 用编程式事务 + 记录失败日志
java
public void importUsersWithSkip(List<User> list) {
List<String> errorMessages = new ArrayList<>();
for (User user : list) {
try {
userService.insertWithNewTransaction(user);
} catch (Exception e) {
errorMessages.add(user.getId() + "失败:" + e.getMessage());
}
}
// 最后返回或保存错误日志
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void insertWithNewTransaction(User user) {
userMapper.insert(user);
}
面试官想听:
- 知道大事务的危害
- 知道两种需求的不同实现方式
- 知道
REQUIRES_NEW可以拆成小事务
场景题3:方法内部调用 @Transactional 为什么失效?怎么解决?
题目
下面代码,调用
orderService.create()时,事务生效吗?调用orderService.save()时呢?
java
@Service
public class OrderService {
public void create(Order order) {
this.save(order); // 内部调用
}
@Transactional
public void save(Order order) {
orderMapper.insert(order);
// 可能抛异常
}
}
追问
- 为什么
this.save()事务会失效? - 给出3种解决方案
标准答案
1. 结论:
- 调用
orderService.create()→ 事务不生效 - 直接调用
orderService.save()→ 事务生效
2. 原因:
- Spring事务通过代理实现
this.save()走的是真实对象,不是代理对象,所以事务切面不执行- 只有通过代理对象调用方法,事务才会生效
3. 三种解决方案:
java
// 方案1:注入自己(推荐)
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入代理对象
public void create(Order order) {
self.save(order); // 走代理,事务生效
}
}
// 方案2:从Spring上下文获取
public void create(Order order) {
OrderService proxy = applicationContext.getBean(OrderService.class);
proxy.save(order);
}
// 方案3:把事务方法放到另一个Service
@Service
public class OrderService {
@Autowired
private SaveService saveService;
public void create(Order order) {
saveService.save(order); // 跨Service调用
}
}
面试官想听:
- 知道事务失效的根本原因(代理 vs 真实对象)
- 能给出至少2种解决方案
- 最好能说出"项目中遇到过,用方案1解决的"
总结:小厂面试官的心理预期
| 场景题 | 能过的最低标准 | 加分项 |
|---|---|---|
| 下单扣库存 | 知道事务加在Service、会回滚 | 高并发优化(乐观锁)、rollbackFor |
| 批量导入 | 知道大事务有问题 | 能说两种需求的不同方案、REQUIRES_NEW |
| 内部调用失效 | 知道this.调用不生效 |
给出3种解法、能说原理(代理) |