MySQL全局锁和表锁
根据加锁的范围,MySQL里面的锁大致可以分成全局锁、表级锁和行锁三类
全局锁
全局锁就是对整个数据库实例进行加锁。它的一个经典使用场景就是做全库的逻辑备份,也就是把整个库的数据都查出来存成文本。
加了全局锁之后,整个数据库系统就变为了只读状态,意味着很多业务不可以进行。但是如果不加,那么考虑以下情况:
现在有一个用户余额表以及用户库存表,假设用户购买商品时发起了逻辑备份,如果先备份余额表,后备份商品表,那么在扣除余额前余额表备份完成,添加商品到用户库存后才备份的库存表,这时如果用备份进行恢复,那么就会导致用户账户没有扣钱,但是却多了库存。如果反过来,则会导致用户账户被扣,而且没有商品。
而我们会发现,在真正导出数据时,是可以对数据库做修改。这是因为导出数据前,数据库开启了一个可重复读隔离级别的事务,保证了在事务执行期间读到的数据和事务开始时是一致的。
表级锁
MySQL的表级锁有两种,一种是表锁,一种是元数据锁(MDL),
表锁
可以使用lock tables … read/write给数据库加表锁,可以使用unlock tables主动释放锁,也可以在客户端断开时自动释放。
但是需要注意,加了表锁以后,其他线程无法访问这个表,而且加锁的线程接下来的操作也会受到限制。
比如线程A对执行了以下操作:
1 | lock tables t1 read, t2 write |
那么其他所有线程写t1,读写t2的语句都会被阻塞,同时在线程A释放锁之前,它只能对表t1进行读操作,t2进行读写操作,而且不允许操作其他表。
元数据锁(MDL)
MDL锁并不用显式使用,在访问一个表时会被自动追加。它的作用是保证读写的正确性。
假如一个线程在遍历一张表获取数据,此时有另外一个线程删除了表中的一列,这样肯定是不行的。所以在MySQL5.5版本引入了MDL锁。当对一个表做增删改查操作时,加MDL共享锁(读锁),当需要对表结构做调整时,加MDL(排他锁)写锁。
MDL读锁之间不互斥,所以加读锁可以允许有多个线程同时对一张表进行增删改查。
读锁与写锁之间以及写锁之间是互斥的,这就意味着如果有线程在进行正删改查时是不可以进行表结构调整,而调整时也不能进行增删改查。
正是由于上面的原因,考虑以下场景:
在一个事务中,线程A执行了查询语句,加了MDL读锁,线程B也执行查询操作,也加了MDL读锁,之后线程C试图修改表结构,需要MDL写锁,这时由于事务没有结束,线程A加的MDL读锁未释放,就会导致线程C被阻塞。但是后续申请MDL读锁的操作也会被线程C阻塞,这就导致了整个表的数据不可访问,严重时会导致数据库崩溃。
行锁
MySQL的行锁是由各个存储引擎实现的,有些引擎是不支持行锁的。
两阶段锁
考虑以下场景:
事务B会被阻塞,直到事务A提交。这就意味着,事务A持有了id = 1和id = 2的行锁,但是在修改完它并不会立即释放该锁,而是等到事务结束后才释放。这个就是两阶段锁协议。
根据这个协议,我们应该在事务中需要锁多个行时,要把最可能造成冲突的、最可能影响并发度的锁尽量往后放。
死锁和死锁检测
死锁,简单来说,就是线程A对资源C加了锁,线程B也对资源D加了锁,而线程A释放锁需要资源D,线程B释放锁需要资源C,就会导致死锁。
在MySQL中,考虑以下情况:
事务A对id = 1的行加了锁, 事务B对id = 2的行加了锁,然后事务A在修改 id = 2时会被阻塞,事务B修改id = 1时会被阻塞。但此时,他们两个会互相等待对方释放资源,导致死锁。
在MySQL中,可以设置超时时间,或者发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务。将innodb_deadlock_detect参数设置未on表示开启死锁检测。
但是死锁的检测是一个时间复杂度为O(N)的操作,即每一个被堵住的线程,都要去判断是不是因为自己的加入导致了死锁,判断的方法就是遍历之前的线程去做判断。
基本原理
InnoDB行锁是通过给索引上的索引项加锁来实现的。这个索引项指的是在B+树索引结构中,指向一个具体行记录的索引项。索引项是一个键值和一个指向数据页和行号的指针。
InnoDB的索引是建立在数据页上面,而每一个数据页里面有很多条记录,每条记录都有一个对应的索引项。行锁就是锁住了这个索引项,也就是对应的每一条记录。
这也就意味着,如果查询或者修改的条件字段没有索引,那么就不会加行锁,而是直接加表锁。
比如说下面语句:
1 | update table1 set name = 1 where age = 20 |
如果age字段没有索引,那么就会对table1整张表加锁。如果age有索引,那么就会加行锁。