锁机制用于管理对共享资源的并发访问。
在处理并发读或写时,可以通过实现一个由两种类型的锁组成的锁系统来解决问题。这两种类型的锁通常被称为共享锁和排他锁,也叫读锁和写锁。
读锁是共享的,或者说是想不阻塞的。写锁则是排他的,也就是说一个写锁会阻塞其他写锁和读锁。
锁粒度:
一种提高共享资源并发性的方式就是让锁定对象更具有选择性。尽量只锁定需要修改的部分,而不是所有的资源。任何时候,在给定资源上,锁定的数量越少,则系统的并发程度越高。
MyISAM和MEMORY存储引擎采用的是表级锁(table-levellocking);BDB存储引擎采用的是页面锁(page-levellocking),但也支持表级锁;InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。
MySQL这3种锁的特性可大致归纳如下。
开销、加锁速度、死锁、粒度、并发性能
l 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
l 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
l 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。锁模式的兼容性如表所示。
请求锁模式是否兼容当前锁模式 |
None |
读锁 |
写锁 |
读锁 |
是 |
是 |
否 |
写锁 |
是 |
否 |
否 |
可见,对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;对 MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;MyISAM表的读操作与写操作之间,以及写操作之间是串行的!
具体语法如下:
LOCK TABLES
tbl_name[AS alias] {READ [LOCAL] | [LOW_PRIORITY] WRITE}
[, tbl_name[AS alias] {READ [LOCAL] | [LOW_PRIORITY] WRITE}] ...
UNLOCK TABLES
当您使用LOCK TABLES时,您必须锁定您打算在查询中使用的所有的表。虽然使用LOCK TABLES语句获得的锁定仍然有效,但是您不能访问没有被此语句锁定的任何的表。同时,您不能在一次查询中多次使用一个已锁定的表——使用别名代替,在此情况下,您必须分别获得对每个别名的锁定。
mysql> LOCK TABLE t WRITE, t AS t1 WRITE;
mysql> INSERT INTO t SELECT * FROM t;
ERROR 1100: Table 't' was notlocked with LOCK TABLES
mysql> INSERT INTO t SELECT * FROM t AS t1;
如果您的查询使用一个别名引用一个表,那么您必须使用同样的别名锁定该表。如果没有指定别名,则不会锁定该表。
mysql> LOCK TABLE t READ;
mysql> SELECT * FROM t AS myalias;
ERROR 1100: Table 'myalias' wasnot locked with LOCK TABLES
相反的,如果您使用一个别名锁定一个表,您必须使用该别名在您的查询中引用该表。
mysql> LOCK TABLE t AS myalias READ;
mysql> SELECT * FROM t;
ERROR 1100: Table 't' was not lockedwith LOCK TABLES
mysql> SELECT * FROM t AS myalias;
注意:在使用表锁时,commit和rollback释放不了表锁。必须使用unlock table。
InnoDB实现了以下两种类型的行锁。
l 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
l 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
l 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
l 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
请求锁模式是否兼容当前锁模式 |
X |
IX |
S |
IS |
X |
冲突 |
冲突 |
冲突 |
冲突 |
IX |
冲突 |
兼容 |
冲突 |
兼容 |
S |
冲突 |
冲突 |
兼容 |
兼容 |
IS |
冲突 |
兼容 |
兼容 |
兼容 |
如果一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。
意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁;事务可以通过以下语句显示给记录集加共享锁或排他锁。
¡ 共享锁(S):SELECT *FROM table_name WHERE ... LOCK IN SHARE MODE。
¡ 排他锁(X):SELECT *FROM table_name WHERE ... FOR UPDATE。
用SELECT ... IN SHARE MODE获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作。但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用SELECT... FOR UPDATE方式获得排他锁。
InnoDB行锁是通过给索引上的索引项加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
举个例子:
我们创建如下表,没有任何索引
mysql> desc t3;
+-------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+---------+------+-----+---------+-------+
| id | int(11) | YES | | NULL | |
| sale | int(11) | YES | | NULL | |
+-------+---------+------+-----+---------+-------+
2 rows in set (0.00 sec)
A会话 |
B会话 |
|
mysql> begin; Query OK, 0 rows affected (0.00 sec) |
|
|
|
|
|
|
|
|
注意:
由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
举个例子:
mysql> desc t;
+-------+---------+------+-----+---------+-------+
| Field |Type | Null | Key | Default | Extra |
+-------+---------+------+-----+---------+-------+
| t | int(11) | YES | MUL | NULL | |
+-------+---------+------+-----+---------+-------+
1 row in set (0.00sec)
A会话 |
B会话 |
|
|
|
|
|
|
|
|
|
|
死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。
一种经典情况:A等待B,B在等待A,这种死锁问题称为AB-BA死锁。
A会话 |
B会话 |
|
|
|
|
|
|
|
|
|
|
另一中常见的锁死情况,即当前事务持有了带插入记录的下一个记录的X锁,但是在等待队列中存在一个S锁的请求,则可能会发生锁死。
例如:
A对话 |
B对话 |
|
|
|
|
|
|
|
|
因为B对话对t=0行加了共享锁,并且A对话插入t=0数据与B对话产生阻塞,再加上B对话需要对t=2获得共享锁因为A对话拥有t=2的排他锁所以也需要等待。所以产生了死锁。
通过锁定机制可以实现事物的隔离性要求,使得事务可以并发地工作。锁提高了并发,但是却带来潜在的问题。问题如下:
l 脏读
所谓的脏读是指事务对缓冲池中行记录的修改,并且还没有提交。
举个例子:
会话A |
会话B |
|
|
|
|
|
|
|
|
我们发现在不同的时间段,在事务两次次select的结果都不相同。其中两次记录是在B未提交的数据,即产生了脏读,违反了事物的隔离性。
l 不可重复读
不可重复读是指在一个事务内多次读取同一个数据集合。在一个事务还没有结束时,另外一个事务也访问该同一数据集合,并做了DML操作。因此,在第一个事务中的两次数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。
不可重复读与脏读的区别是:
脏读是读到未提交的数据,而不可重复读到的却是已提交的数据,但是其违反了数据库事务的一致性的要求。
举例子,
A会话 |
B会话 |
|
|
|
|
|
|
|
|
|
|
l 丢失更新
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题--最后的更新覆盖了由其他事务所做的更新。
l 幻读
一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
mysql> create table t2 (id intprimary key);
A会话 |
B会话 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
四种隔离级别对这些问题的解决情况:
隔离级别 |
脏读 |
不可重复读 |
幻读 |
Read uncommitted |
未解决 |
未解决 |
未解决 |
Read committed |
解决 |
未解决 |
未解决 |
Repeatable read |
解决 |
解决 |
未解决 |
Serializable |
解决 |
解决 |
解决 |