🔥 本文是Java事务系列的第二篇,深入讲解事务的隔离级别和传播机制。通过本文,你将彻底理解事务并发问题的本质,以及如何正确使用事务传播行为。

一、事务的隔离级别
在上一篇文章中,我们详细讲解了事务的ACID特性。其中的I(Isolation,隔离性)是最复杂的一个特性,今天我们就来深入探讨事务的隔离级别。
1.1 为什么需要隔离级别
并发访问的问题
想象一下你在看一本实体书的目录:
- 正当你在看第1页时,别人把第2页撕掉了
- 你翻到第2页时,发现内容不见了
- 等你再回过头看第1页,内容又变了
这就是典型的并发访问问题。在数据库中也是如此:
-- 事务A: 查询账户余额
SELECT balance FROM account WHERE id = 1; -- 返回1000
-- 事务B: 同时进行转账操作
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 事务A: 再次查询余额
SELECT balance FROM account WHERE id = 1; -- 返回900
-- 事务A发现数据莫名其妙变了!
性能与安全的权衡
解决并发问题最简单的方法就是"排队",但这样会导致性能急剧下降:
-- 性能最好,但不安全
SELECT balance FROM account ... -- 多个事务可以同时查询
-- 最安全,但性能最差
LOCK TABLE account ... -- 其他事务都要等待
这就需要在性能和安全性之间做权衡:
- 完全隔离:安全性最高,但性能最差
- 完全不隔离:性能最好,但可能出错
- 部分隔离:在两者之间找平衡点
数据一致性的挑战
在实际业务中,数据一致性的要求也不尽相同:
- 余额查询
-- 对实时性要求高
SELECT balance FROM account WHERE id = 1;
-- 不能看到其他事务未提交的修改
- 报表统计
-- 对实时性要求低
SELECT COUNT(*), SUM(amount) FROM orders
WHERE create_time >= '2024-01-01';
-- 可以容忍短暂的不一致
- 库存管理
-- 既要求实时性,又要求准确性
SELECT stock FROM products WHERE id = 1;
-- 必须看到最新的库存,否则可能超卖
1.2 并发问题详解
脏读现象与解决方案
脏读就像看到了一个"草稿",还没有定稿就被别人看到了:
-- 事务A: 转账操作
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 这时事务还没提交
-- 事务B: 读取余额
SELECT balance FROM account WHERE id = 1;
-- 看到了已经减少的余额
-- 事务A: 发现操作有误,回滚
ROLLBACK;
-- 事务B看到的数据就是"脏数据"
解决方案:
-- 设置隔离级别为READ COMMITTED
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 这样就只能读取已提交的数据
不可重复读分析与实例
不可重复读就像看书时,前后翻页看到的内容不一样:
-- 事务A: 第一次查询
SELECT price FROM products WHERE id = 1; -- 返回100
-- 事务B: 修改价格
UPDATE products SET price = 200 WHERE id = 1;
COMMIT;
-- 事务A: 第二次查询
SELECT price FROM products WHERE id = 1; -- 返回200
-- 同一个事务中,两次查询结果不一样!
在某些业务场景下,这种情况是不能接受的:
public void checkout(Order order) {
// 第一次查询商品价格
double price = getProductPrice(order.getProductId());
// 计算订单金额
double confirmPrice = getProductPrice(order.getProductId());
if (price != confirmPrice) {
throw new PriceChangedException("价格已变化!");
}
// 创建订单
createOrder(order, amount);
}
幻读问题剖析
幻读就像你在统计图书数量,但统计过程中有人不断加入新书:
-- 事务A: 统计某个价格区间的商品数量
SELECT COUNT(*) FROM products WHERE price < 100; -- 返回10
-- 事务B: 插入一个新商品
INSERT INTO products(name, price) VALUES('新商品', 50);
COMMIT;
-- 事务A: 再次统计
SELECT COUNT(*) FROM products WHERE price < 100; -- 返回11
-- 统计结果莫名其妙多了一个!
这在报表统计等场景下特别常见:
public void generateReport() {
// 统计订单总数
long totalOrders = countOrders();
// 分页查询订单详情
for (int page = 1; page <= totalPages; page++) {
List<Order> orders = queryOrdersByPage(page);
// 可能查到了统计之后新增的订单
// 导致总数和明细不匹配!
}
}
💡 实践建议:
- 根据业务场景选择合适的隔离级别
- 对于重要的金融交易,建议使用REPEATABLE READ
- 统计分析类操作可以使用READ COMMITTED
- 特别重要的操作可以考虑使用SERIALIZABLE
1.3 四种隔离级别深度解析
数据库提供了四种隔离级别,从低到高依次是: 读未提交、读已提交、可重复读和串行化。让我们深入理解每种级别的原理和应用。
Read Uncommitted(读未提交)
实现原理
这是最低级别的隔离,实现起来最简单。在这个级别下,事务可以看到其他事务未提交的修改。就像是在看一个实时编辑的文档,别人正在输入的内容你马上就能看到,不管他是否已经完成编辑。
让我们看一个具体的例子:
-- 事务A写入数据时
UPDATE account SET balance = balance - 100;
-- 数据直接写入数据表,不做任何版本控制
-- 事务B读取数据时
SELECT balance FROM account;
-- 直接读取当前值,不管它是否已提交
应用场景
这种隔离级别虽然不安全,但在某些特定场景下还是有用的。比如实时监控系统,我们更关心数据的实时性而不是准确性:
@Transactional</

最低0.47元/天 解锁文章
3385

被折叠的 条评论
为什么被折叠?



