mysql常用的锁_一文看懂Mysql中的常用锁

Mysql中的锁

锁机制是用来解决资源争用的常用手段。对某个粒度的资源加锁,访问资源资源需要先得到锁。

Mysql的锁按锁定粒度分为三类:全局锁(整个数据库实例加锁),表级锁(对表加锁)和行锁(对数据行加锁,引擎层实现)

全局锁

对整个数据库实例加锁,Flush tables with read lock (FTWRL) 对全局加读锁,使得全局可读。

释放全局锁命令如下:unlock tables;此外,在客户端异常断开后,全局锁会自动释放。

应用场景主要是逻辑库数据备份(mysqldump)

表级锁

mysql表级别的锁分为表锁 、元数据锁(meta data lock MDL)、意向锁(InnoDB,后面补充讲解)。

表锁

显式表锁可以锁住表。实际应用场景需要对整个数据表加锁的情况并不多见,没有行锁可用时的方案。

按照锁定的目的分为读锁和写锁。读锁用于读取数据,写锁用于更改数据。

加锁语法lock tables … read/write

解锁语法 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。

当innodb引擎支持更细致的行锁时,很少使用表锁

元数据锁(MDL)

MDL作为另一种表锁,不需要手动加,而是mysql自动加。目的是为了保证数据读写的正确性。场景比如,select一个表的同时有一个删除表字段语句执行,如果允许并发那么查询出的数据应该有没有这个表字段呢?答案是不能允许执行。这就是MDL的设计目的。

对一个表做增删改查时,加MDL读锁;对表做结构变更操作时,加MDL写锁。

读锁之间不互斥,也就是可以多个线程同时对数据进行增删改查

读锁与写锁,写锁与写锁之间是互斥的,也就是说当数据结构变更时,是无法执行增删改查或者其他数据结构变更的

行锁

行锁是在引擎层实现的,innodb支持,myisam不支持

行锁,就是对数据记录的行加锁。事务A更新了一行,事务B要更新这行时则需要等待A执行完成.

共享锁与排他锁

行锁同样分为共享锁(shared lock 简称s锁)和排他锁(exclusive lock简称x锁)

共享锁用于读取时加锁,可以共享,也就是事务T1加了读锁,T2也可以加读锁

排他锁用于更新数据是加锁,是排他的,也就是事务T1加了读锁,则T2必须等待释放后才能获取锁

mysql默认会加锁,不需要显式指定。显示指定的语法为:

# 共享锁(S)

SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE

# 排他锁(X)

SELECT * FROM table_name WHERE ..id=1 FOR UPDATE

# 排他锁解锁

UPDATE ...set... where id=1

注意:

1.行锁必须有索引才能实现,否则会自动锁全表,那么就不是行锁了。

2.两个事务不能锁同一个索引

3.insert ,delete , update在事务中都会自动默认加上排它锁。

两阶段锁协议

在innodb中,行锁是在需要时加的,但是要在事务结束后才释放,这就是两阶段锁协议。相对应的,如果单阶段协议,则在事务一开始时申请所有的锁。

举个例子,如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。

从顾客 A 账户余额中扣除电影票价;

给影院 B 的账户余额增加这张电影票价;

记录一条交易日志。

也就是说,要完成这个交易,我们需要 update 两条记录,并 insert 一条记录。

当然,为了保证交易的原子性,我们要把这三个操作放在一个事务中。那么,你会怎样安排这三个语句在事务中的顺序呢?试想如果同时有另外一个顾客 C 要在影院 B 买票,那么这两个事务冲突的部分就是语句 2 了。因为它们要更新同一个影院账户的余额,需要修改同一行数据。

根据两阶段锁协议,不论你怎样安排语句顺序,所有的操作需要的行锁都是在事务提交的时候才释放的。所以,如果你把语句 2 安排在最后,比如按照 3、1、2 这样的顺序,那么影院账户余额这一行的锁时间就最少。这就最大程度地减少了事务之间的锁等待,提升了并发度。

死锁

上述方案,但是仍然不能避免死锁,当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。这里我用数据库中的行锁举个例子。

T1 T2

从A账户扣钱 给B账户加钱

给B账户加钱 从A账户扣钱

这个事例就是死锁。解决方式是

一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。默认配置式50s,这个时间对于在线业务也是非常长的。但也不能设置的过小,因为误伤就大了。

show global variables like 'innodb_lock_wait_timeout'; # 50

另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。但是死锁检测的性能消耗不能小看。每次加锁时,需要对所有依赖的线程作检测,看是否有锁住资源,然后再来判断是否为死锁。

show global variables like 'innodb_deadlock_detect'; # on

当有大量进程同时更新一行(热点账户)时,死锁检测会造成大量的问题,死锁检测会消耗大量的CPU,怎么解决呢?一种是关掉死锁检测;另一种是控制并发度,这需要在中间层或者在数据库源码层完成;还有就是将逻辑的一行换成多行。将并发压力就降低了,这种也是热点账户问题的常用解决方案。

下面我们来造一个死锁并观察处理结果。

创建movie表的ddl如下,注意一定是innodb引擎

DROP TABLE IF EXISTS `movie`;

CREATE TABLE `movie` (

`movie_id` int(11) NOT NULL AUTO_INCREMENT,

`movie_name` varchar(256) NOT NULL,

`score` int(11) NOT NULL,

PRIMARY KEY (`movie_id`)

) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;

造死锁语句:

1) 开启一个客户端(Navicat的一个查询)

# 客户端1

# 打开事务,必须要有这句,否则事务自动提交就没有效果了

begin;

# 更新id为1的记录,根据行锁规则,修改时自动锁定该记录

update movie set movie_name='测试1' where movie_id=1;

# 睡眠30秒,此时持有id=1记录的锁,睡醒后即将请求id=2的锁

select sleep(30) from dual;

# 请求id=2的锁

update movie set movie_name='测试2' where movie_id=2;

打开另一个客户端

# 客户端2

begin;

# 持有id=2的锁

update movie set movie_name='测试222' where movie_id=2;

select sleep(30) from dual;

# 请求id=1的锁

update movie set movie_name='测试111' where movie_id=1;

这两个语句块同时执行,睡眠结束后会产生死锁,触发innodb的死锁检测,客户端2会报错:“SQL错误1213:Deadlock found when trying to get lock;try restarting transaction”

意向锁-表级与行级协调

表锁和行级锁可能存在冲突情况,比如事务T1申请一个行级排他锁,那么事务T2申请一个表级写锁会如何呢?如果会成功则违反了行级排他锁,其他事务无法修改该行的规定;

T2加锁可以先循环所有行判断是否有行锁,然后再决定是否加锁成功,但是这样效率极低

为何协调表级锁和行级锁,InnoDB有了意向锁的概念,注意意向锁是表级锁。

当行级锁加了共享锁,则自动加上意向共享锁(IS);当行级锁加上排他锁则自动加上意向排他锁(IX)。

意向锁的作用是协调表级锁与行级锁。意向锁之间是兼容的,但是与其他表锁不兼容

共享锁(S)

排他锁(x)

意向共享锁(IS)

意向排他锁(IX)

共享锁(S)

兼容

冲突

兼容

冲突

排他锁(x)

冲突

冲突

冲突

冲突

意向共享锁(IS)

兼容

冲突

兼容

兼容

意向排他锁(IX)

冲突

冲突

兼容

兼容

总结

本文讲述了mysql的锁分类,下面总结下提纲

全局锁 FTWRL 对整个数据库实例加锁

表级锁

表锁 lock table read write / unlock

元数据锁 MDL

读锁:数据增删改查时加,不与其他冲突

写锁 ( 与其他锁排斥): 数据结构变更时加写锁

意向锁,当加行锁时会加意向锁,联动的方式与表锁协调

行锁

共享锁:读取数据时加行锁

排他锁: 数据库数据增删改时默认加排他锁。可以通过 select .. for update方式显示加锁

你可能感兴趣的:(mysql常用的锁)