根据加锁范围,MySQL数据库锁可以分为:全局锁、表级锁、行级锁
一 全局锁
锁的是数据库实例,加锁方式为Flush tables with read lock(FTWRL),
这个命令会使整个库处于只读状态,使用该命令后,数据更新语句、数据定义语句等操作都会被阻塞。
使用场景:全库逻辑备份。
风险:
1.不能在主库执行(会导致业务停摆)
2.在从库执行会导致有从延迟(不能执行主库同步的biglog)
建议:使用官方自带的逻辑备份工具mysqldump,当mysqldump使用参数--single-transaction的时候会启动一个事务,确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的。一致性读是好,但是前提是引擎要支持。
扩展:如果要全库只读,为什么不使用set global readonly=true的方式?
1.在有些系统中,readonly的值会被用来做其他逻辑,比如判断主备库。所以修改global变量的方式影响太大。
2.在异常处理机制上有差异。如果执行FTWRL命令之后由于客户端发生异常断开,那么MySQL会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为readonly之后,如果客户端发生异常,则数据库就会一直保持readonly状态,这样会导致整个库长时间处于不可写状态,风险较高。
二 表级锁
MySQL里面表级锁有两种,一种是表锁,一种是元数据所(meta data lock,MDL)
表锁的语法是:lock tables ... read/write
可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放。lock tables语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。
对于InnoDB这种支持行锁的引擎,一般不使用lock tables命令来控制并发,毕竟锁住整个表的影响面还是太大。
MDL:不需要显式使用,在访问一个表的时候会被自动加上。MDL 会直到事务提交才会释放,在做表结构变更的时候,一定要小心不要导致锁住线上查询和更新。
MDL的作用:保证读写的正确性, 防止DDL和DML并发的冲突。
在对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。
读锁之间不互斥。读写锁之间,写锁之间是互斥的,用来保证变更表结构操作的安全性。
Online DDL的过程是这样的:
1. 拿MDL写锁
2. 降级成MDL读锁
3. 真正做DDL
4. 升级成MDL写锁
5. 释放MDL锁
三 行级锁
在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
GAP锁
间隙锁实质上是对索引前后的间隙上锁,不对索引本身上锁。
根据检索条件向左寻找最靠近检索条件的记录值A,作为左区间,向右寻找最靠近检索条件的记录值B作为右区间,即锁定的间隙为(A,B)。
间隙锁的目的是为了防止幻读,其主要通过两个方面实现这个目的:
(1)防止间隙内有新数据被插入。
(2)防止已存在的数据,更新成间隙内的数
死锁处理:
事务 A 在等待事务 B 释放 id=2 的行锁,而事务 B 在等待事务 A 释放 id=1 的行锁。 事务 A 和事务 B 在互相等待对方的资源释放,就是进入了死锁状态。当出现死锁以后,有两种策略:
一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。默认值超时时间是50S,对于在线服务来说这个等待时间是无法接受的,但超时时间设置的太短时会出现很多误伤。
另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on(默认就是开),表示开启这个逻辑。一般是用这种方式。
每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,因此高并发更行同一行数据时,死锁检测要消耗大量的CPU资源。
解决方案:
1.关闭死锁检测,存在风险,因为业务一般不会把死锁当做一个严重错误,毕竟出现死锁就回滚,然后通过业务重试一般就没问题了,这是业务无损的。而关掉死锁检测意味着可能会出现大量的超时,这是业务有损的。
2.控制并发度,比如同一行同时最多只有10个线程在更新,在客户端做并发控制。
3.从设计上优化,将更新一行的逻辑改成逻辑上的多行来减少锁冲突。