MySQL:事务

事务:保证一组数据库操作,要么全部成功,要么全部失败。

在MySQL中,事务支持是引擎层实现的。MyISAM引擎不支持事务,InnoDB支持事务。

begin/start transaction命令并不是一个事务的起点,在执行到它们之后的第一个操作,事务才真正启动

一、事务的隔离级别

1.1 事务的ACID属性

ACID属性含义
原子性(Atomicity)事务是一个原子操作单元,其对数据的修改,要么全部成功,要么全部失败。
一致性(Consistent)在事务开始和完成时,数据都必须保持一致状态。
隔离性(Isolation)数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的 “独立” 环境下运行。
持久性(Durable)事务完成之后,对于数据的修改是永久的。

1.2 并发事务带来的问题

问题含义
脏写(Lost Update)当两个或多个事务选择同一行,最初的事务修改的值,后面的事务回滚会使最初的事务修改失效。
脏读(Dirty Reads)当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
不可重复读(Non-Repeatable Reads)一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现和以前读出的数据不一致。本质是一个事务期间能读到其他事务已提交的内容。
幻读(Phantom Reads)一个事务按照相同的查询条件重新读取以前查询过的数据,却发现其他事务插入了满足其查询条件的新数据。

 脏写:

事务A事务B
开启事务
开启事务
update user set username=“张三” where id=5;
提交事务
update user set username=“李四” where id=5;
ROllBACK;

若两个事务开启前id为5的姓名为王五。事务B回滚的话,就会回滚到最原始的记录,恢复为王五,使事务A的写入失效。

 脏读:

事务A事务B
开启事务
开启事务
update user set username=“张三” where id=5;
select * from user where id =5;
提交事务
提交事务

事务A还未提交时,事务B读取到username为张三的记录,这种情况就是脏读。若此时事务A突然回滚,事务B再去读就会读取到与刚刚不同的值。

不可重复读:

事务A事务B
开启事务
select * from user where id =5; //读取到的是小王
update user set username=“张三” where id=5;//这里有个隐式事务。自动提交
select * from user where id =5; //读取到的是张三
提交事务

 本质是一个事务期间能读到其他事务已提交的内容。

幻读

事务A事务B
开启事务
select * from user where id =5; //读取到的是小王
insert into user(id,username) values(6,‘老李’);//隐式事务,自动提交
select * from user where id =5; //读取到的是张三
提交事务

一个事务A查询一批数据个数,一开始查询出来是10条数据;此时事务B往表里面插入了几条数据,而且事务B还提交了;接着事务A再次查询数据,发现数据变多了,这就是幻读。

要说明的是,当你在 MySQL 中测试幻读的时候,并不会出现上图的结果,幻读并没有发生,MySQL 的可重复读隔离级别其实解决了幻读问题,利用next-key(行锁+间隙锁)。

总结

  • 脏读:读到其他事务未提交的数据修改
  • 不可重复读:读到其他事务提交的数据修改
  • 幻读:读到其他事务提交的数据插入

1.3 事务隔离级别

为了解决并发事务带来的问题,就有了事务隔离级别的概念。隔离的越严实,效率就会越低。

隔离级别丢失更新脏读不可重复读幻读
读未提交×
读提交××
可重复读(默认)×××
串行化××××

读未提交:一个事务还没提交时,它做的变更就能被别的事务看到。

读提交:一个事务提交之后,它做的变更才会被其他事务看到。

可重复读:一个事务执行过程中看到的数据,总是跟这个事务启动时看到的数据是一致的。

串行化:对于同一行记录,写会加锁,读会加锁。当出现读写冲突的时候,后访问的事务必须前一个事务执行完成,才能继续执行。

假设数据表T中只有一列,其中一行的值为1,下面是按照时间顺序执行两个事务的行为。

若隔离级别是“读未提交”:V1的值是2。这时候事务B虽然还未提交,但结果已经被事务A看到了,因此V2,V3都是2。

若隔离级别是“读提交”:V1是1,V2是2。事务B的更新在提交后才能被A看到。因此V3的值是2。

若隔离级别是“可重复读”:V1,V2是2。事务在执行期间看到的数据前后必须一致(和第一次查询看到的值一样)。若事务A在事务B提交之后才开始查询,则之后看到的值都是2。

若隔离级别是“串行化”:则事务B执行“将1改成2”的时候会被锁住。知道事务A提交时候,事务B才可以继续执行。所以从事务A的角度看,V1,V2是1,V3是2。

二、MySQL 锁

2.1MyISAM表锁:

MyISAM 存储引擎只支持表锁, MyISAM 在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT 等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁。

2.2 InnoDB行锁:

InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用了行级锁。

InnoDB 实现了以下两种类型的行锁。

  • 共享锁(S):又称为读锁,简称S锁,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

  • 排他锁(X):又称为写锁,简称X锁,排他锁就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。

默认机制:

  • 对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);
  • 对于普通SELECT语句,InnoDB不会加任何锁;

可以通过以下语句显示给记录集加共享锁或排他锁 。

共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE

排他锁(X) :SELECT * FROM table_name WHERE ... FOR UPDATE

行级锁都是加在索引上的。所以如果查询条件不走索引则只能加表级锁

三、事务隔离实现原理

需要注意的是,begin transaction并不是事务的开始,事务中的第一条SQL语句才是事务的开始。

第一次查询的时候才生成快照,不是事务开始就生成快照

1. 读未提交(解决脏写)

  • 所有的读不加锁,读到的数据都是最新的数据,性能最好
  • 写加锁,但是读可以不阻塞读最新的数据记录,写写还是会阻塞
  • 事务提交释放锁

2. 读已提交(解决脏读)

  • 使用的是MVCC技术
  • 写操作:加行级锁。事务开始后,会添加一条undo记录。数据行的隐藏列有指向undo记录的指针。
  • 读操作:不加锁,在读取时,如果该行被其他事务锁定,则顺着隐藏列上undo指针,找到上一个有效的历史记录。(有效记录:该记录对当前事务可见,且DELETE_BIT=0)

3. 可重复读(解决不可重复读)

  • 也是使用MVCC技术,读写操作跟上面几乎一样
  • 区别是读已提交每次读都生成最新的read view,而可重复读只在事务第一次读的时候生成read view,之后再读到还是用之前的read view(此方法还能解决幻读)

4. 串行化(解决幻读)

  • 所有的读写都加锁

3.1 当前读和快照读

  • 当前读:读取的是记录的最新版本,需要保证其他并发事务不能修改当前记录,所以会对读取的记录加锁
  • 快照读:不加锁的select操作就是快照读,即不加锁的非阻塞读;不能保证读取的是最新的结果。MVCC为了实现读写冲突不加锁,用的就是这个快照读

3.2 MVCC实现原理

总体:隐式字段 + undo日志 + Read View(读视图)

隐式字段:每行数据记录除了我们自己定义的字段外,还有数据库隐式定义字段

功能 TX_IDROLL_PTRROW_ID
大小6字节7字节6字节
说明最新修改事务ID回滚指针隐含的自增ID
是否必须
注释按事务创建先后顺序递增指向这条记录的上一个版本

每次事务更新数据的时候,该数据行就会生成一个新的数据版本,不一定非要提交才增加修改版本

undo日志

  • insert undo log:代表事务在insert新记录时产生的undo log,在事务回滚时需要,当事务提交后可以丢弃
  • update undo log:事务在update或delete时产生的undo log,不仅在事务回滚时需要,在快照读时也需要,不能随便删除,只能当快照度和事务回滚都不涉及该日志才可以被删除
     

Read View(读视图)

Read View 就是一个保存事务ID的list列表,记录是的本事务执行时,MySQL还有哪些事务在执行。RC(读提交)是每次执行读SQL语句的时候都创建一个Read View,而RR(可重复读)是在事务启动的时候创建一个Read View。

Read View核心数据结构:

  • min_trx_id:read view生成时未提交事务id列表中最小事务id(m_ids的最小值)
  • max_trx_id:read view生成时系统尚未分配的下一个事务id
  • m_ids:read view生成时启动但未提交(活跃)的事务id列表。
  • create_trx_id:生成该readview的事务id。

以下是一条记录的版本链,从下往上代表更新的顺序:

读提交可重复读 都是通过ReadView来记录创建ReadView时的事务的状态,两者的创建时间不一样,读提交是每次查看都创建ReadView,可重复读是第一次查看创建ReadVIew。接着找到要查看的行记录,遍历找到ReadView创建之前的最新已经事务提交的修改记录。

可重复读实现原理:

按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。

因此,一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。

当然,如果“上一个版本”也不可见,那就得继续往前找。还有,如果是这个事务自己更新的数据,它自己还是要认的。

MVCC流程

1 被访问的版本的trx_id属性值与ReadView中的creator_trx_id,意味着这个版本是自己修改的记住,所以得认。

2 如果被访问版本trx_id小于Readview中的min_trx_id ,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问,这种也得认。

3 如果被访问版本的trx_id属性值大于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问,这种不认

说完了等于,小于和大于。还有一种就是介于当前ReadView的min_trx_id和max_trx_id之间,那就要判断一下trx_id值大小是不是在m_ids中,如果在,说明创建ReadView时候生成该版本的事务还是活跃的(这个版本的事务还没提交),该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。(这条我觉的是对RC来说的,因为RR 只生成一次ReadView不会出现这种情况)

MVCC总体来说:

一个事务能够读到那些版本数据,要遵循以下规则:

  1. 当前自己事务内的更新,可以读到
  2. 版本未提交,不能读到()
  3. 版本已提交,但是却在快照创建后提交的,不能读到(大于等于ReadView的max_trx_id)
  4. 版本已提交,且是在快照创建前提交的,可以读到(小于ReadView的max_trx_id且不在m_ids中或小于ReadView的min_trx_id:read)

可重复读是在事务开始的时候生成一个当前事务全局性的ReadVIew,而读提交则是每次执行语句的时候都重新生成一次ReadView,根据ReadView去行记录找符合要求的数据版本。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值