Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL事务与并发读异常

MySQL的事务机制与并发读异常示例详解

作者:洲覆

数据库事务是一组数据库操作,它们被视为一个单独的工作单元,要么全部成功,要么全部失败,这篇文章主要介绍了MySQL事务机制与并发读异常的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

一、事务

事务是用户定义的一系列操作,这些操作需要让MySQL视为一个整体执行,这些操作要么都做,要么都不做,是一个不可分割的工作单位。

1.1 概念

前提:有并发连接访问,多条连接,并发执行 sql 语句

事务是用户定义的不可分割的操作序列,是并发控制的最小单元,这些操作要么全部执行(提交),要么全部不执行(回滚)。

目的:事务将数据库从一种一致性状态转换为另一种一致性状态;保证系统的数据完整性与可靠性。

举个例子:在银行转账中,“A 扣款 + B 收款”就是一个典型事务,要么两个操作都成功,要么都失败,不能只执行一半。

组成:

特征:

1.2 事务控制语句

-- 显示开启事务
START TRANSACTION | BEGIN

-- 提交事务,并使得已对数据库做的所有修改持久化
COMMIT

-- 回滚事务,结束用户的事务,并撤销正在进行的所有未提交的修改
ROLLBACK

-- 创建一个保存点,一个事务可以有多个保存点
SAVEPOINT identifier

-- 删除一个保存点
RELEASE SAVEPOINT identifier

-- 事务回滚到保存点
ROLLBACK TO [SAVEPOINT] identifier

1.3 ACID特性

① 原子性(A)

事务是最小执行单元,事务操作要么都做(提交),要么都不做(回滚);MySQL 通过 undo log 实现回滚,它记录了每个操作的反向操作,从而在异常时恢复数据。

② 一致性(C)

事务执行前后,数据库必须处于一致状态

事务执行前后,数据库的 “完整性约束”(如唯一键、非空约束)与 “逻辑一致性”(如转账总金额不变)不能被破坏。一致性由原子性、隔离性、持久性共同保障。

③ 隔离性(I)

描述:控制 “多个并发事务的相互影响程度”;各自运行的环境隔离。

多个事务并发执行时,应互不干扰

一个事务内部的操作和数据,在其提交前对其他事务是不可见的。

InnoDB 用 MVCC(多版本并发控制) 处理 “读 - 写并发”(读操作通过版本链读历史数据,无需加锁);用锁(表锁、页锁、行锁等) 处理 “写 - 写并发”(写操作加锁避免冲突)。

MVCC 是多版本并发控制,主要解决一致性非锁定读,通过记录和获取行版本,而不是使用锁来限制读操作,从而实现高效并发读性能。锁用来处理并发 DML 操作;数据库中提供粒度锁的策略,针对表(聚集索引 B+ 树)、页(聚集索引 B+ 树叶子节点)、行(叶子节点当中某一段记录行)三种粒度加锁;

不同隔离级别会导致不同并发现象(脏读、不可重复读、幻读)

④ 持久性(D)

事务一旦提交,数据的修改就会永久保存

无论系统宕机、断电还是重启,数据都能通过 redo log(重做日志) 恢复。

redo log 记录的是物理层面的“页修改”信息,比如修改了哪个数据页、偏移量、内容等,保证事务的最终落盘。

1.4 隔离级别

ISO 和 ANIS SQL 标准制定了四种事务隔离级别的标准,各数据库厂商在正确性和性能之间做了妥协,并没有严格遵循这些标准;MySQL innodb默认支持的隔离级别是 REPEATABLE READ;

数据库标准(ISO/ANSI SQL)定义了四种隔离级别,从低到高依次是:

隔离级别说明读操作写操作并发现象性能特点
READ UNCOMMITTED读未提交,读不加锁,性能最高但风险最大不加锁,直接读最新数据自动加 X 锁可能脏读、不可重复读、幻读性能最高,安全性最差
READ COMMITTED读已提交;读取最新已提交版本(支持 MVCC通过 MVCC 读已提交数据自动加 X 锁避免脏读,仍可能不可重复读、幻读性能较好
REPEATABLE READ(默认)可重复读,事务期间读到的是事务开始时的数据快照(支持 MVCC通过 MVCC 读事务开始前版本自动加 X 锁避免脏读、不可重复读,但仍可能幻读;MySQL 已基本避免幻读性能适中
SERIALIZABLE可串行化;所有事务顺序执行S 锁(Next-Key Lock)X 锁完全避免并发异常性能最差,最安全

1.5 命令

-- 设置隔离级别
SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 或者采用下面的方式设置隔离级别
SET @@tx_isolation = 'REPEATABLE READ';
SET @@global.tx_isolation = 'REPEATABLE READ';

-- 查看全局隔离级别
SELECT @@global.tx_isolation;

-- 查看当前会话隔离级别
SELECT @@session.tx_isolation;
SELECT @@tx_isolation;

-- 手动给读加 S 锁
SELECT ... LOCK IN SHARE MODE;

-- 手动给读加 X 锁
SELECT ... FOR UPDATE;

-- 查看当前锁信息
SELECT * FROM information_schema.innodb_locks;

二、并发读异常

2.1 脏读

事务(A)可以读到另外一个事务(B)中未提交的数据;也就是事务A读到脏数据;在读写分离的场景下,可以将 slave 节点设置为 READ UNCOMMITTED;此时脏读不影响,在 slave 上查询并不需要特别精准的返回值。

示例

seqsession Asession B
1SET @@tx_isolation='READ UNCOMMITTED';SET @@tx_isolation='READ UNCOMMITTED';
2BEGIN;
3UPDATE account_t SET money = money - 100 WHERE name = 'A';
4BEGIN;
5SELECT money FROM account_t WHERE name = 'A';
6SELECT money FROM account_t WHERE name = 'B';
7UPDATE account_t SET money = money - 100 WHERE name = 'B';
8COMMITCOMMIT

2.2 不可重复读

事务(A) 可以读到另外一个事务(B)中提交的数据;通常发生在一个事务中两次读到的数据是不一样的情况;不可重复读在隔离级别 READ COMMITTED 存在。一般而言,不可重复读的问题是可以接受的,因为读到已经提交的数据,一般不会带来很大的问题,所以很多厂商(如Oracle、SQL Server)默认隔离级别就是 READ COMMITTED;

示例

seqsession Asession B
1SET @@tx_isolation='READ COMMITTED';SET @@tx_isolation='READ COMMITTED';
2BEGIN;BEGIN;
3SELECT money FROM account_t WHERE name = 'A';
4UPDATE account_t SET money = money - 100 WHERE name = 'A';
5COMMIT;SELECT money FROM account_t WHERE name = 'A';
6COMMIT;

2.3 幻读

两次读取同一个范围内的记录得到的结果集不一样;快照读和当前读不一致;

例如:以 name 为唯一键的表,一个事务中查询 select * from t where name = 'gulu'; 不存在,接下来 insert into t(name) values ('gulu'); 出现错误,因为此时另外一个事务也执行了 insert 操作;

幻读在隔离级别 REPEATABLE READ 及以下存在;但是可以在 REPEATABLE READ 级别下通过读加锁(使用 next-key locking)解决;

示例

seqsession Asession B
1SET @@tx_isolation='REPEATABLE READ';SET @@tx_isolation='REPEATABLE READ';
2BEGIN;BEGIN;
3SELECT * FROM account_t WHERE id >= 2;
4INSERT INTO account_t(id,name,money) VALUES (4,'D',1000);
5COMMIT;
6INSERT INTO account_t(id,name,money) VALUES (4,'D',1000);
7COMMIT;

解决:给读操作加锁

seqsession Asession B
1SET @@tx_isolation='REPEATABLE READ';SET @@tx_isolation='REPEATABLE READ';
2BEGIN;BEGIN;
3SELECT * FROM account_t WHERE id >= 2 lock in share mode;
4INSERT INTO account_t(id,name,money) VALUES (4,'D',1000);
5COMMIT;
6SELECT * FROM account_t WHERE id >= 2;
7COMMIT;

2.4 丢失更新

脏读、不可重复读、幻读都是一个事务写,一个事务读,由于一个事务的写导致另一个事务读到了不该读的数据;丢失更新是两个事务都是写;丢失更新分为提交覆盖和回滚覆盖;回滚覆盖数据库拒绝不可能产生,重点关注提交覆盖;

示例

seqsession Asession B
1SET @@tx_isolation='REPEATABLE READ';SET @@tx_isolation='REPEATABLE READ';
2BEGIN;BEGIN;
3SELECT money FROM account_t WHERE name = 'A';
4SELECT money FROM account_t WHERE name = 'A';
5UPDATE account_t SET money = 1000+100 WHERE name = 'A';
6COMMIT;
7UPDATE account_t SET money = 1000-100 WHERE name = 'A';
8COMMIT;

2.5 隔离级别下并发读异常

隔离级别回滚覆盖脏读不可重复读幻读提交覆盖
READ UNCOMMITTEDnoyesyesyesyes
READ COMMITTEDnonoyesyesyes
REPEATABLE READnononoyes(手动加锁)yes(手动加锁)
SERIALIZABLEnonononono

2.6 区别

三、Redo Log(重做日志)

在 MySQL InnoDB 存储引擎中,Redo Log 是实现事务持久性的核心机制。
简单来说,它用来保证事务提交后,即使系统宕机,也能通过日志恢复到正确状态

在事务执行过程中,数据的修改首先发生在内存中的 Buffer Pool,但内存数据存在易失性,一旦断电或崩溃就会丢失。为了防止这种情况,InnoDB 设计了 Redo Log 来记录数据的物理修改。

Redo Log 由两部分组成:

在事务提交(COMMIT)的过程中,必须先将 redo log buffer 写入磁盘文件(刷盘),确认日志已经持久化后,事务才算真正提交成功。这就是事务的 WAL(Write-Ahead Logging,预写日志)机制 —— 先写日志,后写数据

Redo Log 的特性与作用

四、Undo Log(回滚日志)

与 Redo Log 相对,Undo Log 用来实现事务的原子性(Atomicity)和支持 MVCC(多版本并发控制)
如果 Redo Log 是“向前重做”,Undo Log 就是“向后回滚”。

当事务执行时,InnoDB 会为每一次数据修改生成相应的 Undo 记录,用来保存修改前的数据版本。这些 Undo 日志存储在 共享表空间(Undo Tablespace) 中。

在事务回滚(ROLLBACK)时,系统会根据 Undo Log 执行反向操作,把数据逻辑地恢复到修改前的状态:

Undo Log 的特性与作用

  1. 保证事务原子性:
    如果事务执行中途失败,可以根据 Undo Log 将已执行的部分操作撤销,确保“要么全部成功,要么全部失败”。

  2. 支撑 MVCC(多版本并发控制):
    Undo Log 记录了每行数据的历史版本,通过它 MySQL 可以在不同事务中读取到不同时刻的数据快照,从而实现非锁定读。

  3. 记录形式: Undo Log 是逻辑日志(记录操作逻辑),不同于 Redo Log 的物理页修改。

总结 

到此这篇关于MySQL事务机制与并发读异常的文章就介绍到这了,更多相关MySQL事务与并发读异常内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文