MySQL - 事务 & 锁
事务
第一个前提:事务是存储引擎层面支持的,InnoDB支持事务,MyISAM不支持事务。本文都是指的InnoDB。
第二个前提 :MySQL默认设置autocommit = on
,即任何语句若没有显示地开启事务,都被当做一个独立的事务进行执行 —— Even a select statement opens a transaction
老生常谈,说起事务,就得先提一下事务的ACID特性
事务的ACID特性
- 原子性 Atomicity
事务里的操作要么全部成功,要么全部失败 - 一致性 Consistency
事务和数据约束的一致性,事务完成前后,都不能与数据本身的完整性约束相悖,例如唯一索引约束,表结构约束等等。 - 隔离性 Isolation
数据库应该具有并发事务同时对数据进行读写修改的能力。 --> 事务的四个隔离级别 - 持久性 Durability
事务处理后,数据的修改应该是永久的,系统挂了,也不会丢失。
事务的隔离级别
- 读未提交 Read uncommitted
一个事务能读到另一个事务还没有提交的数据,会产生脏读 - 读提交 Read committed
一个事务只能读到别的事务提交过后的数据,会不可重复读 - 可重复读 Repeatable read
一个事务在事务进行的期间,同样的查询语句不同时刻查得到的结果应该是一样的,可能产生幻读(但是MySQL使用了next-key lock来解决) - 串行化 Serializable
不允许并发,一个数据只有被持有它的事务提交/回滚释放锁之后,才能被其他事务访问。
以上隔离级别由上到下,并发性能越来越差,隔离程度越来越深。InnoDB默认的隔离级别是可重复读,而大部分互联网公司设置的线上隔离级别是读提交。通过show variables like 'tx_isolation'
可以查看数据库事务级别。
有个我觉得很有意思能帮助理解的例子,看这里,对于“脏读”,“不可重复读”,“幻读”的解释,例子里也说的很清楚~
MVCC
解决以上“脏读”,“不可重复读”,“幻读”有两种思路,一个就是加锁,另外一个就是MVCC(Multi-version Concurrency Control)即数据多版本并发控制。MVCC的目的:让读写互相不阻塞,提高事务并发处理的能力。MVCC的核心思想:提供数据的历史版本,即我记录的不是一个静态的2D的表,而是一个“3D”的表,每个数据还有一个维度是他的版本号(transaction id/trx_id,该id按照先后严格递增)。
一致性视图
InnoDB在实现MVCC的时候,用到了一致性视图(consistent read view)。这个视图和我们查询的时候的视图不是一个同一个东西。这个一致性视图其实是根据数据的最新版本和undo log(回滚日志)计算出来的。
以可重复读举例,事务一开始的时候,就创建了一个一致性视图,该视图的数据,如上所述是根据数据当时的最新版本和undo log计算出来的。而这个视图计算出的数据满足以下条件:
- 数据版本未提交,不可见 --> 还要根据undo log往前翻版本
- 数据版本已提交,但是是在视图创建后提交的,不可见
- 数据版本已提交,视图创建前提交的,可见
总的来说,就是根据当前最新数据,和undo log,找到一个提交过的最新版本,作为一致性视图。
- 一个栗子🌰:
假设事务A开始前,(id, k) -> (1,1)这个数据已经提交了。问A和B分别select到的值是什么?
答案是:A select 到1, B select到3 ~
- 变化一下的另一个栗子🌰:
这个例子的结果和上面一致,但是事务B会等到事务C提交后,才能执行。因为第一,update语句是先读后写,而这个读是当前读。第二,事务的两阶段提交(2PC 在日志模块讲到)代表着k=2这个数据版本的写锁没有释放所以事务B就wait在那儿了,要等C提交了之后,事务B才能继续进行它的当前读。
当前读:读的是数据的最新版本,并对读的数据加锁;例如update, delete, insert,select... for update, select ... lock in share mode; 当前读也是通过next-key lock 来实现的
快照读:也就是我们说的这个一致性视图其实就是快照读
MVCC是不是和Copy-on-Write(COW)的思想有一点异曲同工呢?
锁
根据锁的粒度的不同,有以下几种分类:
- 全局锁
锁整个数据库实例的,命令是Flush tables with read lock
,那么所有数据更新语句,事务提交,表结构修改等等都会被阻塞。主要用于全库的逻辑备份。(因为不是所有引擎都支持一致性读,即备份的时候可以给这个事务创建一个一致性视图,不阻塞更新语句) - 表锁和元数据锁
表锁语法:lock tables xxx read/write
;元数据锁(MDL)不需要显示使用,在访问一张表时自动会加上MDL 读锁; 当做DDL的时候,会加上MDL写锁。读锁不互斥,读写锁,写锁互斥。 - 行锁
行锁是由存储引擎自己实现的,如InnoDB支持行锁,MyISAM不支持行锁。注意📌:行锁,其实是加在索引上的。
两阶段锁协议 2PL
InnoDB的行锁,是需要的时候加上,事务提交了才释放;所以我们在一个事务中,尽量要把可能引起锁冲突最多的锁往后放。
Next-lock key
即便是可重复读的隔离级别,如果只有行锁,一样会出现幻读的情况,即一个事务前后两次查询同一个范围,结果不同。如select name from user_info where age >= 14 and age <= 20 for update
,如果期间有其他事务插入了一条满足age范围[14,20]的数据,第二次读就会多读到一个数据。所以InnoDB搞了一个间隙锁(gap lock),而间隙锁 + 行锁就形成了next-lock key一个左开右闭的区间。所以以上语句,在可重复度的隔离级别下,会在索引age上,把间隙给锁住。
注意:当查询的索引含有唯一属性时,next-lock key会优化降级为行锁
死锁
并发系统不同线程出现循环资源依赖,都在等待对方线程释放资源,进入无限等待。
- 死锁出现后如何解决?
- 设置锁超时,
innodb_lock_wait_timeout
默认是50s - 发起死锁检测,发现死锁后,回滚死锁链中某一个事务。可以设置
innodb_deadlock_detect
为on,但是死锁检测会消耗CPU资源,对于热点数据,需要控制并发度。