事务
事务概念:
事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
事务的特性:ACID:
小结:
ACID,分别指的是:原子性、一致性、隔离 性、持久性;
其中:原子性和一致性由undolog实现;隔离性由mvcc实现;持久性由redeolog实现
举个例子:
A向B转账500,转账成功,A扣除500元,B增加500元,原子操作体现在要么都成功,要么都失败
在转账的过程中,数据要一致,A扣除了500,B必须增加500
在转账的过程中,隔离性体现在A像B转账,不能受其他事务干扰
在转账的过程中,持久性体现在事务提交后,要把数据持久化(可以说是落盘操作)
并发事务问题
并发事务概念:
并发事务是指在数据库系统中,多个事务同时对数据进行读写和修改的过程。
并发事务问题:
1.脏读 (违反了事务的隔离性)
例:事务A先查询id为1的数据,再修改id为1的数据。修改完以后事务A还没提交,这时候事务B也来查询id为1的数据,但这时候事务B已经可以读到A修改完的数据了。这就是脏读。(如果A事务回滚,但B已经读到了A修改的数据,造成了数据不一致)
2.不可重复读 (违反了事务的一致性)
例:事务A先查询id为1的数据,再执行某个逻辑。这时事务B修改id为1的数据。事务A再去查询id为1的数据,却和之前不一样了!简单来说就是:在同一个事务内,查询两次同一条数据,却出现不同结果!
3.幻读 (违反了事务的隔离性)(在已经解决了不可重复读的基础上)
例:事务A查询id为1的数据,发现数据库中没有。这个时候事务B来了,正好往数据库中插入了一条id为1的数据并且提交了事务。这个时候,事务A进行插入操作,就直接报错了:里面已经有了id为1的数据(假设我们已经解决了不可重复读的问题)。但A再次进行查询,查询结果和第一次一样,发现还是没有id为1的数据,但插入就是报错,这就是幻读的问题。
注意:幻读是在解决了不可重复读的基础上,不可重复读是读取了其他事务更改的数据,针对update操作;幻读是读取了其他事务新增的数据,针对insert和delete操作。
解决方案:
注:这里✔表示存在该问题,× 表示解决该问题。
小结:
1.事务并发问题:
第一是脏读, 当一个事务正在访问数据并且对数据进行了修改,而这种修改 还没有提交到数据库中,这时另外一个事务也访问了这个数据,因为这个数 据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依 据“脏数据”所做的操作可能是不正确的。
第二是不可重复读:比如在一个事务内多次读同一数据。在这个事务还没有 结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之 间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。 这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重 复读。
第三是幻读(Phantom read):幻读与不可重复读类似。它发生在一个事务 (T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在 随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就 好像发生了幻觉一样,所以称为幻读。
2.事务隔离:
第一个是,未提交读(read uncommitted)它解决不了刚才提出的所有问 题,一般项目中也不用这个。
第二个是读已提交(read committed)它能解 决脏读的问题的,但是解决不了不可重复读和幻读。
第三个是可重复读 (repeatable read)它能解决脏读和不可重复读,但是解决不了幻读,这个 也是mysql默认的隔离级别。
第四个是串行化(serializable)它可以解决刚 才提出来的所有问题,但是由于让是事务串行执行的,性能比较低。所以, 我们一般使用的都是mysql默认的隔离级别:可重复读。
undo log(实现事务原子性和一致性)和redo log(实现事务原子性)
先引入两个概念:
当我们做了一些操作 (update/delete/insert),提交事务后要操作MySql中的数据。
为了能够提升性能,引入了两块区域:内存结构和磁盘结构。
磁盘结构:
主要存储的就是数据页,每个数据页存储的就是表中一行一行的数据。(一个表的数据,可能由多个数据页存储)。
但当我们做一些增删改的操作时,不会直接操作磁盘。而是先去操作内存。
内存结构:
当操作来了以后,先来操作缓冲池。从内存中的缓冲池里面先找,有没有我要操作的数据。如果没有,就会把磁盘中的数据(某一个数据页的数据)加载到缓冲池中。这样直接操作内存,性能会更高!操作完成之后,缓冲池中数据会同步给磁盘。这样就减少磁盘的IO,加快了处理速度!
这就引出了问题:
现在内存中的数据页被操作完了,但还没同步到磁盘中 (此时这个数据页称为脏页),这个时候服务器宕机了,同步失败了。内存中的数据可能就消失了,数据丢失了。这就违背了事务持久化的特性。
解决方案:redo log (解决事务持久性)
加入Redolog buffer和Redolog file后,数据操作也会发生一点变化。
当有增删改操作之后,现在buffer pool已经发生了变化。Redolog buffer就会记录对应数据页发生的变化,一旦Redolog buffer发生变化 (说明有新的数据页变化)。就会同步给磁盘中的Redolog file中。现在一旦buffer pool对脏页数据同步失败了,就可以从Redolog file恢复数据!
注意:你可能会有疑问,这样做岂不是更麻烦了?不用这个Redolog行不行呢?比如说当buffer pool中的数据页发生变化直接进行同步不好吗?
答:这样做不好,会有严重的性能问题。当我们操作增删改的时候,会有大量的update等语句,如果我们同步刷新,对磁盘IO次数太多了,每执行一条sql可能就要进行一次连接数据库。而如果使用redolog,它进行数据同步时,都是顺序的磁盘IO (可以理解为进行了归纳,连接一次数据库会进行多条sql语句)。这样性能就可以大大提升了!
注意:
等到脏页刷新完成后,可以认为redo log不需要了,进行定期清理,等下一个事务需要的时候,又往里面填写内容,文件不会删除,而是重复利用。两个redo log文件循环写的。
undo log:回滚日志 (解决事务一致性和原子性)
二者区别:
redo log
是用来记录事务对数据库进行的修改操作。如果数据库发生崩溃,redo log
可以用于恢复未提交的事务对数据库所做的更改,从而确保事务的持久性。
undo log
是用来记录事务对数据库进行的修改操作的逆操作。如果一个事务需要回滚,或者数据库需要恢复到某个一致的状态,undo log
可以用于撤销已经进行的修改,从而确保事务的一致性。
MVCC(实现事务隔离性)
如何保证事务的隔离性?
1.排他锁:如一个事务获取了一个数据行的排他锁,其他事务就不能再获取改行的其他锁。
2.MVCC:多版本并发控制。
MVCC:
1.隐藏字段
1.DB_TRX_ID:最近修改事务的id。默认值从0开始,每次被修改自增1。也就是说每有一个事务修改了当前数据,这条数据的该字段就会自增1。(从而实现记录最近''修改事务''的id)
2.DB_ROLL_PTR:回滚指针。举个例子:对于这里id为1的数据,进行了三次修改。DB_TRX_ID就是3。但是,.DB_ROLL_PTR记录的是该行数据第一次修改时的版本。也就是第一次修改的事务id -> 1。这样后面配合undo log就知道回滚到上一个版本(就是三次修改前的版本)。
3.DB_ROW_ID:隐藏主键。当前表指定了主键,该字段就没啥意义了。
2.undo log
undo log版本链
这里每有一个事务修改当前行数据,就像链表中的尾插法一样,插入一个节点。
现在有个问题就是,一个select语句过来了,我到底查询这个版本链中选择哪一条数据?而readView就是解决这个问题的!
3.readView
RC (读提交):
在RC隔离级别下,事务在执行每次读取操作时都会生成一个新的读视图(Read View)。这意味着每个读取操作都会基于当前活跃的事务ID(即未提交的事务)来构建一个一致性视图(每个SELECT语句执行时都会创建一个新的Read View,以确保读取到的是最新已提交的数据)。因此,在RC隔离级别下,事务可能看到其他事务提交的更新,但这些更新必须是当前事务开始读取操作时已经存在的。换句话说,RC隔离级别允许不可重复读,即在一个事务中多次读取相同的记录可能会得到不同的结果,因为它可能看到了其他事务提交的更新。
RR (可重复读):
相比之下,在RR隔离级别下,事务在启动时会创建一个读视图,并在整个事务的生命周期内保持不变。这意味着一旦事务开始,它将看到所有已经提交的记录的稳定视图,直到事务结束(一个事务对应一个readView)。在RR隔离级别下,即使其他事务在当前事务执行期间提交了更新,当前事务也不会看到这些变化,因为它已经有了一个稳定的数据快照。因此,RR隔离级别提供了可重复读的保证,即在一个事务中多次读取相同的记录将会得到相同的结果。
举个例子:现在是事务5执行第一条查询sql。
m_ids (当前活跃的事务):3、4、5(事务2已提交)
min_trx_id:3 (3、4、5中最小的id)
max_trx_id:6 (5+1)
creator_trx_id:5 (当前事务5做了查询,ReadView由事务5创建)
read view中四变量。活跃事务(活跃事务就是没提交的事务)集合,当前事务id,活跃事务集合中的最小值,活跃事务集合中最大值的下个值。都是select查出的最新数据事务id跟四个变量进行比较判断的。判断方法可以逆推,就是凡是小于最小值,不包含活跃事务里面的,刚好等于当前事务的id,都可以查询最新值。其他任何情况都要一直递归查询undo日志,直到符合这三个条件为止。
版本链数据访问规则:
RC级别下的访问:
在这个例子中:根据版本链数据访问规则事务5第一个sql对应的查询结构是事务2修改后的数据。原因:根据版本链从最新事务4开始比较。
此时:creator_trx_id = 5;min_trx_id = 3;max_trx_id = 6
4所有条件都不满足,事务3也都不满足。只有事务2满足:trx_id (2) < min_trx_id(3)。
同理,对于事务5第二条查询,也能得到结果。第二条sql查询的是事务3修改后的数据。
RR级别下的访问:
由于RR多次查询,都只生成最初的一个视图。因此我们只需要搞明白第一个视图的查询结果。同上方法可知,这里也是查询到事务2修改后的数据。
在MySQL的“可重复读”(Repeatable Read)隔离级别下,生成的Read View在事务开始时创建后不会被修改。Read View是用来保证事务内部的一致性读,即在事务开始时建立的数据快照在事务的整个生命周期内是固定的。这意味着,即使事务自身对数据进行了修改,这些修改对当前事务而言是可见的,但对于其他并发事务则不可见,直到当前事务提交。这样的设计确保了在同一事务中多次读取同一数据时能够得到相同的结果,从而避免了不可重复读的问题
注意区分:同一个事务中,先select,再update,然后再select。查询结果是不一样的。因为这是同一个事务内。事务能够看到自己所做的修改,即使这些修改还没有提交而如果在两次查询之间由外部事务修改数据,这时由于readView,两次查询的结果是相同的。
总结: