0%

mysql 事务入门

[TOC]

以下无特殊说明,都是 mysql InnoDB 引擎下讨论。

基本性质

1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。

2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。

3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。

4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

事务的并发问题

  • 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
  • 提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
  • 可重复读(Repeated Read):可重复读。在同一个事务内的查询都与事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读。
  • 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

事务隔离等级

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
(读已提交)不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

脏读,不可重复读,幻读

1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据

2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。

3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A修改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

查询MySQL隔离等级及设置隔离等级

1
2
3
4
5
6
7
8
9
10
11
12
# 查询隔离等级
select @@tx_isolation;

# 设置隔离等级为 read commiteed
SET session transaction isolation level read committed;
SET SESSION binlog_format = 'ROW';(或者是MIXED)

SET session transaction isolation level read uncommitted;

SET session transaction isolation level repeatable read;

SET session transaction isolation level serializable;

加锁

我们都知道事务的几种性质,数据库为了维护这些性质,尤其是一致性和隔离性,一般使用加锁这种方式。同时数据库又是个高并发的应用,同一时间会有大量的并发访问,如果加锁过度,会极大的降低并发处理能力。所以对于加锁的处理,可以说就是数据库对于事务处理的精髓所在。这里通过分析MySQL中InnoDB引擎的加锁机制,来抛砖引玉,让读者更好的理解,在事务处理中数据库到底做了什么。

一次封锁or两段锁?

因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。这种方式可以有效的避免循环死锁,但在数据库中却不适用,因为在事务开始阶段,数据库并不知道会用到哪些数据。

数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)

  • 加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得X锁(排它锁,其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
  • 解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。

事务中的加锁方式

MySQL中锁的种类很多,有常见的表锁和行锁,表锁是对一整张表加锁,虽然可分为读锁和写锁,但毕竟是锁住整张表,会导致并发能力下降,一般是做ddl处理时使用。行锁则是锁住数据行,这种加锁方法比较复杂,但是由于只锁住有限的数据,对于其它数据不加限制,所以并发能力强,MySQL一般都是用行锁来处理并发事务。这里主要讨论的也就是行锁。

Read Committed(读取提交内容)

如果一个条件无法通过索引快速过滤,存储引擎层面就会将所有记录加锁后返回,再由MySQL Server层进行过滤。

但在实际使用过程当中,MySQL做了一些改进,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录释放锁 (违背了二段锁协议的约束)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。

这种情况同样适用于MySQL的默认隔离级别RR。所以对一个数据量很大的表做批量修改的时候,如果无法使用相应的索引,MySQL Server过滤数据的的时候特别慢,就会出现虽然没有修改某些行的数据,但是它们还是被锁住了的现象。

Repeatable Read(可重读)

不可重复读和幻读的区别

很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert。

如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别。

悲观锁和乐观锁

悲观锁就是加锁,但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。

乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。**读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据,需要重新做读取数据,更新数据的尝试,直到成功**。

这就引出了 MVCC

Multi-Version Concurrency Control 多版本并发控制,MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问;在编程语言中实现事务内存。

MVCC在MySQL的InnoDB中的实现

数据帧

可以看到在InnoDB中,每一行都有2个隐藏列DATA_TRX_ID和DATA_ROLL_PTR(如果没有定义主键,则还有个隐藏主键列):

  1. DATA_TRX_ID表示最近修改该行数据的事务ID (数据行版本号)。
  2. DATA_ROLL_PTR则表示指向该行回滚段的指针,该行上所有旧的版本,在undo中都通过链表的形式组织,而该值,就是指向undo中该行的历史记录链表(删除版本号)。

整个MVCC的关键就是通过DATA_TRX_ID和DATA_ROLL_PTR这两个隐藏列来实现的。

MVCC 使用了一种不同的手段,每个连接到数据库的读者,在某个瞬间看到的是数据库的一个快照,写者写操作造成的变化在写操作完成之前(或者数据库事务提交之前)对于其他的读者来说是不可见的。

当一个 MVCC 数据库需要更新一条数据记录的时候,它不会直接用新数据覆盖旧数据,而是将旧数据标记为过时(obsolete)并在别处增加新版本的数据。这样就会有存储多个版本的数据,但是只有一个是最新的。这种方式允许读者读取在他读之前已经存在的数据,即使这些在读的过程中半路被别人修改、删除了,也对先前正在读的用户没有影响。这种多版本的方式避免了填充删除操作在内存和磁盘存储结构造成的空洞的开销,但是需要系统周期性整理(sweep through)以真实删除老的、过时的数据。

DATA_TRX_ID, DATA_ROLL_PTR

  • MVCC逻辑流程-插入:插入的过程中会把全局事务ID记录到列 DB_TRX_ID 中去。

  • MVCC逻辑流程-删除:删除操作会把全局事务ID记录到 DB_ROLL_PT 中,不更改 DB_TRX_ID 。

  • MVCC逻辑流程-修改:修改数据的时候 会先复制一条当前记录行数据,同时标记这条数据的数据行版本号为当前是事务版本号,最后把原来的数据行的删除版本号标记为当前事务版本号。(相当于插入了一条,删除了一条

  • MVCC逻辑流程-查询:

    • 查找数据行版本号早于当前事务版本号的数据行记录。

      • 也就是说,数据行的版本号要小于或等于当前是事务的系统版本号,这样也就确保了读取到的数据是当前事务开始前已经存在的数据,或者是自身事务改变过的数据。(不会查到新增的数据和因为修改而新增的数据
    • 查找删除版本号要么为NULL,要么大于当前事务版本号的记录。

      • 这样确保查询出来的数据行记录在事务开启之前没有被删除。(确保被删除的数据和被修改的数据能被查到

MVCC 示例

创建 table

1
2
3
4
5
6
7
drop table test_zq;
create table test_zq (id int (11),test_id int (11));
begin;
insert into `test_zq` (`id`, `test_id`) values('1','18');
insert into `test_zq` (`id`, `test_id`) values('4','8');
commit;
select * from test_zq;

示例1

事务A 事务B
begin; begin;
select * from test_zq;
update test_zq set test_id = 100 where id = 1;
insert into test_zq select 5, 101;
commit;
select * from test_zq; (操作A3)
commit;
select * from test_zq;

有赖于 MVCC 的控制,在操作A3 中保证了,和事务A刚开始时读取到的数据是相同的。保证了可重复读

“快照读”与“当前读”的区别

我们且看,在RR级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,是不及时的数据,不是数据库当前的数据!这在一些对于数据的时效特别敏感的业务中,就很可能出问题。

对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。很显然,在MVCC中:

  • 快照读:就是select
    • select * from table ….;
  • 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。
    • select * from table where ? lock in share mode;
    • select * from table where ? for update;
    • insert;
    • update ;
    • delete;

MVCC 相当于只保证了快照读的可重复读及不发生幻读,但是如何保证当前读的不发生幻读,这就是 Next-key 锁了。

Next-Key锁 (临键锁)

Next-Key锁(临键锁)是行锁和GAP(间隙锁)的合并,行锁上文已经介绍了,接下来说下GAP间隙锁。行锁可以防止不同事务版本的数据修改提交时造成数据冲突的情况。但如何避免别的事务插入数据就成了问题。我们可以看看RR级别和RC级别的对比

RC下的当前读的幻读及RR下当前读的无幻读

准备数据

1
insert into class_teacher select NULL, '初三一班', 30;

RC 下执行

事务A 事务 B
begin; begin;
select id,class_name,teacher_id from class_teacher where teacher_id=30;
update class_teacher set class_name=’初三四班’ where teacher_id=30;
insert into class_teacher values (null,’初三二班’,30);
commit; (这里先提交了,因为可以提交成功)
select id,class_name,teacher_id from class_teacher where teacher_id=30; 当前读产生了幻读,因为明明在当前事务已经将所有teacher_id=30的数据修改了,但是还是有teacher_id=30的数据。
commit;
select * from class_teacher;

RR 下 执行

事务A 事务 B
begin; begin;
select id,class_name,teacher_id from class_teacher where teacher_id=30;
update class_teacher set class_name=’初三四班’ where teacher_id=30;
insert into class_teacher values (null,’初三二班’,30);
waiting (主要的区别就是这里会阻塞,因为加了锁)
select id,class_name,teacher_id from class_teacher where teacher_id=30;
commit;
commit;
select * from class_teacher;

通过对比我们可以发现,在 RC 级别中,事务 A 修改了所有 teacher_id=30 的数据,但是当事务 B insert 进新数据后,事务A发现莫名其妙多了一行 teacher_id=30 的数据,而且没有被之前的 update 语句所修改,这就是“当前读”的幻读。

RR级别中,事务A在update后加锁,事务B无法插入新数据,这样事务A在update前后读的数据保持一致,避免了幻读。这个锁,就是Gap锁。

间隙锁 + 行锁解决 RR 当前读的幻读问题

next-key 示例

添加需要的数据

1
2
3
insert into class_teacher select NULL, '初三一班', 5;
insert into class_teacher select NULL, '初三二班', 30;
select * from class_teacher;

执行事务

事务A 事务B 事务C
begin; begin; begin;
select * from class_teacher;
update class_teacher set class_name=’初一一班’ where teacher_id=20;
insert into class_teacher values (null,’初三五班’,10); insert into class_teacher values (null,’初三五班’,40);
waiting …..(阻塞了,直到A commit) commit; (随时都可以commit,没有被锁住)
select * from class_teacher; 成功
commit;
commit;(事务A commit之后,阻塞才会结束)
select * from class_teacher;

update的teacher_id=20是在(5,30]区间,即使没有修改任何数据,Innodb也会在这个区间加gap锁,而其它区间不会影响,事务C正常插入。

如果使用的是没有索引的字段,比如update class_teacher set teacher_id=7 where class_name=‘初三八班(即使没有匹配到任何数据)’,那么会给全表加入gap锁。同时,它不能像上文中行锁一样经过MySQL Server过滤自动解除不满足条件的锁,因为没有索引,则这些字段也就没有排序,也就没有区间。除非该事务提交,否则其它事务无法插入任何数据。

行锁防止别的事务修改或删除,GAP锁防止别的事务新增,行锁和GAP锁结合形成的的Next-Key锁(临键锁)共同解决了RR级别在写数据时的幻读问题。

Serializable

这个级别很简单,读加共享锁,写加排他锁,读写互斥。使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差。如果你的业务并发的特别少或者没有并发,同时又要求数据及时可靠的话,可以使用这种模式。

示例

演示table

1
2
3
4
5
6
7
8
drop table class_teacher;
CREATE TABLE class_teacher (
`id` int(11) NOT NULL AUTO_INCREMENT,
`class_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`teacher_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_teacher_id` (`teacher_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

究极总结认真看

**RR 模式下,MySQL 实际上实现了 可重复读,并且解决了大部分的幻读问题。这是由以下两个东西解决的。MVCC 解决了快照读的不可重复读和幻读问题,而 Next-key Lock 解决了 当前读 不可重复读的问题及 部分的幻读问题。为什么是部分的幻读问题,这是因为加的是一个间隙锁,在间隙之外的区间,仍然会出现幻读问题 **

参考

美团技术团队:Innodb中的事务隔离级别和锁的关系

https://www.cnblogs.com/huanongying/p/7021555.html

轻松理解MYSQL MVCC 实现机制

https://juejin.im/post/5c68a4056fb9a049e063e0ab