MySQL事务
文章目录
学习网站:https://xiaolincoding.com/
MySQL事务特性
ACID
- 原子性(Atomicity):一个事务的所有操作,要么全部完成,要么全部不完成,不会结束在某个中间环节,事务进行中如果发生错误,就会回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
比如,去商店买商品,购买成功,就给商家付了前,商品属于我;购买失败,商品在商家手中,我的钱也没花出去。
原子性由undo log保证的。
- 一致性(Consistency):事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。
比如,用户A和用户B在银行分别有400元和600元,总共1000元,用户B给用户A转账200元,分为两个步骤,从用户B的账号扣除200元,用户A的账号增加200元。一致性就是要求上述操作执行后,最后的结果是用户B还有400元,用户A还有600元,总共1400元。而不会出现用户B扣除了200,用户A没增加的情况(该情况,用户A和用户B均为600元,总共1200元)。
一致性由持久性+原子性+隔离性保证
- 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写能力和修改能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据,对其他并发事务是隔离的。
比如,同一个商品,不同消费者购买这个商品是互不影响的。
隔离性由MVCC和锁保证
- 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
持久性由redo log保证的。
事务的原子性如何保证
事务的原子性是通过undo log实现的:
在事务还没提交前,历史数据会记录在undo log中,如果事务执行过程中,出现了错误或者用户执行了ROLLBACK,MySQL可以利用undo log中的历史数据,把数据恢复到事务开始之前的状态,从而保证了事务的原子性。
事务的隔离性如何保证
事务的隔离性是通过MVCC和锁实现的:
可重复读隔离级别下的快照读(普通select),是通过MVCC来保证事务隔离性的,当前读(update、select…for update)是通过行级锁来保证事务的隔离性的。
事务的持久性如何保证
事务的持久性是通过redo log实现的:
因为MySQL通过WAL机制(先写日志再写数据),在修改数据的时候,会将本次对数据页的修改以redo log的形式记录下来,这时候更新就算完成了,Buffer Pool的脏页会通过后台线程刷盘,即使脏页还没有刷盘时就发生了数据库重启,由于修改操作都记录到了redo log,之前已提交的记录都不会丢失,重启后就通过redo log,恢复脏页数据,从而保证事务的持久性。
MySQL事务和Redis事务的区别
Redis事务没保证原子性和持久性。
原子性:Redis事务没有回滚功能,没法实现和MySQL事务一样的原子性,无法保证事务运行期间:要不全部失败,要不全部成功。如果Redis事务执行过程中,中间有命令是错误的,不会停止执行和回滚,这时候事务的执行会出现半成功状态。
持久性:如果Redis使用了RDB模式,在一个事务执行后,下一次的RDB快照还未执行,如果发生了宕机,事务修改的数据无法保证持久化;如果Redis使用了AOF,AOF的三个模式:no、eversec、always都会存在数据丢失问题,所以事务的持久性还是无法保证
事务的隔离级别
4种隔离级别
- 读未提交
read uncommitted,指一个事务还没提交时,它对数据做出的修改可以被其他事务看到
- 读提交
read commited,指一个事务提交之后,它对数据做出的修改才能被其他事务看到
- 可重复读
repeatable read,指一个事务执行过程中看到的数据,一直和这个事务启动时看到的数据是一致的。它还是MySQL InnoDB引擎的默认隔离级别。
- 串行化
serializable,指一个事务执行过程会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等待前一个事务执行完成,才能继续执行。
四个隔离水平从高到低是:
串行化 》可重复读 》读已提交 》读未提交
不同隔离级别产生的问题
脏读:指一个事务读取另一个事务还未提交的数据,如果另一个事务回滚,则读取的数据是无效的。脏读可能导致数据的不一致性。
不可重复读:指一个事务多次读取同一条记录,但是在此期间另一个事务修改了记录,导致前后读取的数据不一致。不可重复读可能导致数据的不一致性。
幻读:指一个事务多次执行同一个查询,但是此期间另一个事务插入了符合该查询条件的新数据,导致前后查询的结果不一致。幻读可能导致数据的不完整性。
ps:可重复读很大程度避免了幻读,但没有完全避免。
脏读和幻读
脏读是指一个事务读到了另一个未提交事务修改过的数据,如果另一个事务回滚了,刚才读到的数据就与数据库里的不一致了。
在一个事务内多次查询某个符合条件的记录数量,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了幻读。
比如,如果select执行了两次,但是第二次返回了第一次没有返回的行数据,则该行是幻读行。
串行化隔离是通过什么实现的
串行化隔离级别所有的SQL都会加行级锁,包括普通的select查询,都会加S型的next-key锁。其他事务就没办法对这些已经加锁的记录进行增删改操作了,从而避免了脏读、不可重复读和幻读,但是性能是隔离级别中最差的,没有MVCC机制,读写操作没办法并行。
什么是MVCC
MVCC是多版本并发控制,通过记录历史版本数据,解决读写并发冲突问题,避免了读数据时加锁,提高了事务的并发性能。
MySQL把历史数据存到undo log中,结构逻辑上类似一个链表,MySQL数据行有两个隐藏列,一个是事务ID、一个是指向undo log的指针。
事务开启后,执行第一条select语句时,会创建ReadView,ReadView记录了当前未提交的事务,通过与历史数据的事务ID做比较,根据可见性规则进行判断,判断这条记录是否看见,如果能看见就直接返回给客户端,如果不可见就,往undo log版本链查找第一个可见数据。
可见性规则
ReadView四个字段:
- m_ids:指创建ReadView时,当前数据库中活跃事务的事务id列表,一个列表
活跃事务指,启动了还没提交的事务
-
min_trx_id:指的是在创建ReadView时,当前数据库中活跃事务中事务id最小的事务
-
max_trx_id:指的是在创建ReadView时,当前数据库中应该给下一个事务的id值,也就是全局事务中最大的事务id值+1
-
create_trx_id:指的是创建该ReadView的事务的事务id
聚簇索引记录中的两个隐藏字段:
- trx_id:当一个事务对某条聚簇索引进行改动时,就会把该事务的事务id记录在trx_id隐藏列中;
- roll_pointer:每次对聚簇索引进行改动时,就会把旧版本的记录写入到undo log中,然后这个隐藏列是个指针,指向每个旧版本,通过它就可以找到修改前的记录
在创建ReadView后,我们可以把记录中的trx_id划分三种情况:
一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:
1.如果记录的trx_id值小于ReadView中的min_trx_id值,表示这个版本的记录是在创建ReadView前已经提交的事务生成的,所以该版本的记录对当前事务可见。
2.如果记录的trx_id值大于ReadView中的max_trx_id值,表示这个版本的记录是在创建ReadView后才启动的事务生成的,所以该版本的记录对当前事务不可见。
3.如果记录的trx_id值在ReadView中的min_trx_id值和max_trx_id值之间,需要判断trx_id是否在m_ids之间:
-
如果在,表示该版本记录的活跃事务还没有提交事务,所以该版本的记录对当前事务不可见
-
如果不在,表示该版本记录的活跃事务已经提交事务,所以该版本的记录对当前事务可见
读已提交和可重复读隔离级别实现MVCC的区别
其实它们的区别就在于创建ReadView的时机不同。
-
读已提交在开启事务后,每次执行select都会生成新的ReadView,所以每次都能看到其他事务最近提交的数据
-
可重复读在开启事务后,执行第一条select时生成一个ReadView,直到事务结束都在使用这个ReadView,所以事务执行过程中看到的数据,和事务启动时看到的数据是一致的
可重复读怎么解决不可重复读的
MySQL提供两种查询方式:快照读,普通select语句,靠MVCC解决不可重复读;当前读,select…for update语句,靠行级锁中的记录锁解决不可重复读
-
快照读,通过MVCC解决不可重复读,在可重复读隔离级别下,第一次select查询时,会生成一个ReadView,随后的事务都使用这个ReadView,这样前后两次查询的记录都是一样的
-
当前读,靠行级锁中的记录锁实现,在可重复读隔离级别下,第一次select…for update语句查询时,会对记录加next-key锁,这个锁包含记录锁,此时如果其他事务更新了加锁的记录,就会被阻塞住
可重复读隔离级别完全解决了不可重复读问题吗
并不是,如果前后两次的查询都是快照读,普通select,那就不会产生不可重复读问题;
但是,如果第一次是快照读,第二次是当前读,那么就可能发生不可重复读的问题。
比如:表中有id=1,age=10的记录
- 事务a,先执行select,查询到id=1的age是10
- 事务b,更新id=1的age为20,提交事务
- 事务a,执行select…for update,当前读,然后就读到了id=1的age是20,产生了不可重复读
可重复读有没有解决幻读问题
答案是没有,在特殊情况下会出现幻读问题。
比如:
场景1
首先,数据库不存在id=5的记录,事务a执行第一次查询时,读不到该记录,接着事务b插入了id=5的新记录;
然后,事务a更新了事务b刚刚插入的id=5的记录,更新操作是当前读,所以事务a可以读到id=5的记录并更新,更新时,会把id=5的记录的隐藏列的事务id变为事务a的id,代表是事务a修改的;
最后,事务a再去查询,发现id=5的事务id和本事务id相同,会认为是可见的,所以就读到了id=5的数据,发生了幻读。
场景2
首先,事务a执行快照读,select * from table_name where age > 20,假设得到3条记录;
然后,事务b插入一个age大于20的记录并且提交事务;
最后,事务a再执行上次的语句,发现得到了4条记录,此时发生了幻读。
如何避免
在开启事务后,马上执行select … for update语句,因为它会对记录加next-key锁(临键锁),这样就可以避免其他事务插入一条新记录,就避免了幻读问题。