会出问题的情况
只是读的话,是不会出现并发问题的,那我们首先将这种情况忽略,那么问题出现的重灾区就是:
- 写 - 写
即并发事务相继对一条记录进行改动,在这种情况就可能发生脏写。在任何隔离级别下MySQL都不允许脏写发生。所以在多个并发事务对相同记录进行改动的时候,MySQL会让他们排队执行,这个排队执行事实上是通过锁来实现的,类似于现在的面试(不考虑群面这种情况),一般面试官面试完这个人之后,才会叫HR让下个人进来。在MySQL中当一个事务要对某条事务进行改动的时候,首先会看内存中有没有与这条记录关联的锁结构,当没有的时候就会在内存中生成一个锁结构与之相关联。
- 读 - 写 或 写 - 读
在这种情况下,可能发生脏读、不可重复读、幻读的问题。这里注意一下幻读问题的产生是因为某个事务读取了一个范围的记录,之后其他事务又在该范围内插入新记录,该事务再次读取该范围的记录的时候,可以读到新插入的记录。所以幻读并不是因为读取和写入一条相同的记录而产生。
在前面我们已经唠叨过在不同的隔离级别下可能会发生的问题了,这里再简单的提一下,不再过多的进行赘述,不同数据库厂商对SQL标准的支持可能都不一样,MySQL在 REPEATABLE READ隔离级别实际上就已经解决了幻读问题。
解决方案在MySQL也就是两个:
- MVCC: 读利用多版本并发控制(MVCC), 写操作进行加锁。
- 读、写都采取加锁方式
在一些业务场景下,我们不允许读取记录的旧版本,而是每次都必须去读取记录的最新版本。比方说在银行的存款事务中,我们就需要先读取账户余额,然后将其加上本次存款的数额,最后再写入到数据库中。在将账户余额读取出来后,就不想让别的事务再访问该余额。直到本次存款事务执行完成,其他事务才可以访问账户的余额。这样在读取记录的时候也就需要对其进行加锁操作。
为什么呢,我们来分析一下,我们讨论的还是在可重复读这个级别上进行讨论,假设我们以转账业务为例,如果允许读旧的账户余额会发生什么样的问题。
如果是像下面的语句来执行:
UPDATE Student Set money = money + 50 where id = '1';
两次提交并不会产生问题,因为两个事务排队执行。我原先的想法是在执行UPDATE的时候MySQL也读了,这样读到事务提交之前的记录,最终就会是转了两次账,只加了五十块钱。那如果是如果是做运算呢,比如说我们在代码里面做运算,然后最后的更新语句像下面这样就会有问题:
UPDATE Student Set money = 60 where id = '1';
后提交的事务就会把先提交的事务覆盖掉,这并不算脏写,只是两次独立的更新操作。
采用MVCC方式的话,读-写操作并不冲突,性能更好,采用加锁方式的话,需要排队执行,影响性能。一般情况下我们更愿意采取MVCC来解决事务并发执行带来的问题,但是业务在某些情况下,不能接受MVCC,在MySQL里面似乎也只剩下了加锁这一个选项。
事务利用MVCC进行的读取操作称之为一致性读,或者一致性无锁读、快照读。所有的SELECT语句在READ COMMITTED、REPEATABLE READ隔离级别都算是一致性读。
锁概念浅析
行共享锁和独占锁
在MySQL中锁可以大致分为两类:
- 共享锁 英文名 Shared Locks,简称为S锁。
如果一个事务某个记录上有共享锁, 如果另一个事务也想获取这把锁,也是可以的,这就意味着两个事务在该记录上可以同时持有该锁。
- 独占锁 也常称 排他锁,英文名: Exclusive Locks , 简称为S锁,
如果事务T1已经获取了该记录的S锁,事务T2想要再获取一条记录的X锁,那么此操作就会被阻塞,直到T1释放掉S锁。
MySQL提供了两种语法来让我们在读取记录的时候就可以获取该记录的X锁、S锁:
SELECT ... LOCK IN SHARE MODE; 在读取记录的时候获取该记录的共享锁 SELECT ... FOR UPDATE; 在该事务中获取该记录的X锁
如果当前事务执行了获取共享锁的语句(SELECT ... LOCK IN SHARE MODE),那么它会为读取到的记录加S锁,这样允许别的事务继续获取这些记录的S锁(如果其他事务也使用 SELECT ... LOCK IN SHARE MODE;来读取这些记录),但是不能获取这些记录的X锁(比方说使用SELECT ... FOR UPDATE来读取这些记录,或者修改这些记录 ) 如果别的事务想要获取这些记录的X锁,那么它们会阻塞,直到当前事务提交之后将这些记录尚的S锁释放掉。
如果事务中执行了SELECT ... FOR UPDATE, 那么它会为读取到的记录加X锁,别的事务即无法获取这个记录的S锁和X锁,如果事务想要获取这些记录的S锁和X锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的X锁释放掉。
表共享锁和独占锁
上面我们提到的可以认为是行锁,也就是针对若干行进行加锁,这个锁的粒度是比较细的。其实一个事务也可以在表级别进行加锁,自然就被称之为表级锁或者表锁,对表加锁影响整个表中的记录,给表加的锁也可以被分为共享锁(S锁)和独占锁(X锁):
如果一个事务给表加了S锁:
- 其他事务可以继续获取得该表的S锁
- 其他事务可以继续获取该表中记录的S锁
- 别的事务无法获取该表的X锁
- 如果该行记录上面有S锁,那么其他事务无法获得该行记录的X锁
如果一个事务给表加了X锁(独占这个表):
- 其他事务无法获得该表的S锁
- 如果该表中的记录上有X锁,那么其他事务无法获得该记录的S锁。
- 如果该表中的记录上有S锁,那么其他事务无法获得该记录的X锁
- 其他事务不可以继续获得该表中的某些记录的X锁。
但这事实上有两个问题,如果MySQl想对表整体上S锁,首先需要确保表中的记录不能有X锁,遍历表的记录?MySQL的开发人员设计了意向锁:
- 意向共享锁 Intention Shared Lock, 简称为IS锁。当事务准备在某条记录尚加S锁时,需要先在表加上IS锁。
- 意向独占锁, Intention Exclusive Lock,简称为IX锁。当事务准备在某条记录上加X锁,需要先在表级别加一个IX锁。
这样就可以避免全表扫表。
表锁概述
在对某个表执行一些ALTER TABLE、DROP TABLE这里的DDL语句时,其他事务对这个表并发执行增删改查会发生阻塞,同理,某个事务对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,其他会话中执行DDL语句也会发生阻塞。这个过程其实是通过Server层使用一种元数据锁(Metadata Locks ,简称为MDL)来实现的,一般情况下也不会使用InnoDB存储引擎提供的表级别S锁、X锁。所以这个表级的S、X锁颇有种鸡肋的感觉。我们重点关注行锁。上面我们唠叨的意向锁也属于表锁。再有就是MySQL的自增属性,在MySQL有两种方式实现自增:
- 采用AUTO-INC锁,也就是在执行插入语句的时候就在表上加上一个AUTO-INC锁,然后为每条自增属性的赋值。一个事务持有AUTO—INC锁,其他插入事务会被阻塞。插入语句执行之后就被释放。
不确定插入的记录数量可采取AUTO-INC锁。
- 轻量级锁,在为插入语句生成自增属性列的值之后,就把该轻量级锁释放掉,并不需要等到整个插入语句执行完成才释放锁。
如果语句执行前就可以确定插入记录数量,一般采用轻量级锁的方式对自增列赋值。
那么该如何选择自增方式呢,InnoDB中有一个innodb_autoinc_lock_mode的系统变量来控制哪两种方式来为自增列赋值,为0时,一律采用AUTO-INC锁,为2时,一律采用轻量级锁(可能会造成不同事务中的插入语句的自增列是交叉的,在有主从复制的场景是不安全的)。当为1时,插入记录数量确定时,采取轻量级锁,不确定时采取AUTO-INC锁。
总结
MySQL锁的概念比我原想的要复杂,知也无涯,生也有涯哉,本来打算这一节将MySQL中的锁大致过一遍,但是到现在还是没有梳理出一条主线将这些内容连在一起。到最后就是只介绍了行锁、表锁的基本概念。