前言
MySQL中不同的存储引擎支持不同的锁机制。比如
- MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);
- BDB 存储引擎采用的是页面锁(page-level locking),但也支持表级锁;
- InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。
从对数据操作的类型来看,可以分为:
- 读锁(共享锁):事务T对数据对象A加上读锁,则事务T只可以读A不能修改,其他事务也只能对数据A加读锁。
- 写锁(排它锁):事务T对数据对象A加上写锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。
一、表级锁、行级锁、页面锁的区别
- 表级锁:偏向MyISM存储引擎,开销小,加锁块;无死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
- 行级锁:偏向InnoDB存储引擎,开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率最低,并发度最高。 InnoDB与MyISAM最大不不同有两点,一是支持事务, 二 是采用了行级锁
- 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用; 而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。
二、MyISAM表锁
2.1 查看表锁争用情况
- Table_locks_immediate 表示立即释放表锁数
- Table_locks_waited 表示需要等待的表锁数
如果waited值比较大,说明存在严重的表锁争用情况。
2.2 如何加表锁
MyISAM 在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT 等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,如果需要显式加锁,必须同时取得所有涉及到表的锁。例如:
Lock tables 表1 read local, 表2 read local; Select sum(total) from 表1; Select sum(total) from 表2; Unlock tables;
2.3 MyISAM锁调度
MyISAM 存储引擎的读锁和写锁是互斥的,读写操作是串行的。那么,一个进程请求某个 MyISAM 表的读锁,同时另一个进程也请求同一表的写锁,MySQL 如何处理呢?
答案是写进程先获得锁。不仅如此,即使读请求先到锁等待队列,写请求后到,写锁也会插 到读锁请求之前!这是因为 MySQL 认为写请求一般比读请求要重要。这也正是 MyISAM 表 不太适合于有大量更新操作和查询操作应用的原因,因为,大量的更新操作会造成查询操作很难获得读锁,从而可能永远阻塞。
三、InnoDB锁问题
InnoDB默认使用的是行锁:行锁开销大,加锁慢,会出现死锁,锁定粒度最小,发生锁冲突的概率最低,并发度最高。
可以通过检查 InnoDB_row_lock 状态变量来分析系统上的行锁的争夺情况
除了读锁与写锁之外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
- 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。
- 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。
示例SQL准备:
CREATE TABLE demo_innodb_lock( id INT(11), name VARCHAR(16), sex VARCHAR(1) )ENGINE = INNODB DEFAULT CHARSET = utf8 INSERT INTO demo_innodb_lock VALUES (1,'100','1'); INSERT INTO demo_innodb_lock VALUES (1,'200','0'); INSERT INTO demo_innodb_lock VALUES (2,'90','0'); INSERT INTO demo_innodb_lock VALUES (3,'400','1'); INSERT INTO demo_innodb_lock VALUES (4,'300','0'); INSERT INTO demo_innodb_lock VALUES (5,'500','1'); INSERT INTO demo_innodb_lock VALUES (6,'600','0'); INSERT INTO demo_innodb_lock VALUES (7,'700','1'); INSERT INTO demo_innodb_lock VALUES (8,'800','0'); CREATE INDEX idx_id ON demo_innodb_lock(id); CREATE INDEX idx_name ON demo_innodb_lock(name);
把自动提交关闭
SET autocommit = 0;
示例1:对于UPDATE、DELETE、INSERT语句,InnoDB会自动给涉及数据集加排他锁(X)
此时由于事务没有提交,再开一个窗口对同一行进行更新操作,会发现处于阻塞状态
当第一个窗口中的事务执行commit;后,释放排它锁,如果第二个窗口中锁等待没有超时,则会立即执行
示例2:行锁升级为表锁
在示例准备中我们给id和name创建了索引,但是此时索引失效了,执行更新操作后不提交
在第二个窗口执行更新并提交
此时在第一个窗口中查询发现,id=4的数据并没有发生改变,问题是操作的不是同一行数据,说明此时已经升级为表锁了,原因就是索引失效。
提交窗口一中的事务,释放表锁,再次查询
3.1 InnoDB行锁的实现方式
- InnoDB行锁是通过给索引上的索引项加锁来实现的,InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。
- 由于 MySQL 的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
- 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB 都会使用行锁来对数据加锁。
- 即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。
3.2 间隙锁(Next-Key 锁)
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙
(GAP)”,InnoDB 也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key 锁)。
示例:
此时去执行这样一条更新语句时,由于数据库中没有id=2的数据,那么就意味着存在间隙。
窗口1中事务没提交的情况下,在窗口2插入一条ID=2的数据,发现插入不进去,证明了是存在间隙锁的;提交窗口1中的事务后,窗口2中SQL执行成功。
InnoDB 使用间隙锁的目的,一方面是为了防止幻读,以满足相关隔离级别的要求,另外一方面,是为了满足其恢复和复制的需要。