mysql锁
一、Mysql锁介绍
1. 定义
锁是计算机协调多个进程或线程并发访问某一资源的机制。 在数据库中,除传统的 计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种共享资源,数据库需要保持其高并发下的数据一致性,有效性,所以需要一种锁机制。
mysql中不同的存储引擎支持不同的锁机制,MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。
2. 分类
表级锁
-
特点:开销小,加锁快,不会出现死锁,锁粒度大,发生锁冲突的概率高,并发度低。
-
适用:更适合以查询为主,适合低并发的更新。
行级锁
-
优点:开销大,加锁慢,可能会出现死锁,锁粒度小,发生锁冲突的概率高,并发度高。
-
适用:更适合以更新为主,适合高并发下,按照索引并发更新少量不同数据。
3. 存储引擎
MyISAM | InnoDB | |
---|---|---|
索引类型 | 非聚族索引 | 聚族索引 |
支持事物 | 否 | 是 |
支持表锁 | 是 | 是 |
支持行锁 | 否 | 是 |
支持外键 | 否 | 是 |
支持全文索引 | 是 | 是:5.7以上支持 |
适合操作类型 | 大量查询 | 大量新增,更新,删除等 |
二、MyISAM
1. 表锁
-
表读锁(Table Read Lock)
-
表写锁(Table Write Lock)
MyISAM在执行查询语句之前,会自动给涉及的所有表加读锁,在执行更新操作前,会自动给涉及的表加写锁,(锁是自动加,自动释放的)这个过程并不需要用户干预,因此用户一般不需要使用命令来显式加锁。
对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;对 MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;MyISAM表的读操作与写操作之间,以及写操作之间是串行的!
2. 语法
-
加锁
# 'mylock' 表名 lock table '表名' write; lock table '表名' read; lock table 'mylock' write;
-
解锁
unlock tables;
-
读并发插入
# 'mylock' 表名 lock table '表名' read local;只有读可以加local lock table 'mylock' read local;
3. 代码
建表语句:
CREATE TABLE `mylock` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`NAME` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `mylock` (`id`, `NAME`) VALUES ('1', 'a');
INSERT INTO `mylock` (`id`, `NAME`) VALUES ('2', 'b');
INSERT INTO `mylock` (`id`, `NAME`) VALUES ('3', 'c');
INSERT INTO `mylock` (`id`, `NAME`) VALUES ('4', 'd');
说明: 例子中显示加锁只是为了演示效果,默认crud,MyISAM会自动加锁。下面的例子同时开启了两个session来模拟多线程并发访问,你自己也可以在cmd开启多个窗口来模拟。
-
写锁阻塞读锁例子
session1 session2 1. 获取表的write锁定
lock table mylock write;2. 当前session对表的查询,插入,更新操作都可以执行
select * from mylock;
insert into mylock values(5,‘e’);
update mylock set name = ‘edg’ where id = 5;当前session对表的查询会被阻塞
select * from mylock;3. 当前session不能查询没有锁定的表
select * from person
Table ‘person’ was not locked with LOCK TABLES当前session可以查询或者更新未锁定的表
select * from mylock
insert into person values(1,‘zhangsan’);4. 释放锁:
unlock tables;当前session能够立刻执行,并返回对应结果 解析:session1获得写锁后,session2,想要获取写锁和读锁都必须等待session1的锁退出。
-
读锁阻塞写锁例子
session1 session2 1. 获得表的read锁定
lock table mylock read;2. 当前session可以查询该表记录:
select * from mylock;当前session可以查询该表记录:
select * from mylock;3. 当前session不能查询没有锁定的表
select * from person
Table ‘person’ was not locked with LOCK TABLES当前session可以查询或者更新未锁定的表
select * from mylock
insert into person values(1,‘zhangsan’);4. 当前session插入或者更新表会提示错误
insert into mylock values(6,‘f’)
Table ‘mylock’ was locked with a READ lock and can’t be updated
update mylock set name=‘aa’ where id = 1;
Table ‘mylock’ was locked with a READ lock and can’t be updated当前session插入数据会等待获得锁
insert into mylock values(6,‘f’);5. 释放锁
unlock tables;获得锁,更新成功 解析:session1获得读锁后,session2,可以也获得读锁(查询结束后马上自动释放),因为读锁是共享的,但是获取不了写锁了,因为写锁是独占的。同时session1获得锁后也不能去操作其他未加锁的表了,如普通表person,也不能对加锁的表进行,更新,删除操作了,相当于mysql不但会校验锁争用,还会检验获取锁线程后的行为,是不是满足锁的定义范围。
-
读的时候的并发插入例子
session1 session2 1 获取表的read local锁定
lock table mylock read local2 当前session不能对表进行更新或者插入操作
insert into mylock values(6,‘f’)
Table ‘mylock’ was locked with a READ lock and can’t be updated
update mylock set name=‘aa’ where id = 1;
Table ‘mylock’ was locked with a READ lock and can’t be updated其他session可以查询该表的记录
select* from mylock3. 当前session不能查询没有锁定的表
select * from person
Table ‘person’ was not locked with LOCK TABLES其他session可以进行插入操作,但是更新会阻塞
update mylock set name = ‘aa’ where id = 1;4. 当前session不能访问其他session插入的记录; 5. 释放锁资源:unlock tables 当前session获取锁,更新操作完成 6. 当前session可以查看其他session插入的记录
解析:MyISAM表的读和写是串行的,这是就总体而言的,在一定条件下,MyISAM也支持查询和插入操作的并发执行。
session1获得本地读锁后,session2,也可以获得读锁(查询结束后马上自动释放), 因为读锁是共享的,可以获取写锁了,但是只能用来插入新纪录,mysql会检验其行为。
三、InnoDB
1. 事物
1.ACID
事务是由一组SQL语句组成的逻辑处理单元,事务具有4属性,通常称为事务的ACID属性:
-
原子性(Actomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。一条sql默认就是原子操作(事务自动开始,自动提交)
-
隔离性(Isolation):事务间并发执行,事务间可能互相会受到影响。可以类比线程, mysql提供了一定的隔离机制来消解这种影响。
-
持久性(Durable):事务提交后,对数据的修改就是永久性的。
-
一致性 (Consistent): 事务开始和结束时,数据都必须保持一致状态。
2.事务并发问题
相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持更多用户的并发操作,但与此同时,会带来以下问题:
-
脏读:一个事务读到了另一个事务未提交的数据。一个事务在对数据做修改,还未提交,数据处于不一致的状态,另一个事务也读到了这个数据,有可能就是旧数据,并对这个数据进一步的处理,造成了脏读。
-
不可重复读:一个事务在读取某些数据已经发生了改变、或某些记录已经被删除了!这种现象叫做“不可重复读”。
-
幻读:一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”
上述出现的问题都是数据库读一致性的问题,可以通过事务的隔离机制来进行保证。
数据库的事务隔离越严格,并发副作用就越小,但付出的代价也就越大,因为事务隔离本质上就是使事务在一定程度上串行化,需要根据具体的业务需求来决定使用哪种隔离级别
3.隔离级别
脏读 | 不可重复读 | 幻读 | |
---|---|---|---|
read uncommitted | √ | √ | √ |
read committed | √ | √ | |
repeatable read | √ | ||
serializable |
2.锁机制
-
读锁(共享锁(s)): 又称读锁。允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
-
写锁(排它锁(x)): 又称写锁。允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。
mysql InnoDB存储引擎默认:update,delete,insert都会自动给涉及到的数据行加上排他锁,select语句默认不会加任何锁类型,显示加锁只能给select语句加锁。所以加过排他锁的数据行在其他事务中是不能修改数据的,也不能通过for update和lock in share mode锁的方式再去获取锁,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。
1. 语法
-
手动加锁
# 排他锁(写锁) select * from table 'tab_no_index' for update; # 共享锁(读锁) select * from table 'tab_no_index' lock in share mode;
-
解锁
事务提交完自动释放
2. 表锁
创建表
create table tab_no_index(id int,name varchar(10)) engine=innodb;
insert into tab_no_index values(1,'1'),(2,'23,'3'),(4,'4');
- mysql是自动提交事务的,这里为了方便演示,关闭自动提交。在不通过索引条件查询的时候,innodb使用的是表锁而不是行锁。
session1 | session2 | |
---|---|---|
1. | 禁用自动提交,这样事务没有提交之前,锁就没有释放 set autocommit=0 select * from tab_no_index where id = 1; | set autocommit=0 select * from tab_no_index where id =2 |
2. | id没有加索引所以这里走的表锁 select * from tab_no_index where id = 1 for update | |
3. | 处于阻塞状态 select * from tab_no_index where id = 2 for update; |
3. 行锁
创建表
create table tab_with_index(id int,name varchar(10)) engine=innodb;
alter table tab_with_index add index id(id);
insert into tab_with_index values(1,'1'),(2,'2'),(3,'3'),(4,'4');
2、创建带索引的表进行条件查询,innodb使用的是行锁
session1 | session2 | |
---|---|---|
1. | set autocommit=0 select * from tab_with_indexwhere id = 1; | set autocommit=0 select * from tab_with_indexwhere id =2 |
2. | 加锁 select * from tab_with_index where id = 1 for update | |
3. | select * from tab_with_indexwhere id = 2 for update; | |
4. | commit; | |
5. | 1 row in set (…) |
3、由于mysql的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键值,是会出现冲突的。
insert into tab_with_index values(1,'4');
session1 | session2 | |
---|---|---|
1. | set autocommit=0 | set autocommit=0 |
2. | select * from tab_with_index where id = 1 and name=‘1’ for update | |
3. | select * from tab_with_index where id = 1and name=‘4’ for update 虽然session2访问的是和session1不同的记录,但是因为使用了相同的索引,所以需要等待锁 |
四、总结
对于MyISAM的表锁,主要讨论了以下几点:
(1)共享读锁(S)之间是兼容的,但共享读锁(S)与排他写锁(X)之间,以及排他写锁(X)之间是互斥的,也就是说读和写是串行的。
(2)在一定条件下,MyISAM允许查询和插入并发执行,我们可以利用这一点来解决应用中对同一表查询和插入的锁争用问题。
(3)MyISAM默认的锁调度机制是写优先,这并不一定适合所有应用,用户可以通过设置LOW_PRIORITY_UPDATES参数,或在INSERT、UPDATE、DELETE语句中指定LOW_PRIORITY选项来调节读写锁的争用。
(4)由于表锁的锁定粒度大,读写之间又是串行的,因此,如果更新操作较多,MyISAM表可能会出现严重的锁等待,可以考虑采用InnoDB表来减少锁冲突。
- 对于InnoDB表,本文主要讨论了以下几项内容:
(1)InnoDB的行锁是基于索引实现的,如果不通过索引访问数据,InnoDB会使用表锁。
(2)在不同的隔离级别下,InnoDB的锁机制和一致性读策略不同。
在了解InnoDB锁特性后,用户可以通过设计和SQL调整等措施减少锁冲突和死锁,包括:
- 尽量使用较低的隔离级别; 精心设计索引,并尽量使用索引访问数据,使加锁更精确,从而减少锁冲突的机会;
- 选择合理的事务大小,小事务发生锁冲突的几率也更小;
- 给记录集显式加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁;
- 不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会;
- 尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响; 不要申请超过实际需要的锁级别;除非必须,查询时不要显示加锁;
- 对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能。