外观
MariaDB 事务与锁
事务的基本概念
事务是数据库中一组不可分割的操作序列,要么全部执行成功,要么全部执行失败。事务用于保证数据的完整性和一致性,是数据库并发控制的基础。
事务的 ACID 特性
- 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败回滚
- 一致性(Consistency):事务执行前后,数据库从一个一致性状态转换到另一个一致性状态
- 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务
- 持久性(Durability):事务一旦提交,其结果应该永久保存在数据库中
事务的生命周期
- 开始事务:使用
START TRANSACTION或BEGIN命令 - 执行操作:执行一系列 SQL 语句
- 提交事务:使用
COMMIT命令,将事务的修改永久保存到数据库 - 回滚事务:使用
ROLLBACK命令,撤销事务的所有修改
示例:
sql
-- 开始事务
START TRANSACTION;
-- 执行操作
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 提交事务
COMMIT;
-- 或者回滚事务
-- ROLLBACK;事务隔离级别
事务隔离级别定义了多个并发事务之间的隔离程度,不同的隔离级别提供不同的一致性保证和并发性能。
隔离级别的类型
MariaDB 支持以下四种隔离级别:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 并发性能 |
|---|---|---|---|---|
| 读未提交(READ UNCOMMITTED) | 可能 | 可能 | 可能 | 最高 |
| 读已提交(READ COMMITTED) | 不可能 | 可能 | 可能 | 较高 |
| 可重复读(REPEATABLE READ) | 不可能 | 不可能 | 可能 | 中等 |
| 串行化(SERIALIZABLE) | 不可能 | 不可能 | 不可能 | 最低 |
隔离级别的设置
查看当前隔离级别:
sql
-- 全局隔离级别
SELECT @@global.tx_isolation;
-- 会话隔离级别
SELECT @@session.tx_isolation;设置隔离级别:
sql
-- 设置全局隔离级别
SET GLOBAL tx_isolation = 'REPEATABLE-READ';
-- 设置会话隔离级别
SET SESSION tx_isolation = 'READ-COMMITTED';在 my.cnf 中配置默认隔离级别:
ini
[mysqld]
transaction-isolation = REPEATABLE-READ各隔离级别的具体表现
1. 读未提交(READ UNCOMMITTED)
- 允许一个事务读取另一个未提交事务的数据
- 可能导致脏读、不可重复读和幻读
- 适用于对数据一致性要求不高的场景
2. 读已提交(READ COMMITTED)
- 只能读取已提交事务的数据
- 避免了脏读,但可能导致不可重复读和幻读
- 适用于大多数 Web 应用场景
3. 可重复读(REPEATABLE READ)
- 保证同一事务中多次读取同一数据的结果一致
- 避免了脏读和不可重复读,但可能导致幻读
- MariaDB 默认隔离级别
- 通过 MVCC(多版本并发控制)实现
4. 串行化(SERIALIZABLE)
- 完全隔离并发事务,相当于单线程执行
- 避免了脏读、不可重复读和幻读
- 并发性能最低,适用于对数据一致性要求极高的场景
锁的基本概念
锁是数据库用于控制并发访问的机制,通过锁可以防止多个事务同时修改同一资源,从而保证数据的一致性。
锁的作用
- 保证数据一致性:防止多个事务同时修改同一数据导致数据不一致
- 实现事务隔离:通过锁机制实现不同的事务隔离级别
- 协调并发访问:合理分配资源,提高系统并发性能
锁的类型
1. 按粒度划分
表级锁
- 对整个表加锁
- 开销小,加锁快
- 锁定粒度大,并发度低
- 适用于 MyISAM 存储引擎
表级锁类型:
- 表共享读锁(Table Read Lock):允许其他事务读,不允许写
- 表独占写锁(Table Write Lock):不允许其他事务读和写
示例:
sql
-- 加表共享读锁
LOCK TABLES users READ;
-- 加表独占写锁
LOCK TABLES users WRITE;
-- 释放锁
UNLOCK TABLES;页级锁
- 对数据页加锁(通常为 16KB)
- 开销和加锁速度介于表级锁和行级锁之间
- 锁定粒度中等,并发度中等
- 适用于 BDB 存储引擎
行级锁
- 对单行数据加锁
- 开销大,加锁慢
- 锁定粒度小,并发度高
- 适用于 InnoDB 存储引擎
行级锁类型:
- 共享锁(S Lock):允许读,不允许写
- 排他锁(X Lock):不允许读和写
示例:
sql
-- 加共享锁(读锁)
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- 加排他锁(写锁)
SELECT * FROM users WHERE id = 1 FOR UPDATE;2. 按功能划分
意向锁
- 表级锁,用于表示事务将要对表中的行加锁
- 意向共享锁(IS Lock):事务准备对表中的行加共享锁
- 意向排他锁(IX Lock):事务准备对表中的行加排他锁
间隙锁
- 锁定索引记录之间的间隙,防止幻读
- 只在可重复读(REPEATABLE READ)和串行化(SERIALIZABLE)隔离级别下生效
- 锁定范围包括:
- 索引记录之间的间隙
- 索引记录之前的间隙
- 索引记录之后的间隙
示例:
sql
-- 锁定 id 在 100 到 200 之间的间隙
SELECT * FROM users WHERE id BETWEEN 100 AND 200 FOR UPDATE;临键锁
- 间隙锁和行锁的组合
- 锁定索引记录及其之前的间隙
- 是 InnoDB 行级锁的默认算法
记录锁
- 直接锁定索引记录
- 只锁定索引记录本身,不包括间隙
锁的状态查看
查看表锁状态:
sql
SHOW OPEN TABLES WHERE In_use > 0;查看行锁状态:
sql
-- 查看 InnoDB 锁状态
SHOW ENGINE INNODB STATUS;
-- 查看锁等待情况
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
SELECT * FROM information_schema.INNODB_TRX;锁的等待和死锁
锁等待
当一个事务请求的锁被另一个事务占用时,该事务会进入等待状态,直到锁被释放。锁等待可能导致事务执行时间延长,影响系统性能。
查看锁等待超时时间:
sql
SELECT @@innodb_lock_wait_timeout;设置锁等待超时时间:
sql
-- 全局设置
SET GLOBAL innodb_lock_wait_timeout = 60;
-- 会话设置
SET SESSION innodb_lock_wait_timeout = 60;死锁
死锁是指两个或多个事务相互等待对方释放锁,导致所有事务都无法继续执行的状态。
死锁示例:
sql
-- 事务 A
START TRANSACTION;
UPDATE users SET name = 'A' WHERE id = 1;
-- 等待事务 B 释放 id=2 的锁
UPDATE users SET name = 'A' WHERE id = 2;
-- 事务 B
START TRANSACTION;
UPDATE users SET name = 'B' WHERE id = 2;
-- 等待事务 A 释放 id=1 的锁
UPDATE users SET name = 'B' WHERE id = 1;死锁的检测和处理
死锁检测
MariaDB 默认开启死锁检测,通过 innodb_deadlock_detect 参数控制:
sql
SELECT @@innodb_deadlock_detect;死锁处理
- 自动检测和回滚:InnoDB 会自动检测死锁,并回滚代价较小的事务
- 手动处理:当死锁检测关闭时,需要手动终止事务
查看死锁日志:
sql
SHOW ENGINE INNODB STATUS;手动终止事务:
sql
-- 查看当前事务
SELECT * FROM information_schema.INNODB_TRX;
-- 终止指定事务
KILL TRANSACTION_ID;事务和锁的最佳实践
1. 合理设计事务
- 保持事务短小:事务执行时间越短,锁持有时间越短,并发性能越高
- 避免在事务中执行非数据库操作:如调用外部 API、等待用户输入等
- 避免在事务中执行大量查询:减少事务持有时间
2. 选择合适的隔离级别
- 根据业务需求选择合适的隔离级别
- 大多数场景下,使用默认的 REPEATABLE READ 隔离级别即可
- 对于读多写少的场景,可以考虑使用 READ COMMITTED 隔离级别提高并发性能
3. 优化锁的使用
- 避免锁定不必要的数据:只锁定必要的行和列
- 使用索引减少锁定范围:确保查询使用索引,避免全表扫描导致的表级锁
- 避免长时间持有锁:尽快提交或回滚事务
- 使用乐观锁:对于并发冲突较少的场景,可以使用版本号或时间戳实现乐观锁
4. 避免死锁
- 统一访问顺序:在多个事务中,按照相同的顺序访问表和行
- 减少事务并发度:通过合理设计业务逻辑,减少事务并发执行
- 使用更细粒度的锁:尽量使用行级锁,避免表级锁
- 设置合理的锁等待超时时间:避免事务无限期等待
5. 监控和调优
- 定期监控锁等待情况:及时发现和解决锁等待问题
- 分析死锁日志:找出死锁原因,优化业务逻辑
- 调整事务隔离级别:根据实际情况调整隔离级别
- 优化查询语句:减少锁持有时间
版本差异
MariaDB 10.0+
- 引入了乐观锁机制
- 增强了 MVCC 实现
MariaDB 10.1+
- 引入了 Galera Cluster,支持分布式事务
- 增强了死锁检测机制
MariaDB 10.2+
- 支持事务读写分离
- 增强了 InnoDB 锁机制
MariaDB 10.3+
- 引入了动态列,支持事务操作动态列
- 增强了事务监控功能
MariaDB 10.4+
- 默认事务隔离级别仍然是 REPEATABLE READ
- 增强了 InnoDB 并行复制,提高了事务处理性能
常见问题(FAQ)
1. 如何选择合适的事务隔离级别?
建议:
- 对于大多数 Web 应用,使用 READ COMMITTED 或 REPEATABLE READ 隔离级别
- 对于对数据一致性要求极高的场景,使用 SERIALIZABLE 隔离级别
- 对于对并发性能要求极高且数据一致性要求不高的场景,使用 READ UNCOMMITTED 隔离级别
2. 如何避免死锁?
建议:
- 统一事务中表的访问顺序
- 保持事务短小,尽快提交或回滚
- 避免在事务中执行复杂的查询和非数据库操作
- 使用索引减少锁定范围
- 设置合理的锁等待超时时间
3. 如何处理锁等待问题?
建议:
- 优化查询语句,减少锁持有时间
- 增加系统资源,提高并发处理能力
- 调整事务隔离级别
- 分析锁等待原因,优化业务逻辑
4. 什么情况下会导致表级锁?
可能原因:
- 使用 MyISAM 存储引擎
- 查询没有使用索引,导致全表扫描
- 执行 ALTER TABLE、DROP TABLE 等 DDL 操作
- 手动使用 LOCK TABLES 命令加表锁
5. 如何查看当前正在执行的事务?
方法:
sql
SELECT * FROM information_schema.INNODB_TRX;6. 如何查看死锁日志?
方法:
sql
SHOW ENGINE INNODB STATUS;7. 如何优化事务性能?
建议:
- 保持事务短小
- 避免在事务中执行非数据库操作
- 使用合适的隔离级别
- 优化查询语句,减少锁持有时间
- 增加系统资源,提高并发处理能力
8. 什么是 MVCC?
MVCC(多版本并发控制)是 InnoDB 存储引擎实现事务隔离的机制,通过保存数据的多个版本,实现并发读写操作的隔离。MVCC 可以提高系统的并发性能,避免读写操作之间的阻塞。
9. 如何手动终止一个长时间运行的事务?
方法:
sql
-- 查看当前事务
SELECT * FROM information_schema.INNODB_TRX;
-- 终止指定事务
KILL TRANSACTION_ID;10. 如何监控事务和锁的使用情况?
建议:
- 使用 MariaDB 自带的状态监控工具:
SHOW ENGINE INNODB STATUS - 查询 information_schema 中的事务和锁相关表
- 使用第三方监控工具,如 Prometheus、Grafana 等
- 定期分析慢查询日志和错误日志
总结
事务和锁是 MariaDB 数据库中的核心概念,对于保证数据一致性和提高并发性能至关重要。DBA 需要深入理解事务的 ACID 特性、隔离级别、锁的类型和机制,以及如何处理锁等待和死锁问题。
在实际生产环境中,DBA 应该根据业务需求选择合适的事务隔离级别,优化事务设计,合理使用锁机制,避免死锁和锁等待问题,并定期监控和调优事务和锁的使用情况。
通过本文的介绍,相信您对 MariaDB 事务和锁有了更深入的了解,能够在实际生产环境中更好地管理和优化事务和锁,提高数据库的性能和可靠性。
