mysql:事务

1.什么是事务

数据库中的的事务是指对数据库执行一批操作,在同一个事务中,这些操作要么全部执行成功,要么全部失败,不存在部分成功的情况。

  • 事务是一个源自操作,是一个最小执行单元,可以由一个或者多个sql语句组成。
  • 在同一个事务中,所有sql语句都成功执行时,整个事务成功,有一个SQL语句执行失败,整个事务就会执行失败。

2.事务的四大特性(ACID)

  1. 原子性(Atomicity)

    事务的整个操作如原子操作一样,要么全部成功,或者全部失败,这个原子性是从最终结果来看的,从最终结果来看这个过程是不可分割的。

  2. 一致性(Consistency)

    一个事务完成时,必须是所有的数据都保持一直状态

  3. 隔离性(Isolation)

    数据库系统提供的隔离机制,保证事务不受外部并发操作影响的独立环境下运行

  4. 持久性(Durability)

    事务一旦提交成功或回滚,它对数据库中的数据的改变是永久的

3.隔离级别

并发事务问题:脏读,不可重复读,幻读

  • 脏读:一个事务读到另外一个事务还没有提交的数据

    如下图:事务A修改了数据库中的一条记录,还未提交,事务B已经读到了修改之后的数据。

    image-20240312122348841

  • 不可重复读:一个事务先后读取到同一条记录,但俩次读取的数据不同,称之为不可重复读。

    如图所示:事务A先查询了数据库中id为1的数据,事务还未结束,此时事务B修改了id为1的数据并提交了事务,之后事务A又查询id为1的数据,就出现了在同一个事务内,查询同一条数据结果不一致。

    image-20240312122855348

  • 幻读:一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据以及存在,好像出现了幻影。

    如下图:事务A先从数据库中查询id为1的数据,发现未查询到,当事务A还未执行插入操作时候,事务B执行插入id为1的数据并提交事务B,此时事务开始执行插入操作,发现插入失败,于是又去查询id为1 的数据,由于事务A还未结束,所以查询id为1的数据还是查询不到。(幻读是在解决了不可重复读的基础上所产生的,所以在同一个事务内,查询的数据都是一致的)

    image-20240312123429252

要解决并发事务问题,所以需要对事务进行隔离,一共有四种隔离级别:

  • 读未提交
  • 读已提交
  • 可重复读
  • 串行化

image-20240312124038617

注意:事务隔离级别越高,数据越安全,但性能越低,mysql中默认的隔离级别是可重复读,可以解决脏读和不可重复读,不能解决幻读问题。

4. redo log和undo log

4.1 redo log(重做日志)

redo log 是属于引擎层(innodb)的日志,它的设计目标是支持innodb的“事务”的特性,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。

redo log 能保证对于已经COMMIT的事务产生的数据变更,即使是系统宕机崩溃也可以通过它来进行数据重做,达到数据的一致性,这也就是事务持久性的特征,一旦事务成功提交后,只要修改的数据都会进行持久化,不会因为异常、宕机而造成数据错误或丢失,所以解决异常、宕机而可能造成数据错误或丢是redo log的核心职责。

image-20240312130721652

redo log记录的是操作数据变更的日志,听起来好像和binlog有类似的地方,但是最核心的一点就是redo log记录的数据变更粒度和binlog的数据变更粒度是不一样的,也正因为这个binlog是没有进行崩溃恢复事务数据的能力的。

4.2 Undo log(回滚日志)

用于记录数据被修改之前的信息,作用包含俩个:提供回滚和MVCC(多版本并发控制)。undo log和redo log记录物理日志不一样,它是逻辑日志。

  • 可以认为当delete一条日志时候吗,undo log中会记录一条对应的insert记录,反之亦然。
  • 当update一条记录时,它记录一条对应相反的update记录,当执行rollback时,就可以从undo log 中的逻辑记录中读取到相应的内容并进行回滚。
  • undo log可以实现事务的一致性和原子性

4.3 区别

Undo Log(回滚日志) 和 Redo Log(重做日志)之间的区别,没那么高深,我们只要按字面意思理解就行了。

Redo Log(重做日志)是为了系统崩溃之后恢复数据用的,让数据库照着日志,把没做好的事情重做一遍。 有了Redo Log,就可以保证即使数据库发崩溃重启后,之前提交的记录都不会丢失,这个能力称为 crash-safe。

Undo Log(回滚日志)是为了回滚用的。 在事务提交之前就开始写数据,万一事务到最后又打算不提交了,要回滚,或者系统崩溃了,这些提前写入的数据就变成了脏数据,这时候就必须用Undo Log恢复了。

5.保证事务的隔离性

redo log保证事务的持久性,undo log保证事务的一致性和原子性,那么数据库如何保证事务的隔离性?

数据库是通过加锁,来实现事务的隔离性的。insert,update,delete都会自动添加排它锁,防止其他事物修改这行数据。

加锁确实好使,可以保证隔离性。比如串行化隔离级别就是加锁实现的。但是频繁的加锁,导致读数据时,没办法修改,修改数据时,没办法读取,大大降低了数据库性能

那么,如何解决加锁后的性能问题的?

答案就是,MVCC多版本并发控制!它实现读取数据不用加锁,可以让读取数据同时修改。修改数据时同时可读取。

6. MVCC

MVCC,即Multi-Version Concurrency Control (多版本并发控制)。它是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。

通俗的讲,数据库中同时存在多个版本的数据,并不是整个数据库的多个版本,而是某一条记录的多个版本同时存在,在某个事务对其进行操作的时候,需要查看这一条记录的隐藏列事务版本id,比对事务id并根据事物隔离级别去判断读取哪个版本的数据。

数据库隔离级别读已提交、可重复读 都是基于MVCC实现的,相对于加锁简单粗暴的方式,它用更好的方式去处理读写冲突,能有效提高数据库并发性能。

7.MVCC实现原理

7.1 事务版本号

事务每次开启前,都会从数据库获得一个自增长的事务ID,可以从事务ID判断事务的执行先后顺序。这就是事务版本号。

7.2 隐式字段

对于InnoDB存储引擎,每一行记录都有两个隐藏列trx_idroll_pointer,如果表中没有主键和非NULL唯一键时,则还会有第三个隐藏的主键列row_id

列名 是否必须 描述
row_id 单调递增的行ID,不是必需的,占用6个字节。
trx_id 记录操作该数据事务的事务ID
roll_pointer 这个隐藏列就相当于一个指针,指向回滚段的undo日志

7.3 undo log

undo log,回滚日志,用于记录数据被修改前的信息。在表记录修改之前,会先把数据拷贝到undo log里,如果事务回滚,即可以通过undo log来还原数据。

可以这样认为,当delete一条记录时,undo log 中会记录一条对应的insert记录,当update一条记录时,它记录一条对应相反的update记录。

undo log有什么用途呢?

  1. 事务回滚时,保证原子性和一致性。
  2. 用于MVCC快照读

7.4 undo log 版本链

多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(roll_pointer),连成一个链表,这个链表就称为版本链。如下:

image-20240312135724993

其实,通过版本链,我们就可以看出事务版本号、表格隐藏的列和undo log它们之间的关系。不同事务或相同事务对统一记录进行修改,会导致该记录的undo log生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。

7.5 快照读和当前读

快照读: 读取的是记录数据的可见版本(有旧的版本)。不加锁,普通的select语句都是快照读,如:

1
select * from core_user where id > 2;

当前读:读取的是记录数据的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁,显式加锁的都是当前读。如:select…lock in share mode(共享锁),select…for update、update、insert、delete(排它锁)都是一种当前读。

1
2
select * from core_user where id > 2 for update;
select * from account where id>2 lock in share mode;

7.6 Read View

  • Read View是什么呢? 它就是事务执行SQL语句时,产生的读视图。实际上在innodb中,每个SQL语句执行前都会得到一个Read View。
  • Read View有什么用呢? 它主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据

Read View是如何保证可见性判断的呢?我们先看看Read view 的几个重要属性

  • m_ids:当前系统中那些活跃(未提交)的读写事务ID, 它数据结构为一个List。
  • min_limit_id:表示在生成ReadView时,当前系统中活跃的读写事务中最小的事务id,即m_ids中的最小值。
  • max_limit_id:表示生成ReadView时,系统中应该分配给下一个事务的id值。
  • creator_trx_id: 创建当前read view的事务ID

Read view 匹配条件规则如下:

  1. 如果数据事务ID trx_id < min_limit_id,表明生成该版本的事务在生成Read View前,已经提交(因为事务ID是递增的),所以该版本可以被当前事务访问。
  2. 如果trx_id>= max_limit_id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。
  3. 如果 min_limit_id =<trx_id< max_limit_id,需腰分3种情况讨论
  • (1).如果m_ids包含trx_id,则代表Read View生成时刻,这个事务还未提交,但是如果数据的trx_id等于creator_trx_id的话,表明数据是自己生成的,因此是可见的。
  • (2)如果m_ids包含trx_id,并且trx_id不等于creator_trx_id,则Read View生成时,事务未提交,并且不是自己生产的,所以当前事务也是看不见的;
  • (3).如果m_ids不包含trx_id,则说明你这个事务在Read View生成之前就已经提交了,修改的结果,当前事务是能看见的。