当我们对数据库中的表进行select、update、delete以及insert的时候,innodb存储引擎会根据操作类型的不同来给这些操作添加具体的锁。

在MySQL中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。

这里我们首先给出快照读和当前读的例子:

快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析)

1select * from table where id>10;

当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。

1select * from table where id>10 lock in share mode;2select * from table where id>10 for update;3insert into table values (…);4update table set id=11 where id=10;5delete from table where id>10;读取之后,需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句明确指出了lock in share mode之外,也就是对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。

这里我们给出一个update操作过程中,mysql server和innodb存储引擎进行交互的过程如下:

从上图中,我们可以看出一个update操作的具体流程。当update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁 (current read)。待MySQL Server收到这条加锁的记录之后,会再发起一个update请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,update操作内部,就包含了一个当前读。同理,delete操作也一样。insert操作会稍微有些不同,简单来说,就是insert操作可能会触发Unique Key的冲突检查,也会进行一个当前读。

死锁是指两个或者两个以上的事务在执行的过程中,因争夺资源而造成的一种互相等待的现象。若无外力作用,这两个事务将保持等待状态,无法推进下去。很明显,这是我们不想看到的。

从上面的概念可以看出,死锁的关键点在于互相等待,如果我们要解决死锁的问题,就要从“等待”这个关键词上面入手,如果我们将等待都转化为回滚操作,并且事务都重新开始,这种方法无疑可以避免死锁问题的产生。但是会导致数据库并发性能的降低,这样的问题也是我们无法接受的。

为了解决这一问题,我们采用一种超时的方法进行折中进行处理,超时是指当两个事务互相等待时,当某一方的等待时间超过一个阈值,我们将它进行回滚,这样,另一个事务就能够继续进行,在innodb存储引擎中,我们使用参数innodb_lock_wait_timeout来设置超时时间,这个参数如下:

1mysql> show variables like "innodb_lock_wait_timeout";2+--------------------------+-------+3| Variable_name            | Value |4+--------------------------+-------+5| innodb_lock_wait_timeout | 50    |6+--------------------------+-------+71 row in set, 1 warning (0.11 sec)    为了加深印象,我们模拟一个死锁的现象,让大家感受一下。

首先,要模拟死锁,程序必须并发运行,串行的方法是无法模拟死锁的,这里我们采用两个连接会话进行模拟:

我们先开启事务,然后锁定id=3的行;

1mysql> select * from t; 2+----+-----+ 3| id | age | 4+----+-----+ 5|  1 |   5 | 6|  2 |   4 | 7|  3 |   3 | 8|  4 |   2 | 9|  5 |   1 |10+----+-----+115 rows in set (0.00 sec)1213mysql> begin;14Query OK, 0 rows affected (0.00 sec)1516mysql> select * from t where id=3 for update;17+----+-----+18| id | age |19+----+-----+20|  3 |   3 |21+----+-----+221 row in set (0.02 sec)

在会话B上锁定id=2的行

1mysql> begin 2    -> ; 3Query OK, 0 rows affected (0.00 sec) 4mysql> select * from t where id=2 for update; 5+----+-----+ 6| id | age | 7+----+-----+ 8|  2 |   4 | 9+----+-----+101 row in set (0.00 sec)会话A

我们在会话A上获取id=2的记录的锁,发现无法获取,产生了等待:

1mysql> select * from t where id=2 for update;2##产生等待34mysql>会话B

在会话A进行等待的过程中,我们在会话B上面获取id=3的记录的锁,我们发现了两个变化:

第一、会话B上输出了死锁的提示信息,如下;

1mysql> select * from t where id=3 for update;2ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting traction34mysql>

第二、会话A上输出了id=2的记录,也就是A会话得到了特定的资源,但是产生了9s的延迟,如下;

1mysql> select * from t where id=2 for update;2+----+-----+3| id | age |4+----+-----+5|  2 |   4 |6+----+-----+71 row in set (9.04 sec)89mysql>   在上述操作中,会话B抛出了1213这个错误状态码,它代表事务发生了死锁,死锁的原因是会话A和B的资源进行了相互等待,但是此时我们发现会话B中抛出死锁提示信息之后会话A中立即得到了记录为2的这个资源,这其实是因为会话B中的事务发生了回滚,否则的话,会话A中的事务是不可能得到相应的资源的。

这里又不得不提innodb的一个特性,那就是它会回滚死锁情况下的一个事务,因此当我们在程序中捕获了一个1213的错误,其实不需要我们手动进行回滚。