0%

MySQL数据库事务

MySQL数据库事务

本文所说的 MySQL 事务都是指在 InnoDB 引擎下,MyISAM 引擎是不支持事务的。

数据库事务指的是一组数据操作,事务内的操作要么就是全部成功,要么就是全部失败,什么都不做,其实不是没做,是可能做了一部分但是只要有一步失败,就要回滚所有操作,有点一不做二不休的意思。

假设一个网购付款的操作,用户付款后要涉及到订单状态更新、扣库存以及其他一系列动作,这就是一个事务,如果一切正常那就相安无事,一旦中间有某个环节异常,那整个事务就要回滚,总不能更新了订单状态但是不扣库存吧,这问题就大了。

数据库事务特征:ACID

原子性(Atomicity)

一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节.

一致性(Consistent)

在事务开始之前和完成之后,数据都必须保持一致状态,必须保证数据库的完整性。也就是说,数据必须符合数据库的规则。

隔离性(Isolation)

数据库允许多个并发事务同时对数据进行操作,隔离性保证各个事务相互独立,事务处理时的中间状态对其它事务是不可见的,以此防止出现数据不一致状态。可通过事务隔离级别设置:包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)

持久性(Durability)

事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

脏读、幻读、不可重复读的概念

以下几个概念是事务隔离级别要实际解决的问题,所以需要搞清楚都是什么意思。

脏读:一个事务中访问到了另外一个事务未提交的数据

脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读。

事务A 事务B
账户5000 账户5000
小明老婆花了3000
小明查看账户2000(产生脏读)
取款操作发生未知错误,事务B回滚,账户还是5000(rollback)
commit commit

事务B更新账户为2000,但是在事务B commit之前,事务A查看账户时是事务B还未提交的数据,此时事务B出现错误执行rollback,而事务A拿到的还是2000,这就是脏读。

不可重复读:同一个事务中重复读取时获得的数据不同

不可重复读指的是在同一事务内,不同的时刻读到的同一批数据可能是不一样的,可能会受到其他事务的影响,比如其他事务改了这批数据并提交了。通常针对数据更新(UPDATE)操作。

事务A 事务B
账户5000 账户5000
小明查看账户5000
小明老婆花了3000(修改该账户)
commit
小明查看账户2000
commit

在事务A中事务B修改了该账户,所以两次得到的值不同。

幻读:幻读是不可重复读的特殊场景,但是事务二的数据操作仅仅是插入和删除,不是修改数据,读取的记录数量前后不一致

幻读是针对数据插入(INSERT)或删除(DELETE)操作来说的。假设事务A对某些行的内容作了更改,但是还未提交,此时事务B插入了与事务A更改前的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现好像刚刚的更改对于某些数据未起作用,但其实是事务B刚插入进来的,让用户感觉很魔幻,感觉出现了幻觉,这就叫幻读。

事务A 事务B
查询id>100的用户(只有101)
插入id=102的用户(John)
commit
插入id=102的用户(主键冲突,幻读)
commit

幻读和不可重复读的区别

由于很多人(当然也包括本人), 容易搞混 不可重复读幻读, 这两者确实非常相似。

  • 不可重复读 主要是说多次读取一条记录, 发现该记录中某些列值被==修改==过。
  • 幻读 主要是说多次读取一个范围内的记录(包括直接查询所有记录结果或者做聚合统计), 发现结果不一致(标准档案一般指记录增多, 记录的减少应该也算是幻读)。(可以参考MySQL官方文档对 Phantom Rows 的介绍)
  • 对于 幻读, MySQL的InnoDB引擎默认的RR级别已经通过MVCC(多版本并发控制)自动帮我们解决了,具体可以参考《高性能MySQL》对 RR 隔离级别的描述

总结:

(1)不可重复读是读取了其他事务更改的数据,针对update操作

解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。

(2)幻读是读取了其他事务新增的数据,针对insert与delete操作

解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。

事务隔离级别

READ UNCOMMITTED(读未提交数据)

MySQL 事务隔离其实是依靠锁来实现的,加锁自然会带来性能的损失。而读未提交隔离级别是不加锁的,所以它的性能是最好的,没有加锁、解锁带来的性能开销。但有利就有弊,这基本上就相当于裸奔啊,所以它连脏读的问题都没办法解决。

READ COMMITTED(读已提交数据)

读提交就是一个事务只能读到其他事务已经提交过的数据,也就是其他事务调用 commit 命令之后的数据。那脏数据问题迎刃而解了。

REPEATABLE READ(可重复读)

可重复是对比不可重复而言的,不可重复读是指同一事物不同时刻读到的数据值可能不一致。而可重复读是指,事务不会读到其他事务对已有数据的修改,即使其他事务已提交,也就是说,事务开始时读到的已有数据是什么,在事务提交前的任意时刻,这些数据的值都是一样的。但是,对于其他事务新插入的数据是可以读到的,这也就引发了幻读问题。

SERIALIZABLE(串行化)

串行化是4种事务隔离级别中隔离效果最好的,解决了脏读、可重复读、幻读的问题,但是效果最差,它将事务的执行变为顺序执行,与其他三个隔离级别相比,它就相当于单线程,后一个事务的执行必须等待前一个事务结束。

读的时候加共享锁,也就是其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。

总结

从上往下,隔离强度逐渐增强,性能逐渐变差。采用哪种隔离级别要根据系统需求权衡决定,不同的事务隔离级别会导致不同的问题:

隔离级别 脏读 不可重复读 幻读
Read uncommittied(读未提交数据) 可能 可能 可能
Read committied(读已提交数据) 不可能 可能 可能
Repea read(可重复读) 不可能 不可能 可能
Serializable(可序列化) 不可能 不可能 不可能

查看和设置事务隔离级别

1
2
3
4
5
6
7
8
9
10
11
12
# 查看事务隔离级别
SELECT @@tx_isolation
show variables like 'tx_isolation'

+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+

#设置全局事务隔离级别
mysql> set global transaction isolation level read committed;

修改隔离级别的语句是:set [作用域] transaction isolation level [事务隔离级别],
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}。

其中作用于可以是 SESSION 或者 GLOBAL,GLOBAL 是全局的,而 SESSION 只针对当前会话窗口。

MySQL是如何实现事务隔离的?

首先说读未提交,它是性能最好,也可以说它是最野蛮的方式,因为它压根儿就不加锁,所以根本谈不上什么隔离效果,可以理解为没有隔离。

再来说串行化。读的时候加共享锁,也就是其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。

最后说读提交和可重复读。这两种隔离级别是比较复杂的,既要允许一定的并发,又想要兼顾的解决问题。

实现可重复读

为了解决不可重复读,或者为了实现可重复读,MySQL 采用了 MVVC (多版本并发控制) 的方式。

我们在数据库表中看到的一行记录可能实际上有多个版本,每个版本的记录除了有数据本身外,还要有一个表示版本的字段,记为 row trx_id,而这个字段就是使其产生的事务的 id,事务 ID 记为 transaction id,它在事务开始的时候向事务系统申请,按时间先后顺序递增。

事务七种传播行为:

事务传播行为类型 说明
PROPAGATION_REQUIRED 如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务。这是最常见的选择。
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRED_NEW 新建事务,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER 以非事务方式执行操作,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

InnoDB锁机制

锁机制用于管理对共享资源的并发访问。

为什么要加锁?

当多个用户并发地存取数据时,在数据库中就可能会产生多个事务同时操作同一行数据的情况,若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据的一致性。

一种典型的并发问题——丢失更新(其他锁问题及解决方法会在后面说到):

第一类丢失更新(回滚丢失,Lost update) : A事务撤销时,把已经提交的B事务的更新数据覆盖了。

事务A 事务B
开启事务A
开启事务B
查询账户余额1000
查询并汇入1000元,余额改为2000元
提交事务commit
取出1000元,把余额改为0元
撤销事务
余额1000(丢失更新)

第二类丢失更新(覆盖丢失/两次更新问题,Second lost update) : A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失

事务A 事务B
查询账户余额1000
查询账户余额1000
取出1000元,把余额改为0元
commit
汇入1000元
commit
余额2000(丢失更新)

InnoDB锁类型

锁

乐观锁与悲观锁是两种并发控制的思想,可用于解决丢失更新问题:

乐观锁会“乐观地”假定大概率不会发生并发更新冲突,访问、处理数据过程中不加锁,只在更新数据时再根据版本号或时间戳判断是否有冲突,有则处理,无则提交事务;

悲观锁会“悲观地”假定大概率会发生并发更新冲突,访问、处理数据前就加排他锁,在整个数据处理过程中锁定数据,事务提交或回滚后才释放锁;

InnoDB支持多种锁粒度,默认使用行锁,锁粒度最小,锁冲突发生的概率最低,支持的并发度也最高,但系统消耗成本也相对较高;

共享锁(Shared lock)与排他锁(Exclusive lock)是InnoDB实现的两种标准的行锁;

InnoDB有三种锁算法——记录锁、gap间隙锁、还有结合了记录锁与间隙锁的next-key锁,InnoDB对于行的查询加锁是使用的是next-key locking这种算法,一定程度上解决了幻读问题;

意向锁是为了支持多种粒度锁同时存在;

当前读与快照读

当前读:即加锁读,读取记录的最新版本,会加锁保证其他并发事务不能修改当前记录,直至获取锁的事务释放锁;

使用当前读的操作主要包括:显式加锁的读操作与插入/更新/删除等写操作,如下所示:

1
2
3
4
5
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;

注:当Update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁,待MySQL Server收到这条加锁的记录之后,会再发起一个Update请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,Update操作内部,就包含了当前读。同理,Delete操作也一样。Insert操作会稍微有些不同,简单来说,就是Insert操作可能会触发Unique Key的冲突检查,也会进行一个当前读。

快照读:即不加锁读,读取记录的快照版本而非最新版本,通过MVCC实现;

InnoDB默认的RR事务隔离级别下,不显式加『lock in share mode』与『for update』的『select』操作都属于快照读,保证事务执行过程中只有第一次读之前提交的修改和自己的修改可见,其他的均不可见;

MVCC机制

什么是MVCC?

MVCC,(Multi Version Concurrency Control的简称),多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。

可以认为MVCC是行级锁的一个变种,很多情况下避免了加锁操作,因此开销更低。

MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说不管需要执行多长时间,每个事务看到的数据都是一致的。

InnoDB的MVCC是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。

下面看一下在REPEATABLE READ隔离级别下,MVCC具体是如何操作的。

  • SELECT

    InnoDB会根据以下两个条件检查每行记录:

    1. InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
    2. 行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。

    只有符合上述两个条件的记录,才能返回作为查询结果

  • INSERT

    InnoDB为新插入的每一行保存当前系统版本号作为行版本号。

  • DELETE

    InnoDB为删除的每一行保存当前系统版本号作为行删除标识。

  • UPDATE

    InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。

    保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作

MVCC只在可重复度和读已提交两个隔离级别下工作。因为读未提交总是读取最新的数据行,串行化则会对所有读取的行都加锁。


本文结束啦感谢您阅读