MySQL 事务(4)

原创
2020/10/21 18:58
阅读数 2K

什么是事务?

数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。

这里有两个关键点,第一,它是数据库最小的工作单元,是不可以再分的。第二,它可能包含了一个或一系列DML语句,包括insert delete update。(单条DDL(create drop) 和DCL(grant revoke)也会有事务)

事务的四大特性(ACID)

特性 说明
原子性(Atomicity) 事务中的全部操作在数据库中是不可分割的,要么全部完成,要么全部不执行。
一致性(Consistent) 几个并行执行的事务,其执行结果必须与按某一顺序 串行执行的结果相一致。
隔离性(Isolation) 事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。
持久性(Durability) 对于任意已提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障。
原子性(Atomicity)

以转账为例,一个账户的余额减少,对应一个账户增加这两个一定是同时成功或同时失败。

全部成功好理解,问题是如果前一个操作已经成功,后一个操作失败了,如何让他全部失败呢?这时候我们必须要回滚。

原子性,在InnoDB里面是通过undo log来实现的,它记录了数据之前的值(逻辑日志),一旦发生异常,就可以用undo log来实现回滚操作。

一致性(Consistent)

还以转账为例,A账户余额有500元,这时有两个转账请求过来,都要转账500元,如果两个请求都执行成功,A账户的余额就成-500元,但这是错误的,储蓄卡的余额是不能小于0的,所以违反了一致性。完整性通常需要用户在代码中控制。

隔离性(Isolation)

有了事务的定义以后,在数据库里面会有很多的事务同时去操作我们的同一张表或者同一行数据,必然会产生一些并发或者干扰的操作,那么我们对隔离性的定义,就是这些很多个的事务,对表或者行的并发操作,应该是透明的,互相不干扰的。通过这种方式,我们最终也是保证业务数据的一致性。

像一致性的转账例子,应该是第一个事务转账成功,第二个事务提示余额不足。

持久性(Durability)

事务的持久性是什么意思呢?我们对数据库的任意的操作,增删改,只要事务提交成功,那么结果就是永久性的,不可能因为我们系统宕机或者重启了数据库的服务器,它又恢复到原来的状态了。这个就是事务的持久性。可以理解为数据已被写入到存储介质上。

持久性怎么实现呢?数据库崩溃恢复(crash-safe)是通过什么实现的?

持久性是通过redo log和double write双写缓冲来实现的,我们操作数据的时候,会先写到内存的buffer pool里面,同时记录redo log,如果在刷盘之前出现异常,在重启后就可以读取redo log的内容,写入到磁盘,保证数据的持久性。

当然,恢复成功的前提是数据页本身没有被破坏,是完整的,这个通过双写缓冲(double write)保证。

原子性,隔离性,持久性,最后都是为了实现一致性。

事务并发会遇到哪些问题?

当很多事务并发操作数据库的表或行时,如果没有我们刚才讲的事务隔离性时,会带来哪些问题?

如上图有两个事务,事务A先查询id=1的这行数据,之后事务B修改age=18,但未提交,此时事务A再次查询id=1的数据,这行数据age变成了18。这种在一个事务里,由于其他事务修改了数据并没有提交,而导致了前后两次读取数据不一致的情况,这种事务并发问题,我们将之定义为脏读

如果在转账案例里面,我们第一个事务读取到第二个事务未提交的余额进行了操作,但第二个事务进行了回滚,这个时候会导致数据不一致。

这种读取其他事务未提交的数据情况,我们叫做脏读

同样两个事务,A事务通过id=1查到一条数据。然后在第二个事务里执行一个update操作,并且修改了提交。然后A事务继续查询id=1的数据,此时A事务的前后两次查询数据不一致,像这种age到底等于16还是18,那么这种事务并发带来的问题,我们定义为不可重复读

这种一个事务读取到了其他事务已提交的数据导致前后两次读取数据不一致的情况,我们把它叫做不可重复读。

在上图中,我们在A事务中执行了一个范围查询,这个时候满足条件的数据只有一条。在B事务里面插入了一行新数据,并且提交了。此时在A事务再次查询时,会发现多了一条数据。这种情况我们定义为幻读

不可重复读和幻读的区别在哪里?

不可重复读是修改或者删除,幻读是插入。

小结:上边说的三大问题。无论是脏读、不可重复读、幻读,都是数据库的读一致性问题,都是在一个事务前后两次读取出现了不一致的情况。

读一致性的问题,必须要由数据库提供一定的事务隔离机制来解决。就像我们去饭店吃饭,基本的设施和卫生保证都是饭店提供的。那么我们使用数据库,隔离性的问题也必须由数据库帮助我们来解决。

MySql InnoDB 事务隔离级别

在MySQL InnoDB里面,不需要使用串行化的隔离级别去解决所有问题。我们来看一下MySQL InnoDB里面对数据库事务隔离级别的支持程度是什么样的。

事务隔离级别 脏读 不可重复读 幻读
未提交读(Read Uncommitted) 可能 可能 可能
已提交读(Read Committed) 不可能 可能 可能
可重复读(Repeatable Read) 不可能 不可能 对InnoDB不可能
串行化(Serializable) 不可能 不可能 不可能

InnoDB支持的四个隔离级别和SQL92定义的基本一致,隔离级别越高,事务的并发度就越低。唯一的区别就在于,InnoDB在RR的级别就解决了幻读的问题。这个也是InnoDB默认使用RR作为事务隔离级别的原因,既保证了数据的一致性,又支持较高的并发度。

实现方案

如果要解决读一致性的问题,保证一个事务中前后两次读取数据结果一致,实现事务隔离,你会怎么做?

一、基于锁的并发控制LBCC(Lock Based Concurrency Control)

既然要保证前后两次读取数据一致,那么我读取数据的时候,锁定我要操作的数据,不允许其他事务修改就行了。这种方案我们叫做基于锁的并发控制

如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那就意味着不支持并发的读写操作,而大多数应用都是读多写少的,这样会极大地影响操作数据的效率。

二、多版本的并发控制 MVCC(Multi Version Concurrency Control)

如果要让一个事务前后两次读取的数据保持一致,那么我们可以在修改数据的时候给他建立一个备份或者叫快照,后面再来读取这个快照就行了。这种方案我们叫做多版本的并发控制

MVCC的核心思想是:我可以查到在我这个事务开始之前已经存在的数据,即使它在后面被修改或者删除了。在我这个事务之后新增的数据,我是查不到的。

那么这个快照什么时候创建?读取数据的时候,怎么保证能读取到这个快照而不是最新的数据?该如何实现?

InnoDB为每行记录都实现了4个隐藏字段:

字段名 长度 说明
DB_ROW_ID 6字节 行标识
DB_TRX_ID 6字节 插入或更新行的最后一个事务的事务ID,事务编号是自动递增的(可以理解为创建版本号,在数据新增或者修改为新数据的时候,记录当前事务ID)。
DB_ROLL_PTR 7字节 回滚指针(可以理解为删除版本号,数据被删除或记录为旧数据的时候,记录当前事务ID)。
DELETE_FLAG 删除标记

通过以上演示,通过事务ID的控制,无论其他事务是插入、修改、删除,第一个事务查询到的数据都没有变化。

可以将各事务中的查询逻辑按以下条件理解

select * from user_innoDB 
where DB_TRX_ID <= 2 or DB_ROLL_PTR <= 2;

隔离级别

事务隔离级别 脏读 不可重复读 幻读
未提交读(Read Uncommitted) 可能 可能 可能
已提交读(Read Committed) 不可能 可能 可能
可重复读(Repeatable Read) 不可能 不可能 对InnoDB不可能
串行化(Serializable) 不可能 不可能 不可能
Read Uncommitted (RU)

RU隔离级别:不加锁。

Serializable

Serializable所有select语句都会被隐式的转化为select ... in share mode ,会和update、delete互斥。

Repeatable Read(RR)

RR隔离级别下,普通的select 使用快照读(snapshot read),底层使用MVCC来实现。

加锁的select 以及更新操作update、delete等语句使用当前读(current read),底层使用记录锁、间隙锁、临键锁

Read Committed(RC)

RC隔离级别下,普通的select 都是快照读,使用MVCC实现。

加锁的select 都使用记录锁,因为没有Gap Lock。

除了两种特殊情况

  1. 外检约束检查(foreign-key constraint checking)
  2. 重复键检查(duplicate-key checking)

以上两种会使用间隙锁封锁区间。所以RC会出现幻读的问题。

事务隔离级别如何选择?

RU和Serializable肯定不能用。为什么有些公司要用RC?

RC和RR的主要有几个区别:

  1. RR的间隙锁会导致锁定范围的扩大。
  2. 条件列未使用到索引,RR锁表,RC锁行。
  3. RC的“半一致性”(semi-consistent)读可以增加update操作的并发性。

在RC中,一个update语句,如果读到一行已经加锁记录,此时InnoDB返回记录最近提交的版本,由MySQL上层判断此版本是否满足update的where条件。若满足(需要更新),则MySQL会重新发起一次读操作,此时会读取行的最新版本(并加锁)。

实际上,如果能够正确地使用锁(避免不使用索引加锁),只锁定需要的数据,用默认的RR级别就可以。

展开阅读全文
打赏
0
1 收藏
分享
加载中
更多评论
打赏
0 评论
1 收藏
0
分享
返回顶部
顶部