MySQL快照读和当前读
在MySQL中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
这里我们首先给出快照读和当前读的例子:
快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析)
select * from table where id>10;
当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
select * from table where id>10 lock in share mode;
select * from table where id>10 for update;
insert into table values (…);
update table set id=11 where id=10;
delete 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的冲突检查,也会进行一个当前读。
2
关于死锁
死锁是指两个或者两个以上的事务在执行的过程中,因争夺资源而造成的一种互相等待的现象。若无外力作用,这两个事务将保持等待状态,无法推进下去。很明显,这是我们不想看到的。
从上面的概念可以看出,死锁的关键点在于互相等待,如果我们要解决死锁的问题,就要从“等待”这个关键词上面入手,如果我们将等待都转化为回滚操作,并且事务都重新开始,这种方法无疑可以避免死锁问题的产生。但是会导致数据库并发性能的降低,这样的问题也是我们无法接受的。
为了解决这一问题,我们采用一种超时的方法进行折中进行处理,超时是指当两个事务互相等待时,当某一方的等待时间超过一个阈值,我们将它进行回滚,这样,另一个事务就能够继续进行,在innodb存储引擎中,我们使用参数innodb_lock_wait_timeout来设置超时时间,这个参数如下:
mysql> show variables like "innodb_lock_wait_timeout";
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50 |
+--------------------------+-------+
1 row in set, 1 warning (0.11 sec)
为了加深印象,我们模拟一个死锁的现象,让大家感受一下。
首先,要模拟死锁,程序必须并发运行,串行的方法是无法模拟死锁的,这里我们采用两个连接会话进行模拟:
会话A
我们先开启事务,然后锁定id=3的行;
mysql> select * from t;
+----+-----+
| id | age |
+----+-----+
| 1 | 5 |
| 2 | 4 |
| 3 | 3 |
| 4 | 2 |
| 5 | 1 |
+----+-----+
5 rows in set (0.00 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t where id=3 for update;
+----+-----+
| id | age |
+----+-----+
| 3 | 3 |
+----+-----+
1 row in set (0.02 sec)
会话B
在会话B上锁定id=2的行
mysql> begin
-> ;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t where id=2 for update;
+----+-----+
| id | age |
+----+-----+
| 2 | 4 |
+----+-----+
1 row in set (0.00 sec)
会话A
我们在会话A上获取id=2的记录的锁,发现无法获取,产生了等待:
mysql> select * from t where id=2 for update;
##产生等待
mysql>
会话B
在会话A进行等待的过程中,我们在会话B上面获取id=3的记录的锁,我们发现了两个变化:
第一、会话B上输出了死锁的提示信息,如下;
mysql> select * from t where id=3 for update;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting traction
mysql>
第二、会话A上输出了id=2的记录,也就是A会话得到了特定的资源,但是产生了9s的延迟,如下;
会话A
mysql> select * from t where id=2 for update;
+----+-----+
| id | age |
+----+-----+
| 2 | 4 |
+----+-----+
1 row in set (9.04 sec)
mysql>
在上述操作中,会话B抛出了1213这个错误状态码,它代表事务发生了死锁,死锁的原因是会话A和B的资源进行了相互等待,但是此时我们发现会话B中抛出死锁提示信息之后会话A中立即得到了记录为2的这个资源,这其实是因为会话B中的事务发生了回滚,否则的话,会话A中的事务是不可能得到相应的资源的。
这里又不得不提innodb的一个特性,那就是它会回滚死锁情况下的一个事务,因此当我们在程序中捕获了一个1213的错误,其实不需要我们手动进行回滚。