四、MySQL事务

96 阅读11分钟

MySQL事务

事务概念

事务:一组动作要么全部成功,要么全部失败

事务特性

原子性:事务中的操作要么全部成功,要么全部失败

隔离性:事务与事务之间是相互隔离的,互相不可见

一致性:无论事务提交还是回滚,事务执行后的数据状态与执行前完全一致

持久性:事务只要提交了,数据就永久保存了,不会丢失。这是通过redo log日志完成的

MySQL事务语法注意事项

MySQL在开启事务后,可以对每条sql语句设置一个savepoint保存点。如果最后要回滚事务,可以回滚到任何保存点,这样保存点之前的sql语句都会执行。如此一来,回滚事务就不用所有sql语句都回滚。

MySQL在开启事务后,不要执行DDL、在事务中再开启事务、加载、复制等操作,因为这些操作执行前会偷偷帮我们把之前的sql语句都提交。

MySQL事务并发的问题

脏读:

一个事务读取了另一个事务还未提交的数据

不可重复读:

一个事务并发更新时,另一个事务查询两次相同数据的前后结果不一样

幻读:

一个事务并发新增或者删除时,另一个事务查询两次相同数据的前后结果不一样

MySQL的事务隔离级别

隔离级别:

  • read uncommit(读未提交):会有脏读、不可重复读以及幻读的问题
  • read commit(读已提交):会有不可重复读和幻读的问题
  • repeatable read (可重复读):单独的可重复读级别会有幻读的问题,也是默认的隔离级别
  • serializable(串行):事务并发问题都解决

隔离级别相关命令:

查看默认的隔离级别:SELECT @@GLOBAL.TX_ISOLATION;

查看此次会话的隔离级别:SELECT @@SESSION.TX_ISOLATION;

修改默认的隔离级别:SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

修改此次会话的隔离级别:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

事务隔离级别原理

MVCC(多版本并发控制)

MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后, 只有写写之间阻塞,其他三种操作都可以并行 ,这样大幅度提高了InnoDB的并发度。

当前读和快照读:

当前读:

像select ... lock in share mode(共享锁), select ... for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。当前读不会使用MVCC。

快照读:

像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

MVCC实现原理

MVCC的实现原理主要是依赖记录中的3个隐式字段,undo日志 ,Read View来实现的。

隐式字段:

每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段

  • DB_TRX_ID:当前事务ID
  • DB_ROLL_PTR:回滚指针,事务回滚时通过指针找到之前的数据
  • DB_ROW_ID:如果表没有指定主键,MySQL默认创建的主键

undo日志:

undo日志中记录了旧的数据

例如有以下场景:

  1. 有个事务插入persion表插入了一条新记录,记录如下,name为Jerry,age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL
  2. 现在来了一个事务1对该记录的name做出了修改,改为Tom
  3. 又来了个事务2修改person表的同一个记录,将age修改为30岁

undo日志中的记录如下:

image.png

从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录

Read View视图

Read View就是事务进行快照读操作的时候生产的读视图(Read View)。在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID

Read View有4个字段:

  • m_ids:当前活跃的事务编号集合
  • min_trx_id:最小活跃事务编号
  • max_trx_id:预分配事务编号,当前最大事务编号+1
  • creator_trx_id:创建数据的事务ID,记录了哪个事务创建了这行记录(新增,修改会产生新数据,都属于创建操作)
MVCC整体流程

read commit和repeatable read这两个隔离级别都使用了MVCC。在事务开启后,每一条select语句都会生成一个Read View,然后根据Read View信息和undo log版本链,遵循一定规则,获取数据。

版本链数据访问步骤:

  1. 判断当前事务id是否等于creator_trx_id,如果相等说明是当前事务自己修改了数据,可以访问
  2. 如果当前事务id不等于creator_trx_id,判断creator_trx_id是否在活跃事务列表中,如果在,不可访问
  3. 如果creator_trx_id不在活跃事务列表中,并且小于min_trx_id,说明当前事务已经提交,可以访问
  4. 如果creator_trx_id不在活跃事务列表中,并且大于等于max_trx_id,说明当前事务在Read View创建之后才有的,不可访问
  5. 如果creator_trx_id不在活跃事务列表中,并且大于min_trx_id,小于max_trx_id,可以访问

read commit和repeatable read的区别在于read commit每条select语句都会生成新的Read View,所以每次读取到的是最新的数据;而repeatable read在第一条select语句执行时会生成Read View,后面相同的select语句会复用前面的Read View,所以每次都是读到相同的数据。

read commit和repeatable read是通过MVCC机制,一个解决了脏读问题,另一个解决了脏读和不可重复读问题。但是repeatable read没有完全解决幻读问题,因为在查询时,只要两条select语句中间有一条当前读的sql语句,后面的select语句就会重新生成Read View。这样一来,两次的select语句就会读取不同的数据。

read uncommit隔离级别底层没有任何处理,没有加锁,也没有使用MVCC,所以什么问题也没有解决。

serializable隔离级别底层使用了读锁,使得读写并发的事务有序执行,所以解决了所有问题。

repeatable read脏写问题

repeatable read可能读到的不是最新的数据,如果对这个数据进行修改,再写回表中,就会导致脏写问题

解决办法:

  • 读取数据时可以使用当前读
  • 更新数据时,sql语句中加个条件,将修改列与之前读到的值做等值判断,如果不相等,说明在事务执行过程中旧值已经被修改,sql语句执行也会失败,可以重新执行事务

MySQL的锁

粒度上区分

全局锁:

全局锁是对整个数据库进行上锁,上锁后就无法对数据库中任何一张表进行相应的操作了

全局锁的上锁sql:FLUSH TABLES WITH READ LOCK

全局锁的释放锁sql:UNLOCK TABLES

全局锁有个应用场景是数据库备份。在备份时,不允许对数据库中数据进行修改,可以读取

表锁:

表锁是将一张表加锁,可以加表读锁,也可以加表写锁

MyISAM的表读写操作,都会默认给表加上读写锁。InnoDB大部分情况下使用的是行锁+MVCC机制,并发性更好些

以下sql会加表锁:

  • alter table:修改表结构时会加表锁
  • drop table和truncate table:删除表或者删除表中所有数据时会加表锁
  • lock tables:这个sql是给指定的表加锁
  • 全表扫描或者大范围扫描时会加表锁

行锁:

行锁是将一张表中某一行加锁

共享锁:又称为读锁,支持多个事务去读,但是不支持写

排他锁:又称为写锁,不支持其他事务读和写

以下sql会加行锁:

  • select ... for update:会对行加写锁
  • select ... lock in share mode:会对行加读锁
  • delete...:会对行加写锁
  • update...:会对行加写锁
  • insert...:会对行加写锁

在事务中使用行锁,那么只会在事务提交或者回滚后释放锁;不在事务中使用行锁,那么sql语句执行完就会释放锁

行锁可以提高并发量,但是也存在以下问题:

  • 死锁:行锁更容易出现死锁
  • 锁升级:一个事务如果试图锁定的行太多,InnoDB可能会将锁升级为表锁
  • 资源消耗:行锁需要更多的内存和cpu资源
  • 事务隔离级别:不同事务隔离级别会影响行锁的行为和性能

模式上区分

乐观锁:

乐观锁并不会加锁,它的基本思想是假设多个事务同时访问同一条数据,某个事务修改数据后准备更新,更新前检查是否有其他事务修改了这条数据。如果没有就提交事务,否则就回滚事务。

在MySQL中,乐观锁没有内部实现,但是可以通过编程技巧来实现。常见的实现方式是使用版本号(或者时间戳)字段。每当一条记录被修改时,更新版本号。在更新记录时,先检查版本号是否与读取数据时一样,一样则修改,不一样则事务回滚。

乐观锁适合读操作较多的场合。

悲观锁:

悲观锁是一种并发控制方法,是会对操作的数据进行加锁,以保证数据的完整性和一致性。

悲观锁适合写操作较多的场合。

意向共享锁和意向排他锁

意向锁是表锁,为了协调行锁和表锁的关系,支持多粒度锁并存。

当有事务A有行锁时,MySQL会自动为该表添加意向锁。事务B如果想申请整个表的写锁,那么不需要遍历整张表的每一行,判断是否有行锁的存在,从而提高性能。

意向共享锁与排他锁互斥,可以与共享锁共存;意向排他锁与排他锁和共享锁都互斥。

间隙锁和临键锁

行锁是对索引列进行加锁,如果查询条件中的列不是索引列,会加表锁。行锁+间隙锁就是临键锁

间隙锁是在repeatable read隔离级别下才会触发。

间隙锁是对两个索引范围进行锁定,不包含索引边界。是对不存在的数据进行加锁。

临键锁不光是对两个索引范围进行锁定,而且还包含索引边界(这是行锁的功能)。

唯一索引是指索引列中的数据不能够重复。

场景:表中有记录 id=10,20,30

select * from table where id > 15 for update;
  • 间隙锁:只会锁区间 (10,20) 和 (20,30) 以及 (30,+∞)
    无法防止其他事务修改id=20和30的记录
    无法防止在id=20之前插入新记录
  • 临键锁:间隙锁+行锁,会锁区间 (10,20), [20,30), [30,+∞)
    阻止修改id=20,30的记录