乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

@Transactional 的隔离级别

定义与作用

isolation 是 @Transactional 的核心属性之一,用于定义当前事务与其他并发事务之间的隔离程度。数据库事务的并发执行可能引发三种典型的数据一致性问题:脏读(Dirty Read)、不可重复读(Non-repeatable Read)和幻读(Phantom Read)。隔离级别通过控制事务对数据的可见性,在数据一致性与并发性能之间做出权衡。

Spring 的 Isolation 枚举将 JDBC 标准隔离级别映射为声明式配置,最终由底层数据库实现。不同数据库的默认隔离级别不同:MySQL InnoDB 默认为 REPEATABLE_READ,Oracle 和 SQL Server 默认为 READ_COMMITTED。

适用位置与常用属性

isolation 与 propagation 并列配置在 @Transactional 注解中:

@Transactional(isolation = Isolation.READ_COMMITTED)
public void transferMoney(Long from, Long to, BigDecimal amount) { ... }

五种隔离级别对比表

隔离级别脏读不可重复读幻读底层数据库默认说明
DEFAULT---依数据库而定使用底层数据源的默认隔离级别
READ_UNCOMMITTED✅ 允许✅ 允许✅ 允许无最低隔离,性能最好,一致性最差
READ_COMMITTED❌ 禁止✅ 允许✅ 允许Oracle / SQL Server读取已提交数据,防止脏读
REPEATABLE_READ❌ 禁止❌ 禁止✅ 允许MySQL InnoDB同一事务多次读取结果一致
SERIALIZABLE❌ 禁止❌ 禁止❌ 禁止无最高隔离,完全串行化,性能最差

Spring 的 DEFAULT 不等于数据库的默认级别:Isolation.DEFAULT 表示 Spring 不指定隔离级别,完全交由底层 Connection 的默认设置。在大多数连接池中,这等同于数据库的默认隔离级别。

核心原理:三种并发问题与隔离级别的防御机制

脏读、不可重复读、幻读的定义

隔离级别的实现机制

MySQL InnoDB 的具体实现:

  • READ_UNCOMMITTED:直接读取最新版本,不加锁,不检查事务状态;
  • READ_COMMITTED:每次 SELECT 生成新的 ReadView,只能看到已提交版本;
  • REPEATABLE_READ:事务首次 SELECT 时生成 ReadView,后续复用,保证快照一致性;
  • SERIALIZABLE:所有普通 SELECT 隐式转换为 SELECT ... LOCK IN SHARE MODE,加共享锁,与写操作互斥。

隔离级别影响对比图

MySQL 的特殊性:InnoDB 的 REPEATABLE_READ 通过 Gap Lock(间隙锁) 和 Next-Key Lock 在一定程度上防止了幻读,因此 MySQL 的 RR 比标准 SQL 定义的 RR 更强。但在严格的 SQL 标准意义上,RR 仍允许幻读。

完整示例:飞翔科技转账系统的隔离级别验证

场景简述

飞翔科技公司的内部财务系统需要实现员工转账功能。大翔(CEO)要求白歌(架构师)验证不同隔离级别下的并发行为。小崔(后端开发)编写测试代码,模拟两个事务并发操作同一账户。

操作前:数据库表结构与初始数据

-- 员工账户表
CREATE TABLE employee_account (
    emp_id INT PRIMARY KEY,
    emp_name VARCHAR(50),
    balance DECIMAL(10,2) NOT NULL
);

-- 初始数据
INSERT INTO employee_account (emp_id, emp_name, balance) VALUES
(1001, '大翔', 10000.00),
(1002, '白歌', 8000.00);

Service 层:不同隔离级别的配置

package com.feixiang.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Service
public class TransferService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 转账方法:使用 READ_COMMITTED(防止脏读,允许不可重复读)
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void transfer(int fromId, int toId, double amount) {
        // 1. 查询转出方余额
        Double fromBalance = jdbcTemplate.queryForObject(
            "SELECT balance FROM employee_account WHERE emp_id = ?",
            Double.class, fromId
        );
        System.out.println("[转账前查询] 转出方余额:" + fromBalance);

        if (fromBalance < amount) {
            throw new IllegalStateException("余额不足");
        }

        // 2. 扣减转出方余额
        jdbcTemplate.update(
            "UPDATE employee_account SET balance = balance - ? WHERE emp_id = ?",
            amount, fromId
        );

        // 3. 模拟耗时,期间其他事务可能修改数据
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 4. 再次查询转出方余额(验证不可重复读)
        Double fromBalanceAgain = jdbcTemplate.queryForObject(
            "SELECT balance FROM employee_account WHERE emp_id = ?",
            Double.class, fromId
        );
        System.out.println("[转账后查询] 转出方余额:" + fromBalanceAgain);

        // 5. 增加转入方余额
        jdbcTemplate.update(
            "UPDATE employee_account SET balance = balance + ? WHERE emp_id = ?",
            amount, toId
        );

        System.out.println("[转账完成] " + fromId + " → " + toId + " 金额:" + amount);
    }

    // 查询方法:使用 REPEATABLE_READ(保证快照一致性)
    @Transactional(isolation = Isolation.REPEATABLE_READ, readOnly = true)
    public void checkBalanceConsistency(int empId) {
        // 第一次查询
        Double balance1 = jdbcTemplate.queryForObject(
            "SELECT balance FROM employee_account WHERE emp_id = ?",
            Double.class, empId
        );
        System.out.println("[RR 第一次查询] 余额:" + balance1);

        // 模拟等待其他事务修改
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 第二次查询
        Double balance2 = jdbcTemplate.queryForObject(
            "SELECT balance FROM employee_account WHERE emp_id = ?",
            Double.class, empId
        );
        System.out.println("[RR 第二次查询] 余额:" + balance2);

        if (balance1.equals(balance2)) {
            System.out.println("[RR 验证通过] 两次读取结果一致");
        } else {
            System.out.println("[RR 验证失败] 两次读取结果不一致!");
        }
    }

    // 统计方法:演示幻读(使用 READ_COMMITTED)
    @Transactional(isolation = Isolation.READ_COMMITTED, readOnly = true)
    public int countHighBalanceEmployees(double threshold) {
        // 第一次统计
        int count1 = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM employee_account WHERE balance > ?",
            Integer.class, threshold
        );
        System.out.println("[第一次统计] 余额 > " + threshold + " 的员工数:" + count1);

        // 模拟等待
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 第二次统计
        int count2 = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM employee_account WHERE balance > ?",
            Integer.class, threshold
        );
        System.out.println("[第二次统计] 余额 > " + threshold + " 的员工数:" + count2);

        return count2;
    }
}

并发测试代码

package com.feixiang;

import com.feixiang.service.TransferService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.concurrent.CountDownLatch;

public class IsolationTest {
    public static void main(String[] args) throws InterruptedException {
        AnnotationConfigApplicationContext ctx =
            new AnnotationConfigApplicationContext("com.feixiang");
        TransferService service = ctx.getBean(TransferService.class);

        // 场景一:READ_COMMITTED 下的不可重复读验证
        System.out.println("=== 场景一:READ_COMMITTED 不可重复读验证 ===");
        CountDownLatch latch1 = new CountDownLatch(2);

        // 事务 A:转账(READ_COMMITTED)
        new Thread(() -> {
            try {
                service.transfer(1001, 1002, 2000.00);
            } catch (Exception e) {
                System.err.println("事务 A 异常:" + e.getMessage());
            } finally {
                latch1.countDown();
            }
        }, "Tx-A-Transfer").start();

        // 事务 B:并发修改转出方余额
        new Thread(() -> {
            try {
                Thread.sleep(50); // 让事务 A 先执行第一次查询
                // 通过另一个 Service 方法模拟并发修改
                service.simulateExternalDeduction(1001, 1000.00);
            } catch (Exception e) {
                System.err.println("事务 B 异常:" + e.getMessage());
            } finally {
                latch1.countDown();
            }
        }, "Tx-B-Deduction").start();

        latch1.await();

        // 场景二:REPEATABLE_READ 下的快照一致性验证
        System.out.println("\n=== 场景二:REPEATABLE_READ 快照一致性验证 ===");
        CountDownLatch latch2 = new CountDownLatch(2);

        new Thread(() -> {
            try {
                service.checkBalanceConsistency(1001);
            } catch (Exception e) {
                System.err.println("事务 C 异常:" + e.getMessage());
            } finally {
                latch2.countDown();
            }
        }, "Tx-C-RR-Check").start();

        new Thread(() -> {
            try {
                Thread.sleep(50);
                service.simulateExternalDeduction(1001, 500.00);
            } catch (Exception e) {
                System.err.println("事务 D 异常:" + e.getMessage());
            } finally {
                latch2.countDown();
            }
        }, "Tx-D-Deduction").start();

        latch2.await();

        // 场景三:幻读验证
        System.out.println("\n=== 场景三:READ_COMMITTED 幻读验证 ===");
        CountDownLatch latch3 = new CountDownLatch(2);

        new Thread(() -> {
            try {
                service.countHighBalanceEmployees(5000.00);
            } catch (Exception e) {
                System.err.println("事务 E 异常:" + e.getMessage());
            } finally {
                latch3.countDown();
            }
        }, "Tx-E-Count").start();

        new Thread(() -> {
            try {
                Thread.sleep(50);
                service.insertNewEmployee(1003, "小崔", 9000.00);
            } catch (Exception e) {
                System.err.println("事务 F 异常:" + e.getMessage());
            } finally {
                latch3.countDown();
            }
        }, "Tx-F-Insert").start();

        latch3.await();
        ctx.close();
    }
}

辅助方法(用于模拟外部并发操作)

// 在 TransferService 中添加

@Transactional
public void simulateExternalDeduction(int empId, double amount) {
    jdbcTemplate.update(
        "UPDATE employee_account SET balance = balance - ? WHERE emp_id = ?",
        amount, empId
    );
    System.out.println("[外部扣款] 员工" + empId + " 被扣款 " + amount);
}

@Transactional
public void insertNewEmployee(int empId, String name, double balance) {
    jdbcTemplate.update(
        "INSERT INTO employee_account (emp_id, emp_name, balance) VALUES (?, ?, ?)",
        empId, name, balance
    );
    System.out.println("[插入新员工] " + name + ",余额:" + balance);
}

操作后:控制台输出与数据变化

场景一(READ_COMMITTED 不可重复读)典型输出:

=== 场景一:READ_COMMITTED 不可重复读验证 ===
[转账前查询] 转出方余额:10000.0
[外部扣款] 员工1001 被扣款 1000.0
[转账后查询] 转出方余额:7000.0
[转账完成] 1001 → 1002 金额:2000.0

关键观察:事务 A 第一次查询余额为 10000.0,在事务 A 执行期间,事务 B 扣款 1000.0 并提交。事务 A 第二次查询余额变为 7000.0(因为转账扣了 2000,外部扣款扣了 1000)。两次查询结果不一致,不可重复读现象发生。

场景二(REPEATABLE_READ 快照一致性)典型输出:

=== 场景二:REPEATABLE_READ 快照一致性验证 ===
[RR 第一次查询] 余额:7000.0
[外部扣款] 员工1001 被扣款 500.0
[RR 第二次查询] 余额:7000.0
[RR 验证通过] 两次读取结果一致

关键观察:事务 C 在 REPEATABLE_READ 下,两次查询结果均为 7000.0。尽管事务 D 在此期间修改了数据并提交,但事务 C 的 ReadView 在第一次查询时生成并复用,保证了快照一致性。

场景三(READ_COMMITTED 幻读)典型输出:

=== 场景三:READ_COMMITTED 幻读验证 ===
[第一次统计] 余额 > 5000.0 的员工数:2
[插入新员工] 小崔,余额:9000.0
[第二次统计] 余额 > 5000.0 的员工数:3

关键观察:事务 E 第一次统计结果为 2 人,事务 F 插入新员工(余额 9000)并提交后,事务 E 第二次统计结果为 3 人。同一事务内两次统计结果不同,幻读现象发生。

最终数据库状态:

emp_idemp_namebalance
1001大翔6500.00
1002白歌10000.00
1003小崔9000.00

易错场景与面试考点

反例一:在 MySQL 下使用 READ_UNCOMMITTED 读取未提交数据

@Service
public class RiskyService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 错误:为了性能使用 READ_UNCOMMITTED
    @Transactional(isolation = Isolation.READ_UNCOMMITTED, readOnly = true)
    public double queryBalance(int empId) {
        return jdbcTemplate.queryForObject(
            "SELECT balance FROM employee_account WHERE emp_id = ?",
            Double.class, empId
        );
    }
}

问题分析:READ_UNCOMMITTED 允许读取其他事务未提交的数据(脏读)。在转账场景中,如果事务 A 修改了余额但未提交,事务 B 读取到修改后的值并基于此做决策(如发放优惠券),随后事务 A 回滚,事务 B 的决策就是基于错误数据的。除非在极端性能敏感且允许数据短暂不一致的场景(如实时统计近似值),否则严禁在业务逻辑中使用 READ_UNCOMMITTED。

反例二:误用 SERIALIZABLE 导致系统吞吐量暴跌

@Service
public class ReportService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 错误:报表查询使用 SERIALIZABLE
    @Transactional(isolation = Isolation.SERIALIZABLE, readOnly = true)
    public List<Map<String, Object>> generateDailyReport() {
        // 复杂的聚合查询,扫描大量数据
        return jdbcTemplate.queryForList(
            "SELECT * FROM employee_account WHERE ..."
        );
    }
}

问题分析:SERIALIZABLE 通过加共享锁实现完全串行化,报表查询扫描大量数据时会锁定整个表或大量行,导致所有写操作(转账、扣款)阻塞。在并发较高的生产环境中,这会导致死锁和超时风暴。报表类只读查询应使用 READ_COMMITTED 或 REPEATABLE_READ,并通过快照读(MySQL 的 MVCC)避免加锁。

反例三:依赖 Spring 隔离级别解决应用层并发问题

@Service
public class InventoryService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 错误:认为 REPEATABLE_READ 可以防止超卖
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void deductStock(int productId, int quantity) {
        Integer stock = jdbcTemplate.queryForObject(
            "SELECT stock FROM inventory WHERE product_id = ?",
            Integer.class, productId
        );
        if (stock >= quantity) {
            jdbcTemplate.update(
                "UPDATE inventory SET stock = stock - ? WHERE product_id = ?",
                quantity, productId
            );
        }
    }
}

问题分析:REPEATABLE_READ 保证的是同一事务内多次读取同一行的结果一致,但上述代码是"先读后写",两次操作之间没有原子性保护。两个事务并发执行时,可能都读到 stock = 10,然后各自扣减 1,最终 stock = 8 而非预期的 9。防止超卖需要乐观锁(版本号)或悲观锁(SELECT FOR UPDATE),而非仅靠隔离级别。

正确写法:

@Transactional
public void deductStock(int productId, int quantity) {
    // 使用乐观锁:UPDATE 时检查版本号
    int updated = jdbcTemplate.update(
        "UPDATE inventory SET stock = stock - ?, version = version + 1 " +
        "WHERE product_id = ? AND stock >= ? AND version = ?",
        quantity, productId, quantity, expectedVersion
    );
    if (updated == 0) {
        throw new ConcurrentModificationException("库存不足或数据已被修改");
    }
}

面试高频考点

Q1:脏读、不可重复读、幻读的区别是什么?

脏读:读取了其他事务未提交的数据,随后该事务回滚,导致读取到的数据无效。不可重复读:同一事务内两次读取同一行数据,结果不同(被其他事务修改并提交)。幻读:同一事务内两次执行相同条件的范围查询,结果集的行数不同(被其他事务插入或删除符合条件的行)。

Q2:MySQL 的 REPEATABLE_READ 为什么能一定程度上防止幻读?

标准 SQL 的 REPEATABLE_READ 只保证同一行数据多次读取一致,不保证范围查询结果集不变。但 MySQL InnoDB 在 REPEATABLE_READ 下使用 Gap Lock(间隙锁) 和 Next-Key Lock,不仅锁定查询到的行,还锁定行之间的间隙,阻止其他事务在间隙中插入新行,从而在一定程度上防止了幻读。但这不是 SQL 标准的要求,其他数据库(如 PostgreSQL)的 RR 可能表现不同。

Q3:Spring 的 @Transactional(isolation = ...) 一定会生效吗?

不一定。Spring 只是将隔离级别设置传递给底层 JDBC Connection,最终由数据库实现。如果数据库不支持该隔离级别(如某些嵌入式数据库),或当前事务已存在且传播行为为 REQUIRED(加入现有事务),则新设置的隔离级别不会生效——因为加入现有事务意味着共享同一个 Connection 和事务上下文,隔离级别在事务开始时已经确定。

Q4:生产环境中如何选择隔离级别?

绝大多数 OLTP 业务使用 READ_COMMITTED(Oracle/SQL Server 默认)或 REPEATABLE_READ(MySQL 默认)。READ_UNCOMMITTED 仅用于对一致性要求极低的统计场景。SERIALIZABLE 仅在极端一致性场景(如金融对账)中使用,且通常通过业务锁替代。选择原则是:在满足业务一致性的前提下,使用最低的隔离级别以获得最佳并发性能。

上一页
@Transactional 的传播行为
下一页
@Transactional 的回滚规则