文章目录
- 前言
- 一、mysql 的事务
- 1 .1 mysql 事务:
- 1 .2 mysql 为什么要支持事务:
- 二、mysql的事务实现:
- 2.1 mysql的事务隔离级别:
- 2.2 mysql 脏读,不可重复读,幻读:
- 2.2.1 脏读:
- 2.2.2 不可重复读:
- 2.2.3 幻读:
- 2.2.4 不可重复读和幻读的区别:
- 2.3 mysql 不同事务隔离级别的实现(InnoDB):
- 2.3.1 MVCC:
- 2.3.1.1 MVCC 思想:
- 2.3.1.2 MVCC实现:
- 2.3.1.2.1 Read View 与MVCC 的关系:
- 2.3.1.2.2 Read View 的结构:
- 2.3.1.2.3 基于Read View 实现不同的隔离级别:
- 2.3.2 Mysql锁:
- 2.3.2.1 Mysql锁的作用:
- 2.3.2.2 Mysql锁的类型:
- 2.3.2.3 Mysql表锁与行锁的粒度区别:
- 2.3.2.4 Mysql锁的本质:
- 2.3.3 Mysql当前读和快照读:
- 2.3.3.1 当前读和快照读的区别:
- 2.3.3.2 MVCC和锁的关系:
- 2.3.3.3 当前读时RC怎么实现隔离性:
- 2.3.3.4 当前读时RR怎么实现隔离性:
- 三、总结
前言
Mysql 的事务是什么,为什么它要支持事务,它的事务又是怎么实现;
一、mysql 的事务
1 .1 mysql 事务:
事务是MySQL等其他关系型数据库中的一个重要概念。事务是一系列原子性的SQL查询,这些查询要么全部执行,要么全部不执行。事务特别适合处理财务信息或其他需要保持一致性的重要数据。
1 .2 mysql 为什么要支持事务:
-
需要保证 数据一致性:事务可以保证一系列操作作为整体被处理,要么全部成功,要么全部失败,当发生错误时,可以将数据回滚到事务开始时的状态,保证了数据的正确性和一臀性。
-
需要考虑 并发控制:在多用户并发访问数据库时,事务可以提供必要的锁机制,防止用户之间数据访问发生冲突。
-
需要支持 错误恢复:当系统发生故障时,只需要回滚还未提交的事务,而不需要重新运行整个程序。
二、mysql的事务实现:
2.1 mysql的事务隔离级别:
mysql 不仅支持了事务,而且还提供了事务的多种隔离级别可以供用户选择,隔离级别越高,能防止的并发问题越多,但相应的性能损耗也越大,因此需要根据实际情况进行选择:
-
READ UNCOMMITTED(RU读未提交): 最低级别的隔离,此级别可以读取到其他事务未提交的更改(即“脏读”),并且可能会产生幻读和不可重复读。
-
READ COMMITTED(RC 读已提交): 此隔离级别只允许读取其他事务已经提交的更改,避免了脏读的问题但可能会产生幻读和不可重复读。
-
REPEATABLE READ(RR 可重复读): 此级别在事务开始后,不再允许其他事务更改已经被读取的数据,可以避免不可重复读的问题。 MySQL 的 InnoDB 存储引擎的默认隔离级别就是 REPEATABLE READ,但由于在这个级别下 InnoDB 实施了额外的锁策,所以在对行进行“范围查询”的情况下,可以避免掉大部分的幻读。
-
SERIALIZABLE(串行化): 最高级别的隔离,此级别强制事务串行执行,牺牲了并发性能,但可以解决所有的隔离级别问题,包括幻读。
2.2 mysql 脏读,不可重复读,幻读:
2.2.1 脏读:
- 2673 的事务 中开启了查询;
- 2674 的事务中更新了改条数据,但是未 提交
- 2673 的事务读取到了2674 中未提交的修改;
因为在2674 中未提交的修改,会被保存到buffer bool 脏页中,等待事务提交后的刷脏,所以此种读到了脏页的数据称之为 脏读;
2.2.2 不可重复读:
- 2673 的事务 中开启了查询;
- 2674 的事务中更新了改条数据,并且 提交
- 2673 的事务读取到了2674 中已提交的修改;
在事务2673 中两次读取的数据不一致,不能重复读取,不可重复读;
2.2.3 幻读:
- 2673 的事务 中开启了查询;
- 2674 的事务中插入了条数据,并且 提交
- 2673 的事务读取到了2674 中已提交的数据;
在事务2673 中两次读取的数据不一致,不能重复读取,读到了另外一个事务插入的数据,幻读;
2.2.4 不可重复读和幻读的区别:
- 不可重复读: 针对 更新和删除;
- 幻读:针对插入,只有插入数据造成了一个事务两次读取数据不一致的情况称之为 幻读;
2.3 mysql 不同事务隔离级别的实现(InnoDB):
MySQL的InnoDB存储引擎中,MVCC(MultiVersion Concurrency Control)多版本并发控制和锁机制一起协作实现各种事务隔离级别。
2.3.1 MVCC:
2.3.1.1 MVCC 思想:
生成一个数据请求时间点的一致性数据快照 (Snapshot),并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取 (MVCC) MultiVersion Concurrency Control.
核心思想:建立了一个快照,同一个事务无论查多少次都是相同的数据;
- 一个事务能看到的数据版本:
第一次查询之前已经提交的事务的修改
本事务的修改 - 一个事务不能看见的数据版本:
在本事务第一次查询之后创建的事务 (事务ID比我的事务ID大)2、活跃的 (未提交的) 事务的修改
2.3.1.2 MVCC实现:
2.3.1.2.1 Read View 与MVCC 的关系:
InnoDB 引擎中,MVCC 借助于Read View实现,ReadView是InnoDB通过MVCC实现数据可见性的一个视图概念,表示在某个时间点上,对于同样的数据,由于并发事务的存在,不同的事务看到的可能是不同版本的数据,ReadView就是用来确定当前事务应该看到哪个版本的数据。
2.3.1.2.2 Read View 的结构:
2.3.1.2.3 基于Read View 实现不同的隔离级别:
- 从数据的最早版本开始判断 (undo log);
- 数据版本的trx_id = creatortrx_id,本事务修改,可以访问;
- 数据版本的trx_id < min_trx_id (未提交事务的最小ID) ,说明这个版本在生成ReadView已经提交,可以访问;
- 数据版本的trx_id>max_trx_id (下一个事务ID) ,这个版本是生成ReadView之后才开启的事务建立的,不能访问;
- 数据版本的trx_id 在min_trx_id和max_trx_id之间,看看是否在m_ids中。如果在,不可以。如果不在,可以;
- 如果当前版本不可见,就找undo log链中的下一个版本, 在undolog 链中继续使用判断规则查找可以看到的版本;
RR 的 Read View 是事务第一次查询的时候建立的,后续Read View不会被修改,所以不存在不可重复读和幻读的情况。
RC的 Read View 是事务每次查询的时候建立的,所以每次读取都重新按照读取的规则建立Read View 会出现不可重复读和幻读。
2.3.2 Mysql锁:
2.3.2.1 Mysql锁的作用:
数据库的锁机制是一种保证同时多个用户并发读写数据时数据一致性和完整性的方法。根据其锁定的内容粒度不同,MySQL锁可以分为全局锁、表级锁、行级锁等。在MySQL数据库中,锁的主要作用包括:
-
保证事务的原子性:一个事务中包含的所有操作,要么全部成功,要么全部失败,保持数据一致性。
-
隔离性:多个用户并发地存取数据时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
-
防止数据不一致性利用锁机制,可以防止一个事务在读取一个数据行时,另一个事务却对此行进行修改,导致数据不一致。
-
防止更新丢失:当多个并发事务试图同时修改数据时,锁可以确保每次只有一个事务能够进行操作。例如,两个事务同时读取一个账户余额,并且都在其基础上减去10,如果没有排他锁,最终账户余额可能只会减去10。有了排他锁,另一个事务会等待,直到第一个完成,这样,账户余额将减去20,这才是正确的结果。
-
提高并发性能:适当的锁策略可以允许多个用户对数据库的同一部分进行操作,从而提高整体性能。
-
在MySQL数据库操作中,应用适当的锁策略,可以大大提高操作效率,保证数据的正确性,防止数据冲突。
2.3.2.2 Mysql锁的类型:
-
行锁:行锁是MySQL InnoDB表类型提供的。当访问到一行记录时,就将这行记录锁定。这是最高的粒度,但是开销最大。如果启用了行锁,只有当前事务提交后,其他事务才能操作这行数据。
-
表锁:表锁在表级别上锁定整个表,一个事务如果获得了对一个表的锁,其他事务就不能对这个表进行操作。MyISAM存储引擎使用的就是表级锁。表锁的优势是开销小,加锁快;缺点是并发度最低,冲突最多,尤其是在写操作多的情况下,大部分读操作也会被堵塞。
-
间隙锁:InnoDB引擎中的一种锁,用于解决幻读问题。一个范围上添加一个锁,让这块范围内无法插入新的行。
-
共享锁(S锁):多个读操作之间不会互相阻塞,所以读操作会采取共享锁,允许一个事务进行读取操作,但是在该事务持有共享锁期间,阻止其他事务进行写入操作。
加锁释锁方式:
select * from student where id=1 LOCK IN SHARE MODE;
commit/rollback;
- 排他锁(X锁):如果一个事务对数据进行了改动(如插入、删除、更新操作),则需要对数据加上排他锁,阻止其他事务同时对数据进行读取和修改操作。
加锁释锁方式
-- 自动,默认加上X锁::
delete /update /insert
-- 手动:
select * from student where id=1 FOR UPDATE;
commit/rollback;
- MySQL同时还有一种名为元数据锁(MDL)的锁,用于事务或者全局来限制对一个对象的访问。这个锁是自动获取的,不能由用户显式获取。
2.3.2.3 Mysql表锁与行锁的粒度区别:
- 锁定粒度:表锁 > 行锁
- 加锁效率:表锁 > 行锁
- 冲突概率:表锁 > 行锁
- 并发性能:表锁< 行锁
2.3.2.4 Mysql锁的本质:
Mysql的锁是通过索引来实现的,MySQL 的锁和索引之间存在密切的关联关系。
-
首先,行级锁的实现是基于索引的。也就是说,如果在执行查询语句时没有使用索引,那么MySQL会进行表级锁定。相反,如果使用了索引,MySQL将使用行级锁。这是因为MySQL只有通过索引才能定位到数据行,之后对这行数据加锁,完成数据的读取或者修改。
例如,对于一个UPDATE语句,如果条件中包含的列有索引,那么MySQL将使用行级锁。如果条件中的列没有索引,那么MySQL将用表级锁。这也就是为什么我们在实际应用中,经常要注意创建合适的索引,以提高查询性能的原因。 -
另外,在InnoDB存储引擎中,实际上行级锁分为两种类型:记录锁(Record Locks)和间隙锁(Gap Locks),它们的作用范围都和索引有关。
记录锁是单个行记录上的锁,含有索引的列可以很精确的选择哪一行,并对其加锁。
间隙锁是在索引的记录之间的锁,用来防止幻读(Phantom Read)现象。这个锁,锁定的并不是索引的记录,而是记录之间的“间隙”。 -
所以,从行级锁的设计和使用来看,它和索引的关系非常密切。为了更有效率地使用行级锁,我们必须对索引有深入的理解,合理的创建和使用索引。
2.3.3 Mysql当前读和快照读:
2.3.3.1 当前读和快照读的区别:
MySQL的InnoDB存储引擎中的事务读操作可以分为两种,当前读(Current Read)和快照读(Consistent Read)。
-
当前读(Current Read):又称之为"锁定读",在进行读取的时候会加锁,并且读取的是最新的数据。INSERT、UPDATE、DELETE、LOCK IN SHARE MODE 和 SELECT FOR UPDATE 这些语句都属于当前读。当前读会读取最新的数据,适用于需要读取最新(current)数据版本的场合,例如更新操作,需要先查看记录的当前版本。
-
快照读(Consistent Read):又称之为一致性读,它读取的是行的快照版本,并不需要加锁,普通的SELECT操作就是快照读。快照读通过多版本并发控制(MVCC)获取数据在某一时刻的版本,而不是最新版本。这种方式尽可能地提高了数据的并发读取,保证在读取过程中不会被其他写入操作阻塞,确保了读的一致性。
简单来说,如果你的读取操作需要对数据进行更新,应当使用当前读;而如果你只是需要读取数据,而不进行修改,那就应该使用快照读。
2.3.3.2 MVCC和锁的关系:
既然有了MVCC 进行数据普通读取时 基于RR 隔离模式已经解决了不可重复读和幻读的问题,为什么还要用到锁,锁对MVCC 作了哪些补充呢:
-
在数据修改(如UPDATE, DELETE, INSERT等)时,MVCC是无法防止数据竞态条件的,这就需要使用锁来解决,以确保数据的一致性和完整性;
-
在某些场景下,我们可能需要更严格的数据控制。例如,一个线程读取了一条记录并对其进行了修改,我们需要防止在这个线程完成修改之前,其他线程读取或修改这条记录。这就需要显式地进行行级锁定;
-
在更高的隔离级别(如Serializable)下,仅仅依靠MVCC是无法满足隔离要求的,必须依靠更严格的锁策略;
-
在需要控制对整个表进行操作权限时,也需要用到表级锁。比如,一种策略是在进行对表的大批量写入操作时,先对整个表加锁,避免写入过程中读取到不完整的数据;
-
在一个事务中,如果使用普通的的select 读取,会使用到mvcc 来保证数据的一致性问题,但是如果使用LOCK IN SHARE MODE 和 SELECT FOR UPDATE 进行数据读取,这个时候需要使用到锁来保证改隔离级别数据的一致性问题;
-
总的来说,尽管MVCC可以解决一部分并发控制的问题,但是在必要的情况下,仍然需要数据库锁的参与,以保证数据的一致性和完整性;
2.3.3.3 当前读时RC怎么实现隔离性:
在 RC 隔离级别下,既然当前读已经对读取到的数据进行了加锁,在事务未提交之前,其他的事务无法对改数据进行修改,那么为什么还会出现不可重复读和幻读?
不可重复读主要是由于在同一个事务中,多次读取同一数据时,因其他事务的提交,使得同一数据的内容发生了改变。在RC(Read Committed)隔离级别下,每次读取都是读取的最新已提交的数据,所以就可能出现不可重复读的问题。
以这样的例子来说明,假如有两个事务,事务1和事务2。
- 事务1读取一个数据。
- 事务2修改了这个数据,并提交。
- 事务1再次读取这个数据。
在这个过程中,虽然事务1每次读取数据时,由于读取的是当前版本(已提交的数据),会对该数据加锁,防止数据在读取过程中被修改。但是,这个锁是在读取期间存在的,读取结束后锁就被释放了。这就有可能出现在事务1第一次读取和第二次读取之间,事务2修改了这个数据并提交,所以事务1两次读取的数据就可能不一样,也就是出现了不可重复读和幻读的情况。
2.3.3.4 当前读时RR怎么实现隔离性:
在RR(Repeatable Read)隔离级别下,当前读(例如 SELECT … FOR UPDATE, UPDATE, DELETE等)会在读取数据时,对数据行加上排他锁,防止其他事务对该行进行并发修改。
这个锁不会在读取结束后立即释放,而是在整个事务结束后才会释放。也就是说,只有当整个事务提交(commit)或者回滚(rollback)后,这个锁才会被释放。这样可以确保在一个事务中,一个数据行全程不会被其他事务改变,从而保证了可重复读的性质,因为在RR隔离级别下,会有间隙锁锁的存在,进行数据范围的锁定,这样也解决了幻读。
如果是普通读(不带FOR UPDATE的SELECT等),则会读数据在事务开始时的快照,而不是最新版本的数据。这也是RR级别下可以避免不可重复读和幻读的原因。
三、总结
Mysql 的事务与锁本质上都是用来解决读到的数不一致性的问题:
- 在一个事务中对于不带LOCK IN SHARE MODE 和 SELECT FOR UPDATE 进行读取的普通select(快照读) 来说,读取数据使用的是MVCC 技术,在RC 级别下因为每次select 都会建立新的read view 从而会读取到新的其它事务提交的数据,从而造成不可重复读和幻读的情况;在RR 级别下载第一次select 时建立的read view 一直到事务结束都不会进行变化,所有不会有不可重复读和幻读的情况;
- 在一个事务中对于 LOCK IN SHARE MODE 和 SELECT FOR UPDATE 加锁的select(当前读) ,读取数据时会读到当前的最新数据,在RC 级别下只会在读取数据的时候读数据加锁,读取结束之后就会释放锁,所以此时其它的事务在 前一个事务还没有结束的情况下,也可以对改数据进行修改,从而造成不可重复读和幻读的情况;在RR级别下,读取到的数据会被加锁,直到事务结束之后才会释放到锁,在此期间其它的事务并不能对加锁的数据进行修改(会被阻塞直到加锁数据的事务结束),所以在改事务期间,数据是可以重复读取的;在RR 级别下 会使用到间隙锁,对范围的数据进行加锁,从而保证改间隙锁范围内不允许插入数据(直到改事务结束),从而也避免了幻读的情况;
- 在一个事务上,实际即存在快照读又存在当前读,它们根据你的查询方式不同而使用不同的读取,如果使用普通的select 则使用快照读,如果使用LOCK IN SHARE MODE 和 SELECT FOR UPDATE则会加锁,它们共同存在且互不影响;
- 行级锁的实现是基于索引的,也就是说,如果在执行查询语句时没有使用索引,那么MySQL会进行表级锁定。相反,如果使用了索引,MySQL将使用行级锁。为了更有效率地使用行级锁,我们必须对索引有深入的理解,合理的创建和使用索引。