MySQL 事务全解析:从 ACID 特性到实战落地(部门 - 员工场景)

76 阅读11分钟

在企业级系统开发中,“数据一致性” 是核心诉求 —— 比如员工入职时,“新增员工记录” 和 “更新部门人数” 这两个操作必须同时成功或同时失败;再比如财务转账时,“扣款” 和 “收款” 也不能只执行其一。而保证这种 “要么全成、要么全败” 的核心技术,就是数据库事务(Transaction) 。本文将以 “部门 - 员工” 这一经典业务场景为核心,从事务的核心价值、ACID 特性、隔离级别,到 JDBC/MyBatis/Spring 中的实战落地,帮你彻底掌握事务的使用与优化。

一、事务的核心价值:为什么离不开事务?

先看一个典型的业务场景:

企业招聘新员工 “钱八” 入职研发部,需要执行两个操作:

  1. emp表插入 “钱八” 的员工记录;
  2. dept表更新研发部的员工数量(emp_count字段),加 1。

如果没有事务控制,可能出现两种异常:

  • 场景 1:插入员工记录成功,但更新部门人数失败 → 研发部人数少算 1 人,数据不一致;
  • 场景 2:更新部门人数成功,但插入员工记录失败 → 研发部人数多算 1 人,数据错误。

而事务的核心作用,就是将一组操作封装为一个 “不可分割的执行单元” ,确保单元内的所有操作要么全部执行成功,要么全部回滚(撤销),从根本上避免数据不一致问题。

二、事务的核心特性:ACID 原则

事务的本质是遵循ACID 四大特性的一组数据库操作,这是事务的核心定义,我们结合 “部门 - 员工” 场景逐一拆解:

特性英文全称核心定义部门 - 员工场景示例
原子性(A)Atomicity事务中的操作要么全部执行成功,要么全部回滚,不存在 “部分成功”入职操作:插入员工 + 更新部门人数,要么都成,要么都回滚(比如员工姓名重复导致插入失败,部门人数也不更新)
一致性(C)Consistency事务执行前后,数据库的 “业务规则” 保持一致(如部门人数 = 该部门员工数)执行前研发部人数是 2,执行后必须是 3(成功)或仍为 2(失败),不会出现 “员工新增但人数不变” 的不一致
隔离性(I)Isolation多个事务并发执行时,彼此隔离、互不干扰,避免并发导致的数据错误管理员 A 新增员工、管理员 B 统计部门人数,两个事务并发执行时,B 不会读到 A “未提交的中间数据”
持久性(D)Durability事务提交后,修改会永久保存到数据库,即使数据库崩溃也不会丢失入职操作提交后,“钱八” 的员工记录和研发部人数的修改会永久保存,重启数据库后数据仍有效

关键说明:ACID 是事务的 “契约”,其中原子性、一致性、持久性由数据库的 “重做日志(Redo Log)” 和 “回滚日志(Undo Log)” 保证,隔离性由事务隔离级别和锁机制保证。

三、事务的隔离级别:解决并发事务冲突

在多用户并发操作数据库时,若没有隔离性控制,会出现脏读、不可重复读、幻读三类问题。MySQL 通过 “事务隔离级别” 来平衡 “隔离性” 和 “并发性能”,共定义了 4 个隔离级别(从低到高):

1. 四类并发问题(先理解问题,再看解决方案)

问题类型定义部门 - 员工场景示例
脏读(Dirty Read)一个事务读取到另一个事务 “未提交” 的修改数据事务 A 新增员工 “钱八” 但未提交,事务 B 查询研发部人数为 3;事务 A 回滚后,事务 B 读到的 “3” 就是 “脏数据”
不可重复读(Non-repeatable Read)一个事务内多次读取同一数据,结果不一致(被其他事务 “修改并提交”)事务 B 第一次查研发部人数为 2,事务 A 新增员工并提交,事务 B 再次查询人数为 3,两次结果不同
幻读(Phantom Read)一个事务内多次执行同一查询,结果集行数不一致(被其他事务 “插入 / 删除并提交”)事务 B 查询 “薪资> 15000 的研发部员工” 有 2 人,事务 A 新增 1 名高薪员工并提交,事务 B 再次查询变为 3 人,如同 “出现幻觉”

2. 四种隔离级别(解决并发问题的不同程度)

MySQL 默认隔离级别为REPEATABLE READ(可重复读),也是生产环境最常用的级别。

隔离级别英文全称解决的问题允许的问题性能
读未提交READ UNCOMMITTED无(最低隔离)脏读、不可重复读、幻读最高
读已提交READ COMMITTED脏读不可重复读、幻读较高
可重复读(默认)REPEATABLE READ脏读、不可重复读幻读(MySQL 已通过 MVCC 解决)中等
串行化SERIALIZABLE所有问题(完全隔离)无(并发事务串行执行)最低

关键说明

  • MySQL 的REPEATABLE READ通过 “多版本并发控制(MVCC)” 额外解决了幻读问题,是比标准定义更优的实现;
  • 隔离级别越高,并发性能越低,需根据业务场景选择(如财务系统用SERIALIZABLE,普通业务用REPEATABLE READ)。

3. 查看 / 设置隔离级别

-- 查看当前会话隔离级别
SELECT @@SESSION.tx_isolation;
-- 查看全局隔离级别
SELECT @@GLOBAL.tx_isolation;

-- 设置当前会话隔离级别为读已提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置全局隔离级别为可重复读(默认)
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

四、事务的基础操作语法(MySQL 实战)

1. 核心语法

MySQL 默认开启 “自动提交(AUTOCOMMIT)”,即每条 SQL 语句都是一个独立事务。需手动关闭自动提交或显式开启事务。

-- 1. 关闭自动提交(当前会话有效)
SET AUTOCOMMIT = 0;

-- 2. 开启事务(显式声明事务开始)
BEGIN; -- 或 START TRANSACTION;

-- 3. 执行事务内的操作(如插入员工+更新部门人数)
INSERT INTO `emp` (emp_name, emp_salary, hire_date, dept_id) 
VALUES ('钱八', 14000.00, '2024-01-01', 1);

UPDATE `dept` SET emp_count = emp_count + 1 WHERE dept_id = 1;

-- 4. 提交事务(所有操作生效,持久化到数据库)
COMMIT;

-- 4. 回滚事务(撤销所有操作,回到事务开始前的状态)
-- ROLLBACK;

-- 5. 保存点(可选,回滚到指定位置,而非整个事务)
SAVEPOINT sp1; -- 设置保存点
ROLLBACK TO sp1; -- 回滚到保存点

2. 实战案例(部门 - 员工入职)

先给dept表新增emp_count字段(用于统计部门人数):

ALTER TABLE `dept` ADD COLUMN `emp_count` INT DEFAULT 0 COMMENT '部门员工数';
-- 初始化现有部门人数
UPDATE `dept` SET emp_count = 2 WHERE dept_id = 1; -- 研发部2人
UPDATE `dept` SET emp_count = 1 WHERE dept_id = 2; -- 人事部1人
UPDATE `dept` SET emp_count = 1 WHERE dept_id = 3; -- 财务部1人
UPDATE `dept` SET emp_count = 0 WHERE dept_id = 4; -- 市场部0人

执行带事务的入职操作:

-- 步骤1:开启事务
BEGIN;

-- 步骤2:执行操作1:插入新员工
INSERT INTO `emp` (emp_name, emp_salary, hire_date, dept_id, manager_id) 
VALUES ('钱八', 14000.00, '2024-01-01', 1, 1);

-- 步骤3:执行操作2:更新研发部人数
UPDATE `dept` SET emp_count = emp_count + 1 WHERE dept_id = 1;

-- 步骤4:验证数据(事务未提交,仅当前会话可见)
SELECT * FROM `emp` WHERE emp_name = '钱八'; -- 能查到
SELECT emp_count FROM `dept` WHERE dept_id = 1; -- 3

-- 步骤5:提交事务(数据持久化)
COMMIT;

-- 若操作失败,执行回滚:ROLLBACK;

五、实战落地:不同开发场景下的事务使用

1. 原生 JDBC 中的事务控制

JDBC 默认也是自动提交事务,需手动关闭自动提交,通过Connection对象控制事务:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class JdbcTransactionDemo {
    // 数据库连接信息
    private static final String URL = "jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useSSL=false";
    private static final String USER = "root";
    private static final String PASSWORD = "123456";

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstmt1 = null;
        PreparedStatement pstmt2 = null;

        try {
            // 1. 获取连接
            conn = DriverManager.getConnection(URL, USER, PASSWORD);
            // 2. 关闭自动提交(开启手动事务)
            conn.setAutoCommit(false);

            // 3. 操作1:插入新员工
            String sql1 = "INSERT INTO emp (emp_name, emp_salary, hire_date, dept_id, manager_id) VALUES (?, ?, ?, ?, ?)";
            pstmt1 = conn.prepareStatement(sql1);
            pstmt1.setString(1, "钱八");
            pstmt1.setBigDecimal(2, new java.math.BigDecimal("14000.00"));
            pstmt1.setDate(3, java.sql.Date.valueOf("2024-01-01"));
            pstmt1.setLong(4, 1L);
            pstmt1.setLong(5, 1L);
            pstmt1.executeUpdate();

            // 4. 操作2:更新部门人数
            String sql2 = "UPDATE dept SET emp_count = emp_count + 1 WHERE dept_id = ?";
            pstmt2 = conn.prepareStatement(sql2);
            pstmt2.setLong(1, 1L);
            pstmt2.executeUpdate();

            // 5. 提交事务(所有操作生效)
            conn.commit();
            System.out.println("入职操作成功,事务提交!");

        } catch (SQLException e) {
            e.printStackTrace();
            // 6. 发生异常,回滚事务
            try {
                if (conn != null) {
                    conn.rollback();
                    System.out.println("操作失败,事务回滚!");
                }
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        } finally {
            // 7. 关闭资源
            try {
                if (pstmt1 != null) pstmt1.close();
                if (pstmt2 != null) pstmt2.close();
                if (conn != null) {
                    conn.setAutoCommit(true); // 恢复自动提交
                    conn.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

2. MyBatis 中的事务控制

MyBatis 的事务默认由SqlSession控制,默认关闭自动提交,需手动commit()rollback()

(1)编写 Mapper 接口和 XML
// EmpMapper.java
public interface EmpMapper {
    // 插入员工
    int insertEmp(Emp emp);
}

// DeptMapper.java
public interface DeptMapper {
    // 更新部门人数
    int updateDeptCount(@Param("deptId") Long deptId, @Param("count") int count);
}
<!-- EmpMapper.xml -->
<insert id="insertEmp">
    INSERT INTO emp (emp_name, emp_salary, hire_date, dept_id, manager_id)
    VALUES (#{empName}, #{empSalary}, #{hireDate}, #{deptId}, #{managerId})
</insert>

<!-- DeptMapper.xml -->
<update id="updateDeptCount">
    UPDATE dept SET emp_count = emp_count + #{count} WHERE dept_id = #{deptId}
</update>
(2)MyBatis 事务实战
import org.apache.ibatis.session.SqlSession;

public class MyBatisTransactionDemo {
    public static void main(String[] args) {
        // 1. 获取SqlSession(默认autoCommit=false)
        try (SqlSession session = MyBatisUtil.getSqlSession(false)) {
            // 2. 获取Mapper
            EmpMapper empMapper = session.getMapper(EmpMapper.class);
            DeptMapper deptMapper = session.getMapper(DeptMapper.class);

            // 3. 构建员工对象
            Emp emp = new Emp();
            emp.setEmpName("钱八");
            emp.setEmpSalary(new java.math.BigDecimal("14000.00"));
            emp.setHireDate(java.sql.Date.valueOf("2024-01-01"));
            emp.setDeptId(1L);
            emp.setManagerId(1L);

            // 4. 执行操作
            empMapper.insertEmp(emp);
            deptMapper.updateDeptCount(1L, 1);

            // 5. 提交事务
            session.commit();
            System.out.println("入职操作成功,事务提交!");

        } catch (Exception e) {
            e.printStackTrace();
            // 6. 异常回滚(try-with-resources会自动关闭session,触发rollback)
            System.out.println("操作失败,事务回滚!");
        }
    }
}

3. Spring 声明式事务(生产环境首选)

在 Spring/Spring Boot 项目中,推荐使用@Transactional注解实现 “声明式事务”,无需手动控制commit/rollback,更简洁高效:

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class EmpDeptService {
    // 注入Mapper
    private final EmpMapper empMapper;
    private final DeptMapper deptMapper;

    public EmpDeptService(EmpMapper empMapper, DeptMapper deptMapper) {
        this.empMapper = empMapper;
        this.deptMapper = deptMapper;
    }

    // 声明事务:默认发生RuntimeException时回滚
    @Transactional(rollbackFor = Exception.class) // 捕获所有异常回滚
    public void addEmpWithDept(Emp emp) {
        // 1. 插入员工
        empMapper.insertEmp(emp);
        // 2. 更新部门人数
        deptMapper.updateDeptCount(emp.getDeptId(), 1);
        
        // 模拟异常(测试回滚)
        // int i = 1 / 0;
    }
}

关键说明

  • @Transactional默认只捕获RuntimeException,需通过rollbackFor = Exception.class指定捕获所有异常;
  • 可通过isolation属性设置隔离级别(如isolation = Isolation.REPEATABLE_READ);
  • 可通过propagation属性设置事务传播行为(如REQUIRED:默认,有事务则加入,无则新建)。

六、事务的常见问题与最佳实践

1. 常见问题排查

  • 事务不回滚:检查是否捕获了异常(未抛出异常则 Spring 不会回滚)、是否是非运行时异常(需指定rollbackFor)、是否调用了非 public 方法(@Transactional仅对 public 方法生效);
  • 长事务导致性能问题:长事务会占用数据库连接、持有锁时间长,导致并发下降,需尽量缩小事务范围;
  • 死锁:多个事务互相等待对方持有的锁,可通过SHOW ENGINE INNODB STATUS查看死锁日志,优化 SQL 执行顺序。

2. 核心最佳实践

(1)尽量缩小事务范围

只将 “必须原子执行” 的操作放入事务,避免无关操作(如日志打印、远程调用):

@Transactional
public void addEmpWithDept(Emp emp) {
    // 事务内:核心操作(必须原子)
    empMapper.insertEmp(emp);
    deptMapper.updateDeptCount(emp.getDeptId(), 1);
}

// 事务外:无关操作
public void logEmpAdd(Emp emp) {
    logger.info("新增员工:{}", emp.getEmpName());
}
(2)避免长事务
  • 不在事务内执行耗时操作(如文件上传、第三方接口调用);
  • 批量操作可拆分为多个小事务(如每 1000 条数据提交一次)。
(3)合理设置隔离级别
  • 普通业务:使用 MySQL 默认的REPEATABLE READ
  • 高并发读业务:可降级为READ COMMITTED(提升性能);
  • 财务 / 核心数据:使用SERIALIZABLE(保证绝对一致性)。
(4)避免手动锁与事务冲突

尽量利用事务的隔离级别和 MVCC,而非手动加锁(如SELECT ... FOR UPDATE),手动锁易导致死锁。

(5)监控事务执行情况

生产环境需监控长事务、死锁、事务失败率,可通过 MySQL 的information_schema表查询:

-- 查询当前运行的事务
SELECT * FROM information_schema.INNODB_TRX;
-- 查询死锁日志
SHOW ENGINE INNODB STATUS;

七、总结与拓展

本文以 “部门 - 员工” 场景为核心,讲解了事务的 ACID 特性、隔离级别、基础语法,以及 JDBC/MyBatis/Spring 中的实战落地,核心要点总结:

  1. 事务的核心目标:保证一组操作的原子性,避免数据不一致,遵循 ACID 四大特性;

  2. 隔离级别选择:默认用REPEATABLE READ,根据业务场景平衡 “隔离性” 和 “并发性能”;

  3. 实战建议

    • 原生 JDBC/MyBatis:手动控制commit/rollback,关闭自动提交;
    • Spring 项目:优先使用@Transactional声明式事务,简化开发;
  4. 性能优化:缩小事务范围、避免长事务、监控死锁和长事务。

拓展方向:

  • 分布式事务:跨多个数据库 / 服务的事务,可使用 Seata、TCC、SAGA 等方案;
  • 事务日志:了解 Redo Log/Undo Log 的工作原理,理解事务持久性和回滚的底层实现;
  • 锁机制:深入学习 InnoDB 的行锁、表锁、间隙锁,理解隔离级别与锁的关系。

事务是数据库保证数据一致性的核心机制,也是企业级开发中必须掌握的技能。掌握事务的使用与优化,不仅能避免数据错误,还能提升系统的并发性能和稳定性。