【MySQL】十二、掌握MySQL锁的必备知识
本文主要介绍MySQL数据库的必须要掌握的数据库锁的相关知识
多个事务更新同一行数据时,是如何加锁避免脏写的
当有多个事务同时并发更新一行数据的时候,不就是会有脏写的问题吗?那么脏写是靠什么防止的呢?
其实就是靠锁机制,依靠锁机制让多个事务更新一行数据的时候串行化,避免同时更新一行数据。
在MySQL里,假设有一行数据摆在那儿不动,此时有一个事务来了要更新这行数据,这个时候他会先琢磨一下,看看这行数据此时有没有人加锁,一看没人加锁,说明他是第一个人,此时这个事务就会创建一个锁,里面包含了自己的trx_id和等待状态,然后把锁跟这行数据关联在一起。
同时更新一行数据必须把他所在的数据页从磁盘文件里读取到缓存页里才能更新的,所以说此时这行数据和关联的锁数据结构都是在内存里的,如下图:
上图中,因为事务A给那行数据加了锁,所以此时就可以说那行数据已经被加锁了。
既然被加锁了,此时就不能再让别人访问了,现在有另外一个事务B过来了,这个事务B也想更新那行数据,此时就会检查一下,当前这行数据有没有别人加锁,这时就会发现事务A已经给这行数据加锁了,这怎么办呢?
事务B也会生成一个锁数据结构,里面有他的trx_id,还有自己的等待状态,但是他因为是在排队等待,所以他的等待状态就是true了,意思是我在等着呢,如下图:
接着事务A这个时候更新完了数据,就会把自己的锁给释放掉了,锁一旦释放了,他就会去找此时还有没有别人也对这行数据加锁了呢,他会发现事务B也加锁了。于是就会把事务B的锁里的等待状态修改为false,然后唤醒事务B继续执行,此时事务B就获取到锁了,如下图:
共享锁和独占锁到底是什么?
多个事务同时更新一行数据,此时都会加锁,然后都会排队等待,必须一个事务执行完毕了,提交了,释放了锁,才能唤醒别的事务继续执行,那么在这多个事务运行的时候,加的是什么锁呢?
其实是X锁,也就是Exclude独占锁,当有一个事务加了独占锁之后,此时其他事务再要更新这行数据,都是要加独占锁的,但是只能生成独占锁在后面等待。
那么当有人在更新数据的时候,其他的事务可以读取这行数据吗?默认情况下需要加锁吗?答案是不用,因为默认情况下,有人在更新数据的时候,然后你去读取这行数据,直接默认就是开启mvcc机制的。
也就是说,此时对一行数据的读和写两个操作是不会加锁互斥的,因为MySQL设计的mvcc机制就是为了解决这个问题,避免频繁加锁互斥。
此时你读取数据,完全可以根据你的ReadView,去undo log版本链条里找到一个你能读取的版本,完全不用去顾虑别人在不在更新。
就算你真的等他更新完毕提交了,基于mvcc机制,你也读取不到他更新的值,因为ReadView机制是不允许的,所以你默认情况下的读,完全不需要加锁,不需要去care其他事务的更新加锁问题,直接基于mvcc机制读某个快照就可以了。
万一要是你在之心查询操作的时候,就是想要加锁呢?
那也是ok的,MySQL首先支持一种共享锁,就是S锁,这个共享锁的语法如下:
1 | select * from table lock in share mode |
在查询语句后面加上lock in share mode,意思就是查询的时候对一行数据加共享锁。
如果此时有别的事务在更新这行数据,已经加了独占锁,此时你的共享锁能加吗?当然不行了,共享锁和独占锁是互斥的,此时你这个查询只能等待了。
那么如果你先加了共享锁,然后别人来更新要加独占锁可以吗?也是不行的,此时锁是互斥的,只能等待。
如果你在加共享锁的时候,别人也加共享锁呢?此时是可以的,你们俩都是可以加共享锁的,共享锁和共享锁是不会互斥的。
所以可以看出一个规律,就是更新数据的时候必然是加独占锁,独占锁和独占锁是互斥的,此时别人不能更新;但是此时你要查询,默认是不加锁的,走mvcc机制读快照版本,但是你查询是可以手动加共享锁的,共享锁和独占锁是互斥的,但是共享锁和共享锁是不互斥的。
不过一般开发业务系统的时候,其实查询主动加共享锁的情况比较少见,数据库的行锁是实用功能,一般不会再数据库层面做复杂的手动加锁操作,反而会用基于redis/zookeeper的分布式锁来控制业务系统的锁逻辑。
另外就是,查询操作也可以加互斥锁,他的方法是:select * from table for update
这个意思就是我查出来数据以后还要更新,此时我加独占锁,其他人都不要更新这个数据了。
一旦你查询的时候加了独占锁,此时在你事务提交之前,任何人都不能更新数据了,只能你在本事务里更新数据,等你提交了,别人在更新数据。
在数据库里,哪些操作会导致在表级别加锁呢?
在多个事务并发更新数据的时候,都是要在行级别加独占锁的,这就是行锁,独占锁都是互斥的,所以不可能发生脏写问题,一个事务提交了才会释放自己的独占锁,唤醒下一个事务执行。
如果你此时去读取别的事务在跟新的数据,有两种可能:
- 第一种可能是基于mvcc机制进行事务隔离,读取快照版本,这是比较常见的;
- 第二种可能是查询的同时基于特殊语法去加独占锁或者共享锁。
如果你查询的时候加独占锁,那么跟其他更新数据的事务加的独占锁都是互斥的,如果你查询的时候加共享锁,那么跟其他查询加的共享锁是不互斥的,但是跟其他事务更新数据就加的独占锁是互斥的,跟其他查询加的独占锁也是互斥的。
当然一般而言,不是太建议在数据库粒度去通过行锁实现复杂的业务锁机制,而更加建议通过redis、zookeeper来用分布式锁实现复杂业务下的锁机制,其实更为合适一些。
因为如果你把分布式系统里的复杂业务的一些锁机制依托数据库查询的时候,在SQL语句里加共享锁或者独占锁,会导致这个加锁逻辑隐藏在SQL语句里,在你的Java业务系统层面其实是非常不好维护的,所以一般是不建议这么做的。
比较正常情况而言,其实还是多个事务并发运行更新一条数据,默认加独占锁互斥,同时其他事务读取基于mvcc机制进行快照版本读,实现事务隔离。
在数据库里,不光可以通过lock in share mode、for update等加行锁,还可以通过一些方式在表级别加锁。
有些人可能会认为当你执行增删改的时候默认加行锁,然后执行DDL语句的时候,比如alter table之类的语句,会默认在表级别加锁,这么说有一定道理,但也不太正确,因为确实在执行DDL的时候,会阻塞所有增删改操作,执行增删改的时候,会阻塞DDL操作。
但这是通过MySQL的元数据锁实现的,也就是Metadata Locks,但这还不是表锁的概念,因为表锁其实是InnoDB存储引擎的概念,InnoDB存储引擎提供了自己的表级锁,跟这里DDL语句用的元数据锁还不是一个概念。
【MySQL】十二、掌握MySQL锁的必备知识