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日志中记录了旧的数据
例如有以下场景:
- 有个事务插入persion表插入了一条新记录,记录如下,name为Jerry,age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL
- 现在来了一个事务1对该记录的name做出了修改,改为Tom
- 又来了个事务2修改person表的同一个记录,将age修改为30岁
undo日志中的记录如下:
从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的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版本链,遵循一定规则,获取数据。
版本链数据访问步骤:
- 判断当前事务id是否等于creator_trx_id,如果相等说明是当前事务自己修改了数据,可以访问
- 如果当前事务id不等于creator_trx_id,判断creator_trx_id是否在活跃事务列表中,如果在,不可访问
- 如果creator_trx_id不在活跃事务列表中,并且小于min_trx_id,说明当前事务已经提交,可以访问
- 如果creator_trx_id不在活跃事务列表中,并且大于等于max_trx_id,说明当前事务在Read View创建之后才有的,不可访问
- 如果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的记录