MySQL中的事务基本使用和详解。

191 阅读7分钟

MySQL中的事务基本使用和详解

事务是数据库管理系统(DBMS)的核心概念之一,用于确保数据操作的原子性、一致性、隔离性和持久性(ACID)。本文将系统讲解MySQL中事务的基本概念、使用方法和实现原理。


一、事务基础概念

1. 什么是事务?

事务是一组逻辑操作单元,要么全部执行成功,要么全部不执行。就像银行转账:

	-- 转账示例(伪代码)
	BEGIN;
	UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
	UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
	COMMIT;

2. 事务的ACID特性

特性解释
原子性(A)事务是不可分割的工作单位,要么全成功,要么全失败
一致性(C)事务执行前后,数据库从一个一致状态变为另一个一致状态
隔离性(I)并发事务之间互不干扰
持久性(D)事务一旦提交,其结果就是永久性的

二、MySQL事务的基本操作

1. 事务控制语句

	-- 1. 开启事务
	START TRANSACTION;  -- 或 BEGIN
	-- 2. 执行SQL语句
	UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
	UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
	-- 3. 提交事务(使更改永久生效)
	COMMIT;
	-- 4. 回滚事务(撤销所有更改)
	ROLLBACK;

2. 自动提交模式

MySQL默认开启自动提交模式(autocommit=1),每条SQL语句都会自动作为一个事务执行。

	-- 查看当前自动提交模式
	SHOW VARIABLES LIKE 'autocommit';
	-- 临时关闭自动提交(当前会话)
	SET autocommit = 0;
	-- 重新开启自动提交
	SET autocommit = 1;

3. 保存点(Savepoint)

允许在事务中设置标记点,可以回滚到特定保存点而非整个事务。

	START TRANSACTION;
	UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
	-- 设置保存点

	SAVEPOINT sp1;
	UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
	-- 发现问题,回滚到保存点
	ROLLBACK TO SAVEPOINT sp1;
	-- 继续执行其他操作
	UPDATE accounts SET balance = balance + 50 WHERE account_id = 3;
	COMMIT;  -- 只有第一条更新和最后一条更新会被提交

三、事务隔离级别

1. 隔离级别概述

隔离级别决定了事务之间的可见性和并发程度,MySQL支持4种隔离级别:

隔离级别脏读不可重复读幻读描述
READ UNCOMMITTED(读未提交)可能可能可能最低级别,可能读取到其他事务未提交的数据
READ COMMITTED(读已提交)不可能可能可能只能读取到已提交的数据
REPEATABLE READ(可重复读)不可能不可能可能MySQL默认级别,保证同一事务中多次读取结果一致
SERIALIZABLE(可串行化)不可能不可能不可能最高级别,完全串行执行,性能最低

2. 查看和设置隔离级别

	-- 查看当前会话隔离级别
	SELECT @@transaction_isolation;  -- MySQL 8.0+
	-- 或
	SELECT @@tx_isolation;  -- MySQL 5.7及以下

	-- 查看全局隔离级别

	SELECT @@global.transaction_isolation;
	-- 设置会话隔离级别
	SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
	-- 设置全局隔离级别(需要SUPER权限)
	SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

3. 隔离级别示例

场景:两个事务同时操作账户余额

	-- 事务1
	START TRANSACTION;
	SELECT balance FROM accounts WHERE account_id = 1;  -- 假设余额1000
	-- 此时事务2提交了更新,将余额改为900
	SELECT balance FROM accounts WHERE account_id = 1;  -- 不同隔离级别下结果不同
	COMMIT;
  • READ UNCOMMITTED:第二次查询可能看到900
  • READ COMMITTED:第二次查询会看到900
  • REPEATABLE READ:两次查询都看到1000
  • SERIALIZABLE:事务2会被阻塞,直到事务1完成

四、事务相关问题

1. 脏读(Dirty Read)

一个事务读取到另一个事务未提交的数据。

	-- 事务1
	START TRANSACTION;
	UPDATE accounts SET balance = 800 WHERE account_id = 1;
	-- 不提交
	-- 事务2
	START TRANSACTION;
	SELECT balance FROM accounts WHERE account_id = 1;  -- 可能读取到800(脏读)
	ROLLBACK;

2. 不可重复读(Non-repeatable Read)

同一事务中多次读取同一数据,结果不一致(其他事务修改了数据)。

	-- 事务1
	START TRANSACTION;
	SELECT balance FROM accounts WHERE account_id = 1;  -- 1000
	-- 事务2提交了更新
	SELECT balance FROM accounts WHERE account_id = 1;  -- 900(不可重复读)
	COMMIT;

3. 幻读(Phantom Read)

同一事务中多次查询,结果集数量不同(其他事务插入了新数据)。

	-- 事务1
	START TRANSACTION;
	SELECT COUNT(*) FROM orders WHERE status = 'pending';  -- 10条
	-- 事务2插入了新订单
	SELECT COUNT(*) FROM orders WHERE status = 'pending';  -- 11条(幻读)
	COMMIT;

4. 丢失更新(Lost Update)

两个事务同时读取并修改同一数据,后提交的事务覆盖了先提交的事务的修改。

	-- 事务1
	START TRANSACTION;
	SELECT balance INTO @bal FROM accounts WHERE account_id = 1;  -- 1000
	SET @new_bal = @bal - 100;
	UPDATE accounts SET balance = @new_bal WHERE account_id = 1;
	-- 等待提交
	-- 事务2
	START TRANSACTION;
	SELECT balance INTO @bal FROM accounts WHERE account_id = 1;  -- 1000
	SET @new_bal = @bal - 50;
	UPDATE accounts SET balance = @new_bal WHERE account_id = 1;
	COMMIT;  -- 事务2先提交,余额变为950
	-- 事务1提交
	COMMIT;  -- 事务1后提交,余额变为900(覆盖了事务2的修改)

解决方案:使用SELECT ... FOR UPDATE加锁

	START TRANSACTION;
	SELECT balance INTO @bal FROM accounts WHERE account_id = 1 FOR UPDATE;
	SET @new_bal = @bal - 100;
	UPDATE accounts SET balance = @new_bal WHERE account_id = 1;
	COMMIT;

五、事务最佳实践

1. 事务设计原则

  1. 短事务:事务执行时间越短,锁竞争越小
  2. 明确边界:避免在事务中执行耗时操作(如网络请求)
  3. 合理使用隔离级别:根据业务需求选择最低必要的隔离级别
  4. 避免嵌套事务:MySQL不支持真正的嵌套事务

2. 典型事务场景

	-- 订单创建事务示例
	START TRANSACTION;
	-- 1. 扣除库存
	UPDATE products 
	SET stock = stock - 1 
	WHERE product_id = 1001 AND stock >= 1;
	-- 检查是否影响行数(库存是否足够)
	IF ROW_COUNT() = 0 THEN
	    ROLLBACK;
	    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '库存不足';
	END IF;
	-- 2. 创建订单
	INSERT INTO orders (user_id, product_id, quantity, amount, create_time)
	VALUES (100, 1001, 1, 99.99, NOW());
	-- 3. 记录订单明细
	INSERT INTO order_items (order_id, product_id, quantity, price)
	VALUES (0, 1001, 1, 99.99);
	COMMIT;

3. 分布式事务简介

对于跨多个数据库服务的事务,MySQL原生不支持分布式事务,但可以通过以下方式实现:

  1. XA事务:两阶段提交协议
  2. 消息队列+最终一致性:如RocketMQ、RabbitMQ
  3. Saga模式:将长事务拆分为多个本地事务,通过补偿事务回滚

六、事务相关命令

1. 查看当前事务

	-- 查看当前运行的事务
	SELECT * FROM information_schema.INNODB_TRX;
	-- 查看锁等待情况
	SELECT * FROM information_schema.INNODB_LOCK_WAITS;

2. 杀死阻塞事务

	-- 找到阻塞事务的trx_mysql_thread_id
	SELECT * FROM information_schema.INNODB_LOCK_WAITS;
	-- 杀死该线程(需要足够权限)
	KILL [thread_id];

七、常见问题解答

1. 事务越大越好吗?

不是。大事务会导致:

  • 锁持有时间过长,降低并发性
  • 回滚成本高
  • 增加死锁概率

建议:将大事务拆分为多个小事务。

2. 为什么SELECT不需要事务?

SELECT语句默认是自动提交的,但以下情况需要事务:

  • 需要可重复读(REPEATABLE READ)一致性视图
  • 需要加锁(FOR UPDATE/LOCK IN SHARE MODE)

3. 隐式提交的语句

以下操作会隐式提交当前事务:

  • DDL语句(CREATE, ALTER, DROP等)
  • 事务控制语句(START TRANSACTION, BEGIN等除外)
  • 锁表语句(LOCK TABLES, UNLOCK TABLES)
  • 加载数据的语句(LOAD DATA INFILE)

4. 如何避免死锁?

死锁预防策略:

  1. 按固定顺序访问表和行
  2. 保持事务简短
  3. 避免事务中用户交互
  4. 使用较低的隔离级别
  5. 为表添加适当的索引

通过掌握这些事务基础知识和实践技巧,你可以设计出更可靠、更高效的数据库应用。在实际开发中,建议结合业务场景选择合适的隔离级别,并通过监控工具(如SHOW ENGINE INNODB STATUS)诊断事务相关问题。