从概念上来讲,事务是一个有限的数据库操作序列构成,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位 。
事务的四大特性
事务的四大特性ACID(原子性、一致性、隔离性、持久性)
- 原子性(Atomicity):事务是一个不可再分的个体,要么全部成功,要么全部失败。
原子性的实现通过undo log回滚日志 来完成,当事务执行更新操作之前,数据库将当前数据行的旧值存储到 undo log回滚日志当中,如果事务执行过程中出现异常情况,就将事务进行回滚,InnoDB引擎就是利用 undo log 保存下来的记录,将数据恢复到事务开始之前。
- 一致性(Consistency):事务执行后必然将数据库从一个一致性状态转换到另一个一致性状态
MySQL使用各种约束(如主键约束、唯一性约束、外键约束等)和触发器(Trigger)来保证数据的一致性。在事务提交之前,MySQL会对所有的数据修改进行检查,确保满足约束条件,如果不满足约束条件,事务将回滚。
- 隔离性(Isolation):事务之间是互不影响的。
如果多个事务可以同时操作⼀个 数据,那么就会产⽣脏读、重复读、幻读的问题,MySQL中定义了四种隔离级别提供使用,隔离级别的底层实现是锁或者MVCC,但是屏蔽了加锁的细节。
- 持久性(Durability):事务一旦提交,对数据库的影响是持久的。
当事务执行更新操作时 , 首先将数据的修改操作记录保存到Redo log 中, 而不是直接修改写入到磁盘的数据文件, 接着以顺序追加的方式写入磁盘,使得写入的操作非常高效, 当系统发生故障导致数据库重启时, MySQL通过Redo log 重做日志来恢复为写入磁盘的数据库修改,重新执行这些操作,将数据持久化到磁盘,这样就保证了事务的持久性。
并发事务可能产生的问题
- 脏读:指读到了别的事务回滚前的脏数据。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读
- 不可重复读:指两次读取到的数据不一致的情况。比如事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了,就是所谓的不可重复读了。强调的是更新。 不可重复读的解决方案为加行锁或者表锁。
- 幻读:和可重复读类似,但是事务二的数据操作是插入和删除,不是修改数据,读取的记录数量前后不一致。幻读的解决方案一般为加间隙锁。
其中严重性 : 脏读 > 不可重复读 > 幻读
不同的隔离级别导致的数据不一致情况:
读未提交 | 读已提交 | 可重复读 | 串行化 | |
---|---|---|---|---|
脏读 | 是 | 否 | 否 | 否 |
不可重复读 | 是 | 是 | 否 | 否 |
幻读 | 是 | 是 | 是 | 否 |
事务的隔离级别
- Read Uncommitted(读未提交):允许事务A读取B事务未提交的数据,隔离性最差;
- Read Committted(读已提交):事务A可以读取到事务B已提交的数据,避免造成脏读;
- Repeatable Read(可重复读):在同一个事务内的查询结果都是一致的。避免了脏读和不可重复读。可重复读是MySQL的默认隔离级别。
- Serializable(串行化):事务以串行化的方式执行,避免了脏读、不可重复读、幻读问题。安全性最高。
MySQL 的隔离级别基于锁和 MVCC 机制共同实现的
锁、行锁、表锁
在数据库中,锁用于控制对共享资源的并发访问。它可以防止多个并发事务同时修改同一数据,确保数据的一致性和完整性。MySQL数据库中的锁机制是用于控制并发访问的重要组成部分。常见的锁类型如下:
- 乐观锁:认为不会有冲突,只有在数据提交的时候才去检查。如果被更改则提交失败。
- 实现方式:版本控制(给表加一个版本号或时间戳字段),具体可通过给表加一个版本号或时间戳字段实现,当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断当前版本信息与第一次取出来的版本值大小,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据,拒绝更新,让用户重新操作。
- 悲观锁:认为会发生冲突,在并发访问前先对数据进行加锁。共享锁和排他锁是悲观锁的两种不同的实现:
- 共享锁:允许事务读一行数据(读锁)。共享锁指的就是对于多个不同的事务,对同一个资源共享同一个锁。在执行语句后面加上
lock in share mode
就代表对某些资源加上共享锁了。 - 排它锁:允许事务删除或更新一行数据(写锁)。与共享锁相对应,对于多个不同的事务,对同一个资源只能有一把锁。在需要执行的语句后面加上
for update
就可以了。
- 共享锁:允许事务读一行数据(读锁)。共享锁指的就是对于多个不同的事务,对同一个资源共享同一个锁。在执行语句后面加上
- 行锁:行锁是MySQL中最常用的锁类型。当事务修改某一行数据时,会为该行数据加上行级锁。行锁的特点是只锁定所需修改的行,其他事务可以并发访问其他行。MySQL中MyISAM只支持表锁,而InnoDB不仅支持表锁还支持行锁。由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。InnoDB有三种行锁的算法:
- Record Lock:单个行记录上的锁
- Gap Lock:间隙锁,锁定一个范围,但不包括记录本身
- Next-Key Lock:GapLock + Record Lock,锁定一个范围,包括锁定记录本身。
- 表锁:表锁是锁定整个表的锁类型。当事务需要修改整个表或查询涉及整个表时,会对该表加上表级锁。与行锁不同,表锁会阻塞其他事务对整个表的修改或查询。
InnoDB中锁的特性
- 在不通过索引条件查询时,innodb会锁定表中的所有记录
- mysql的行锁是针对索引加的锁,所以虽然是访问不同行的记录,如果使用相同的索引键,是会出现锁冲突的
- 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,不论是使用主键索引,唯一索引或普通索引,innodb都会使用行锁来对数据加锁
- 即使在条件中使用了索引字段,但是否使用索引来检索数据是由mysql通过判断不同计划的代价来决定的,如果mysql认为全表扫描效率更高,它就不会使用索引,这种情况下innodb会对所有记录加锁。
MVCC(多版本并发控制)
undo log主要用于事务回滚时恢复原来的数据
- mysql在执行sql语句时,会将一条逻辑相反的日志保存到undo log中。因此,undo log中记录的也是逻辑日志。
- 当sql语句为insert时,会在undo log中记录本次插入的主键id。等事务回滚时,delete此id即可。
- 当sql语句为update时,会在undo log中记录修改前的数据。等事务回滚时,再执行一次update,得到原来的数据。
- 当sql语句为delete时,会在undo log中记录删除前的数据。等事务回滚时,insert原来的数据即可。
在内部,InnoDB向存储在数据库中的每一行添加三个字段:在数据库中的每一行上,除了存放真实的数据以外,还存在着3个隐藏列——row_id、trx_id与roll_pointer。
- 6 字节的
DB_ROW_ID
,row_id,行号,如果当前表有整数类型的主键,则row_id就是主键的值。如果没有整数类型的主键,则mysql会按照字段顺序选择一个非空的整数类型的唯一索引作为row_id。如果mysql没有找到,则会自动生成一个自动增长的整数作为row_id。- 6 字节的
DB_TRX_ID
当一个事务开始执前,mysql会为这个事务分配一个全局自增的事务id。之后该事务对当前行进行的增、删、改操作时,都会将自己的事务id记录到trx_id中。- 7 字节的
DB_ROLL_PTR
,回滚指针。事务对当前行进行改动时,会将旧数据写入进undo log中,再将新数据写入当前行,且当前行的roll_pointer指向刚才那个undo log,因此可以通过roll_pointer找到该行的前一个版本。当一直有事务对该行改动时,就会一直生成undo log,最终将会形成undo log版本链。
MVCC(多版本并发控制):MVCC是一种用于实现并发控制的机制,MySQL的InnoDB存储引擎采用了MVCC。在MVCC中,读写不冲突,记录每一行的多个版本,来避免在多个事务之间的竞争。以空间换时间的思路,极大地提高了读写性能。
MVCC是通过隐藏的事务id列生成的ReadView读视图 + undo log 版本链实现的。每一个事务对该行改动时,都会生成一个undo log,用于保存之前的版本,之后再将新版本的roll_pointer指向刚才生成的undo log。因此roll_pointer可以将这些不同版本的undo log串联起来,形成undo log版本链。也就是创建和维护数据的多个版本(或快照),允许事务之间的并发执行而不会产生冲突。
快照读:简单的select查询,即不包括 select … lock in share mode, select … for update,可能会读到数据的历史版本。
当前读:以下语句都是当前读,总是读取最新版本,会对读取的最新版本加锁。
- select … lock in share mode
- select … for update
- insert
- update
- delete
ReadView:ReadView的作用是,判断undo log版本链中的哪些数据对当前事务可见。在事务执行每一个快照读时,会生成ReadView,ReadView包含几个重要的参数:
- m_ids:mysql中所有未提交的事务id集合。
- min_trx_id:m_ids中的最小值
- max_trx_id:mysql即将为下一个事务分配的事务id,并不是m_ids中的最大值。
那么事务在执行快照读时,可以通过以下的规则来确定undo log版本链上的哪个版本数据可见。
- 如果当前undo log的版本的trx_id<min_trx_id,说明该版本对应的事务在生成ReadView之前就已经提交了,因此是可见的。
- 如果当前undo log的版本的trx_id≥max_trx_id,说明该版本对应的事务在生成ReadView之后才开始的,因此是不可见的。
MVCC在四种隔离级别下的区别
在读未提交(Read Uncommitted)级别下,事务总是读取到最新的数据,因此根本用不到历史版本,所以MVCC不在该级别下工作。
在串行化(Serializable)级别下,事务总是顺序执行。写会加写锁,读会加读锁,完全用不到MVCC,所以MVCC也不在该级别下工作。
真正和MVCC兼容的隔离级别是读已提交(Read Committed)与可重复读Repeatable Read。