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

    • 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 的超时与只读属性

定义与作用

timeout 和 readOnly 是 @Transactional 的两个性能与稳定性相关属性,分别控制事务的时间边界和数据访问模式。

  • timeout:定义事务的最大允许执行时间(单位:秒)。如果事务在指定时间内未完成(未提交或未回滚),Spring 会强制回滚该事务并抛出 TransactionTimedOutException。这是防止长事务导致数据库连接池耗尽、锁长时间不释放的关键防御手段。
  • readOnly:向底层事务管理器提示该事务为只读操作。在只读模式下,底层数据库可以进行优化(如禁止脏页检查、跳过锁升级、使用只读副本),同时 Spring 的 JdbcTemplate 等模板类也会避免执行不必要的 flush 操作。

这两个属性通常配合使用:查询类方法配置 @Transactional(readOnly = true, timeout = 10),既保证查询效率,又防止慢查询拖垮系统。

适用位置与常用属性

属性类型默认值适用场景典型配置
timeoutint-1(无限制)防止长事务、死锁等待、慢查询timeout = 30(30秒)
readOnlybooleanfalse只读查询、报表统计、数据导出readOnly = true

配置示例:

@Transactional(readOnly = true, timeout = 10)
public List<Student> findAllStudents() { ... }

@Transactional(timeout = 5)
public void quickUpdate(String id, String status) { ... }

timeout = -1 的含义:表示使用底层事务系统的默认超时,通常等同于"无超时限制"。在生产环境中,除极短的原子操作外,建议为所有事务方法显式设置超时。

核心原理:超时检测与只读优化

超时检测机制

Spring 的超时检测并非通过独立的定时器线程实现,而是在事务执行的关键检查点(如获取连接、执行 SQL、提交前)计算已耗时,若超过阈值则主动抛出异常并回滚。

关键理解:timeout 计量的是整个事务的存活时间,而非单个 SQL 的执行时间。如果事务内包含多次数据库交互、外部 HTTP 调用或复杂计算,累计时间超过阈值即触发超时。

只读事务的底层优化

各持久层技术的具体表现:

技术readOnly = true 的效果
JDBC + JdbcTemplate调用 Connection.setReadOnly(true),驱动可据此优化(如 MySQL 路由到从库)
Hibernate / JPA禁止 Session.flush(),跳过脏检查,禁止 save() / persist() 等写操作
Spring Data JPA底层 SimpleJpaRepository 的查询方法默认设置 readOnly = true

重要限制:readOnly 只是提示(hint),而非强制约束。某些数据库驱动可能忽略该设置,业务代码中仍可通过原生 SQL 执行写操作。真正的写保护需要依赖数据库用户权限或应用层校验。

完整示例:飞翔科技报表查询与限时扣款

场景简述

飞翔科技公司的运营部需要定期导出用户数据报表。高英(用户运营)的报表查询经常因为数据量大而拖慢系统,李眉(运维)要求所有查询必须加超时限制。同时,大翔(CEO)要求财务扣款操作必须在 5 秒内完成,防止死锁导致连接池耗尽。

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

-- 用户表(模拟大数据量场景)
CREATE TABLE app_user (
    user_id INT PRIMARY KEY,
    username VARCHAR(50),
    register_date DATE,
    status VARCHAR(20)
);

-- 插入 10 万条测试数据(略)
INSERT INTO app_user (user_id, username, register_date, status) VALUES
(1, '大翔', '2024-01-15', 'ACTIVE'),
(2, '白歌', '2024-03-20', 'ACTIVE'),
(3, '小崔', '2024-06-10', 'INACTIVE');

-- 财务扣款表
CREATE TABLE finance_record (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    emp_id INT NOT NULL,
    amount DECIMAL(10,2),
    record_type VARCHAR(20),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Service 层:超时与只读配置

package com.feixiang.service;

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

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;

@Service
public class ReportService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 大数据量报表查询:只读 + 30 秒超时
    @Transactional(readOnly = true, timeout = 30)
    public List<Map<String, Object>> exportUserReport(String status) {
        System.out.println("[报表导出] 开始查询,状态过滤:" + status);

        // 模拟复杂查询(实际可能是多表 JOIN、聚合)
        List<Map<String, Object>> result = jdbcTemplate.queryForList(
            "SELECT user_id, username, register_date, status " +
            "FROM app_user WHERE status = ? ORDER BY register_date DESC",
            status
        );

        System.out.println("[报表导出] 查询完成,记录数:" + result.size());
        return result;
    }

    // 快速统计:只读 + 5 秒超时
    @Transactional(readOnly = true, timeout = 5)
    public int countActiveUsers() {
        return jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM app_user WHERE status = 'ACTIVE'",
            Integer.class
        );
    }
}
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.Transactional;

@Service
public class FinanceService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 限时扣款:5 秒超时,防止死锁拖垮连接池
    @Transactional(timeout = 5)
    public void deductSalary(int empId, double amount) {
        System.out.println("[财务扣款] 开始处理,员工:" + empId + ",金额:" + amount);

        // 1. 查询员工当前工资余额
        Double currentBalance = jdbcTemplate.queryForObject(
            "SELECT balance FROM employee_account WHERE emp_id = ?",
            Double.class, empId
        );
        System.out.println("[财务扣款] 当前余额:" + currentBalance);

        if (currentBalance < amount) {
            throw new IllegalStateException("余额不足,员工:" + empId);
        }

        // 2. 模拟复杂审批逻辑(可能耗时)
        simulateApprovalProcess();

        // 3. 执行扣款
        jdbcTemplate.update(
            "UPDATE employee_account SET balance = balance - ? WHERE emp_id = ?",
            amount, empId
        );

        // 4. 插入财务记录
        jdbcTemplate.update(
            "INSERT INTO finance_record (emp_id, amount, record_type) VALUES (?, ?, 'DEDUCT')",
            empId, amount
        );

        System.out.println("[财务扣款] 处理完成,员工:" + empId);
    }

    // 模拟可能超时的审批流程
    private void simulateApprovalProcess() {
        try {
            // 模拟调用外部审批系统,可能耗时 3-10 秒
            Thread.sleep(6000); // 故意设置 6 秒,超过 5 秒超时限制
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

测试执行与结果

package com.feixiang;

import com.feixiang.service.FinanceService;
import com.feixiang.service.ReportService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class TimeoutReadOnlyTest {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx =
            new AnnotationConfigApplicationContext("com.feixiang");
        ReportService reportService = ctx.getBean(ReportService.class);
        FinanceService financeService = ctx.getBean(FinanceService.class);

        // 场景一:正常报表查询(只读 + 30 秒超时)
        try {
            List<Map<String, Object>> report = reportService.exportUserReport("ACTIVE");
            System.out.println("[场景一] 报表导出成功,记录数:" + report.size());
        } catch (Exception e) {
            System.err.println("[场景一] 报表导出失败:" + e.getClass().getSimpleName() + " - " + e.getMessage());
        }

        // 场景二:快速统计(只读 + 5 秒超时)
        try {
            int count = reportService.countActiveUsers();
            System.out.println("[场景二] 活跃用户统计:" + count);
        } catch (Exception e) {
            System.err.println("[场景二] 统计失败:" + e.getClass().getSimpleName() + " - " + e.getMessage());
        }

        // 场景三:限时扣款超时(5 秒超时)
        try {
            financeService.deductSalary(1001, 2000.00);
            System.out.println("[场景三] 扣款成功");
        } catch (Exception e) {
            System.err.println("[场景三] 扣款失败:" + e.getClass().getSimpleName() + " - " + e.getMessage());
        }

        ctx.close();
    }
}

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

场景一(正常报表查询)控制台输出:

[报表导出] 开始查询,状态过滤:ACTIVE
[报表导出] 查询完成,记录数:2
[场景一] 报表导出成功,记录数:2

场景二(快速统计)控制台输出:

[场景二] 活跃用户统计:2

场景三(限时扣款超时)控制台输出:

[财务扣款] 开始处理,员工:1001,金额:2000.0
[财务扣款] 当前余额:10000.0
[场景三] 扣款失败:TransactionTimedOutException - Transaction timed out: deadline was Wed Jun 10 10:15:00 CST 2026

场景三数据库状态:

emp_idemp_namebalance
1001大翔10000.00(未变化)

finance_record 表:无新增记录。

关键验证:尽管 deductSalary 方法在超时前已经查询到余额并准备执行扣款,但由于 simulateApprovalProcess() 耗时 6 秒,超过了 timeout = 5 的限制,Spring 在下次数据库交互前检测到超时,强制回滚事务。余额未扣减,财务记录未插入,数据一致性得到保护。

易错场景与面试考点

反例一:timeout 只限制 SQL 执行时间而非事务总时间

@Service
public class MisunderstoodService {

    @Transactional(timeout = 5)
    public void longRunningBusiness() {
        // 1. 执行一个 1 秒的 SQL
        jdbcTemplate.update("...");

        // 2. 调用外部 HTTP 接口,耗时 10 秒
        callExternalApi();  // 不涉及数据库交互

        // 3. 再执行一个 1 秒的 SQL
        jdbcTemplate.update("...");  // 此时才检测到超时!
    }
}

问题分析:timeout 限制的是事务总存活时间,但 Spring 的检测时机是在数据库交互的边界点(获取连接、执行 Statement、提交/回滚前)。如果事务中间执行了长时间非数据库操作(如 HTTP 调用、复杂计算),这些时间会计入总超时,但检测只在下次数据库操作时发生。这意味着非数据库操作期间不会立即中断,可能导致实际超时时间略长于配置值。

正确认知:timeout 是"软性超时",而非精确的实时中断。对于包含大量非数据库操作的事务,应在业务层额外设置超时控制(如 Future.get(timeout, TimeUnit.SECONDS))。

反例二:readOnly = true 中执行写操作

@Service
public class RiskyService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 错误:readOnly = true 中执行 INSERT
    @Transactional(readOnly = true)
    public void queryAndInsert(String name) {
        // 查询操作
        int count = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM app_user", Integer.class
        );

        // 危险:在只读事务中执行写操作
        jdbcTemplate.update(
            "INSERT INTO app_user (user_id, username, status) VALUES (?, ?, 'ACTIVE')",
            count + 1, name
        );
    }
}

问题分析:readOnly = true 对 JDBC 层面只是调用 Connection.setReadOnly(true),这是一个提示而非强制约束。大多数数据库驱动不会因此拒绝写操作,MySQL 的 setReadOnly 甚至只是影响查询路由(主从复制场景)。在 Hibernate/JPA 中,readOnly = true 会禁止 flush(),但原生 SQL 仍可能绕过检查。在只读事务中执行写操作可能导致:

  1. 主从复制架构下,写操作被错误地路由到从库,导致同步异常;
  2. Hibernate 的缓存状态与数据库不一致;
  3. 代码语义混乱,维护者误以为该方法是纯查询。

正确写法:写操作必须使用 readOnly = false(默认),或在方法级别显式覆盖类级别的 readOnly = true。

反例三:忽略 timeout 导致死锁引发系统雪崩

@Service
public class OrderService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 错误:未设置 timeout,死锁时连接永远不释放
    @Transactional
    public void createOrderWithInventoryCheck(int productId) {
        // 1. 先锁库存表
        jdbcTemplate.update("UPDATE inventory SET ... WHERE product_id = ?", productId);

        // 2. 再锁订单表(与另一个事务的加锁顺序相反,形成死锁)
        jdbcTemplate.update("INSERT INTO orders ...");
    }
}

问题分析:未设置 timeout 时,如果发生死锁,数据库会等待锁直到 innodb_lock_wait_timeout(MySQL 默认 50 秒)才报错。在此期间,该数据库连接被事务占用,无法归还连接池。高并发下,几十个死锁就能耗尽连接池(如 HikariCP 默认最大 10 个连接),导致所有后续请求阻塞,形成级联故障。

正确写法:

@Transactional(timeout = 10)  // 10 秒内必须完成,快速失败
public void createOrderWithInventoryCheck(int productId) {
    // 统一加锁顺序:先订单表,后库存表,避免循环等待
    jdbcTemplate.update("INSERT INTO orders ...");
    jdbcTemplate.update("UPDATE inventory SET ... WHERE product_id = ?", productId);
}

面试高频考点

Q1:@Transactional(timeout = 5) 是在第 5 秒精确中断事务吗?

不是精确中断。Spring 的超时检测是在数据库交互的边界点检查已耗时是否超过阈值。如果事务在第 3 秒执行完最后一个 SQL 后进入纯计算逻辑,直到第 10 秒才尝试提交,此时才会检测到超时并回滚。因此 timeout 是"软性超时",实际中断时间可能略晚于配置值。对于需要精确超时的场景,应结合业务层定时器或 CompletableFuture.get(timeout)。

Q2:readOnly = true 能防止写操作吗?

不能绝对防止。readOnly = true 是向底层持久层和数据库发出的优化提示,而非强制约束。JDBC 的 Connection.setReadOnly(true) 行为取决于驱动实现;Hibernate 会禁止 flush() 和脏检查,但原生 SQL 仍可能执行写操作。真正的写保护需要数据库用户权限控制或应用层代码审查。

Q3:为什么查询方法也要加 @Transactional?

两个原因:一是只读优化,readOnly = true 可以启用底层数据库的查询优化(如跳过锁升级、使用只读副本);二是连接管理,在复杂查询(如多表 JOIN、懒加载关联对象)中,@Transactional 确保整个查询过程复用同一个数据库连接,避免懒加载时 LazyInitializationException(Hibernate 的 "no Session" 错误)。但简单的单表查询可以不使用事务,由 JdbcTemplate 自动管理连接。

Q4:timeout 与数据库层面的 lock_wait_timeout 有什么区别?

timeout 是 Spring 应用层的事务存活时间限制,计量整个事务的累计耗时。lock_wait_timeout(如 MySQL 的 innodb_lock_wait_timeout)是数据库层等待单个锁的最长时间。Spring 的 timeout 通常应小于数据库的 lock_wait_timeout,确保应用层先超时回滚,释放连接,而不是让数据库层长时间持有连接等待锁。

上一页
@Transactional 的回滚规则
下一页
@TransactionalEventListener