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

在企业级系统开发中,"数据一致性" 是核心诉求 ------ 比如员工入职时,"新增员工记录" 和 "更新部门人数" 这两个操作必须同时成功或同时失败;再比如财务转账时,"扣款" 和 "收款" 也不能只执行其一。而保证这种 "要么全成、要么全败" 的核心技术,就是数据库事务(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. 查看 / 设置隔离级别

sql 复制代码
-- 查看当前会话隔离级别
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 语句都是一个独立事务。需手动关闭自动提交或显式开启事务。

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字段(用于统计部门人数):

sql 复制代码
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人

执行带事务的入职操作:

sql 复制代码
-- 步骤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对象控制事务:

java 复制代码
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
java 复制代码
// EmpMapper.java
public interface EmpMapper {
    // 插入员工
    int insertEmp(Emp emp);
}

// DeptMapper.java
public interface DeptMapper {
    // 更新部门人数
    int updateDeptCount(@Param("deptId") Long deptId, @Param("count") int count);
}
xml 复制代码
<!-- 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 事务实战
java 复制代码
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,更简洁高效:

java 复制代码
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)尽量缩小事务范围

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

java 复制代码
@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表查询:

sql 复制代码
-- 查询当前运行的事务
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 的行锁、表锁、间隙锁,理解隔离级别与锁的关系。

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

相关推荐
码界奇点2 小时前
基于Spring Boot和Vue.js的视频点播管理系统设计与实现
java·vue.js·spring boot·后端·spring·毕业设计·源代码管理
爱吃山竹的大肚肚2 小时前
MySQL 支持的各类索引
java·数据库·sql·mysql·spring·spring cloud
黑白极客2 小时前
mysql的 order by是怎么工作的?redo-log和binlog为什么采用双确认机制?
数据库·mysql
程序员水自流2 小时前
MySQL常用内置函数详细介绍
java·数据库·mysql
慌糖2 小时前
开发当中常见注解备注
数据库·sql
TAEHENGV2 小时前
关于应用模块 Cordova 与 OpenHarmony 混合开发实战
android·javascript·数据库
赵思空2 小时前
window docker 安装 mysql 数据库,及不能连接问题
数据库·mysql·docker
yinshuilan2 小时前
Postgresql安装配置
数据库·postgresql
回家路上绕了弯2 小时前
分布式事务本地消息表详解:中小团队的低侵入落地方案
分布式·后端