4、MySQL事务处理
4.1、 简介
事务(Transaction)指作为一个逻辑工作单元执行一系列操作,这些操作要么全部成功,要么全部失败,确保对多个数据的操作当成一个单元来处理。
- MySQL中只有使用了Innodb存储引擎的数据库或表才支持事务处理。
- 事务用于维护数据库的完整性,保证成批的sql语句要么都执行,要么都不执行,尽可能保证数据的完整。
- 事务用于管理 INSERT、UPDATE、DELETE 语句。
比如我们在银行转账,A给B转了1000,成功则A账户减少1000,B账户增加1000,在转的过程中如果失败,那么A和B的账户余额都不会改变,这就是事务成功。假设没有事务,如果A转账失败了,但是A的账户余额少了,而B却没有收到钱,那么就会乱套。
我们在处理SQL时,会根据情况将同一组的SQL语句放到一个事务中去处理,这样只要有一个语句执行失败,整组的SQL语句就全部失败。
事务的原则(ACID):
1、原子性(Atomicity):事务必须是原子工作单位,事务中的操作要么都要做,要么都不做。
2、一致性(Consistency):事务在执行完成时,必须使所有数据保持一致状态。(转账,双方的钱总数在转账前后是一致的)
3、隔离性(Isolation):事务独立运行,多事务间互相隔离,互不干扰。100%的隔离,会牺牲一部分的速度
4、持久性(Durability):事务执行完成后,对系统的影响是持久的,不会随外界原因导致数据丢失。
以下几个概念是事务隔离级别要实际解决的问题
1、脏读:一个事务读取了另外一个事务尚未提交的数据(事务A更新了数据,但还没有提交,这时事务B读取到事务A更新后的数据,然后事务A回滚并没有保存到数据库中,这时候事务B读取到的数据就成为脏数据了)
2、不可重复读:一个事务在读取数据时,多次读取的结果不同(事务A对数据进行多次读取,事务B在事务A多次读取的过程中执行了更新操作并提交了,导致事务A多次读取到的数据并不一致)(update操作)
3、幻读:幻读是针对数据delete、insert操作来说的。一个事务使用sql两次读取数据,第二次读取了新插入的数据(事务A在读取数据后,事务B向事务A读取的数据中插入了几条数据,事务A再次读取数据时发现多了几条数据,和之前读取的数据不一致)
事务隔离级别:从上往下,隔离强度逐渐增强,性能逐渐变差。
1、读到未提交的数据(READ UNCOMMITTED):所有事务都能看到其他事务未提交的执行结果。不能解决脏读、不可重复读、幻读(基本不使用)
2、读到提交内容(READ COMMITTED):一个事务提交之后,它的修改才能被其他事务看到。可解决脏读,不能解决不可重复读、幻读
3、可重复读(REPEATABLE READ)( MySQL 的默认级别。):同一个事物的多个实例在并发读取时,会获得相同的数据。能解决脏读、不可重复读,不能解决幻读
4、串行化(SERIALIZABLE):需要加锁实现,会强制事务串行执行。能解决脏读、不可重复读、幻读。(但会导致大量的超时现象和锁竞争)
4.2、MVCC
MySQL使用的事务隔离级别为可重复度,使用了以乐观锁为理论基础的MVCC 实现的,即 Mutil-Version Concurrency Control(多版本并发控制)。
InnoDB 在每行记录后面保存两个隐藏的列,分别保存了数据行的创建版本号(记录这行数据何时被创建)和删除版本号(记录这行数据何时过期)。实际保存的就是是事务的版本号,每开启一个新事务,事务的版本号就会递增。
在A事务开始时,系统会从操作数据中读取一个版本号,这个版本就作为A事务的版本号,A事务会读取版本号<=当前版本号的数据。如果一个B事务修改了这条数据,那么这条数据的版本号会+1,此时A事务再读取数据时,读取的依然是修改前的数据。
MVCC对增删改查的规定:
- SELECT:必须同时满足以下两个条件,才能查询到。
- 只查版本号早于当前版本的数据行。创建版本号<=当前事务版本号
- 行的删除版本要么未定义,要么大于当前事务版本号。(未定义即插入的数据从未进行修改过):删除版本号为空或>当前事务版本号
- INSERT:存当前事务版本号为行的创建版本号,此时删除版本号为空。
- DELETE:保存当前事务版本号为行的删除版本号
- UPDATE:保存当前系统版本号作为创建版本号,同时保存当前系统版本号作为原来的数据行删除版本号
比如:插入一条数据,假设开始的版本号为1
![]()
修改数据
![]()
对这个数据进行修改
![]()
对数据进行删除
![]()
MVCC这种读取出来的数据其实是历史数据,而不是最新数据,我们称为快照读(snapshot read)。而读取数据库当前版本数据的方式,叫当前读(current read)。我们平时只用使用select就是快照读,这样可以减少加锁所带来的开销。对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式。
4.3、版本链、ReadView
我们知道可重复读是无法解决幻读这个问题的,所以InnoDB 在MVCC机制上增加了版本链来解决幻读的问题。
InnoDB在每行数据后增加三个隐藏字段,
- ROW_ID:行ID,随着插入新行而单调递增(这个是非必须的,因为如果我们设置了主键,就不会包含ROW_ID了)。
- TRX_ID:每次对数据进行改动时,都会记录下改动事务的ID。
- ROLL_PTR:回滚指针,每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
![image-20220805180832623](https://i-blog.csdnimg.cn/blog_migrate/83a4c8c96a54d8b8e925dac687bc3d35.png)
每次数据修改,旧数据会保存在 undo日志中,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表称为版本链,头节点就是最新的值。
同时InnoDB存在一个ReadView的概念,它帮助我们识别那些版本是当前事务可见的。每开启一个事务时,系统会给该事务会分配一个事务 Id,在事务进行select时,会生成当前时间点的事务快照 ReadView,它主要包含一下属性:
- trx_ids:记录当前系统中活跃的事务 Id 列表
- up_limit_id:低水位,取 trx_ids 中最小的那个,trx_id 小于该值都能看到。
- low_limit_id:高水位,生成 ReadView 时系统将要分配给下一个事务的id值,trx_id 大于等于该值都不能看到。
- creator_trx_id:生成该 ReadView 的事务的事务 Id。
在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:
- 如果被访问版本的trx_id与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id小于 low_limit_id,表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问
- 如果被访问版本的trx_id大于ReadView中的low_limit_id值,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。
- 如果被访问版本的trx_id属性值在ReadView的up_limit_id和low_limit_id之间,那就需要判断一下trx_id属性值是不是在trx_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
在进行判断时,首先会拿记录的最新版本来比较,如果这个版本数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本,如果最后一个版本也不可见的话,那么就意味着该条记录对该事务不可见,查询结果就不包含该记录。
删除并不是真的删除,可以把它当成一种特殊的更新,InnoDB 用一个额外的标记位 delete_bit 标识是否删除。当我们在进行判断时,会检查下 delete_bit 是否被标记,如果是,则跳过该版本,通过 DB_ROLL_PTR 拿到下一个版本进行判断。
(ReadView和版本链解决了快照读(普通 select 语句就是快照读)的幻读问题,对于当前读(读取数据的最新版本)的幻读,MySQL在 RR 级别下是默认开启Gap 锁,通过Gap锁可以防止幻读)。
4.4、锁
数据库管理系统DBMS中并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性。主要手段是乐观控制和悲观控制
-
乐观锁:持乐观的态度,认为数据变动不会太频繁,因此允许多个事务同时对数据进行变动,仅在提交时检查是否违反数据的完整性。在修改数据时通过version的方式进行锁定。更新时带上版本号(cas更新)
-
悲观锁:持悲观的态度,认为数据容易发生冲突,所以屏蔽一切可能违反数据完整性的操作,在查询时就将其锁定,直到提交了事务(使用共享锁和排它锁)
- 共享锁:读锁,多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
- 排他锁:写锁,不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,获取排他锁的事务可以对数据就行读取和修改
4.5、事务的使用
MySQL是默认开启事务自动提交的。
-- 事务事务自动提交的开启和关闭
SET autocommit = 0 -- 关闭
SET autocommit = 1 -- 开启(默认的)
-- 事务流程
SET autocommit = 0 -- 设置自动提交关闭
START TRANSACTION -- 标记事务开启,以下的sql语句都在同一个事务内
SQL.....
SQL.....
...
COMMIT -- 提交成功,数据持久化
ROLLBACK -- 提交失败,数据回滚,回到原来的数据
SET autocommit = 1 -- 事务结束,开启自动提交
-- 其中可以设置保存点,类似我们玩游戏时的存档功能
SAVEPOINT 名 -- 保存点
ROLLBACK TO SAVEPOINT 名-- 回滚到一个保存点
RELEASE SAVEPOINT 名 -- 撤销一个保存点