阻塞
因为不同锁之间的兼容性关系,所以在有些时刻,一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源。在InnoDB存储引擎的源代码中,用Mutex数据结构来实现锁。在访问资源前需要用mutex_enter函数进行申请,在资源访问或修改完毕后立即执行mutex_exit函数。当一个资源已被一个事务占有时,另一个事务执行mutex_enter函数会发生等待,这就是阻塞。阻塞并不是一件坏事,阻塞是为了保证事务可以并发并且正常运行。
在InnoDB存储引擎中,参数innodb_lock_wait_timeout用来控制等待的时间(默认是50秒),innodb_rollback_on_timeout用来设定是否在等待超时时对进行中的事务进行回滚操作(默认是OFF,代表不回滚)。
参数innodb_lock_wait_timeout是动态的,可以在MySQL数据库运行时进行调整,而innodb_rollback_on_timeout是静态的,不可在启动时进行修改,如:
set @@innodb_lock_wait_timeout=60;
set @@innodb_rollback_on_timeout=on;
ERROR 1238(HY000):Variable'innodb_rollback_on_timeout'is a read only variable
当发生超时时,数据库会抛出一个1205的错误,如:
begin;
select * from t where a=1 for update;
ERROR 1205(HY000):Lock wait timeout exceeded;try restarting transaction
需要牢记的是,默认情况下InnoDB存储引擎不会回滚超时引发的错误异常。其实InnoDB存储引擎在大部分情况下都不会对异常进行回滚。
如在一个会话中执行了如下语句:
#会话A
select * from t;
begin;
select * from t where a<4 for update;
会话A中开启了一个事务,Next-Key Lock算法下锁定了小于4的所有记录(其实也锁定了4这个记录)。在另一个会话中执行如下语句:
#会话B
begin;
insert into t select 5;
insert into t select 3;
ERROR 1205(HY000):Lock wait timeout exceeded;try restarting transaction
select * from t;
可以看到,在会话B中插入记录5是可以的,但是插入记录3的话,因为Next-Key Lock算法的关系,需要等待会话A中事务释放这个资源,因此等待后产生了超时。但是在超时后,我们再进行SELECT会发现,5这个记录并没有并回滚。其实这时事务发生了错误,但是既没有commit,也没有rollback,这是十分危险的,用户必须判断是需要commit还是需要rollback,然后再进行下一步操作。
死锁
如果程序是串行的,那么不可能发生死锁。死锁只发生于并发的情况,数据库就是一个并发进行着的程序,因此可能会发生死锁。InnoDB存储引擎有一个后台的锁监控线程,该线程负责查看可能的死锁问题,并自动告知用户。
下面的操作演示了死锁的一种经典的情况,即A等待B,B在等待A:
(for update加的是排他锁)在上述操作中,会话中的事务抛出了1213这个出错提示,即发生了死锁。死锁的原因是会话A和B的资源互相在等待。大多数的死锁InnoDB存储自己可以侦测到,不需要人为进行干预。
但是在上面的例子中,会话B中的事务抛出死锁异常后,会话A中马上得到了记录为2的这个资源,这其实是因为会话B中的事务发生了回滚,否则会话A中的事务是不可能得到该资源的。InnoDB存储引擎并不会回滚大部分的错误异常,但是死锁除外。发现死锁后,InnoDB存储引擎会马上回滚一个事务,这点是需要注意的。如果在应用程序中捕获了1213这个错误,其实并不需要对其进行回滚。
Oracle数据库中产生死锁的常见原因是没有对外键添加索引,而InnoDB存储引擎会自动对其进行添加,因此很好地避免了这种情况的发生。人为删除外键上的索引数据库会抛出一个异常:
create table p(a int,primary key(a));
create table c(b int,foreign key(b) references p(a))engine=innodb;
show index from c\G;
drop index b on c;
ERROR 1553(HY000):Cannot drop index 'b':needed in a foreign key constraint
可以看到,虽然在建立子表时指定了外键,但是InnoDB存储引擎还是自动在外键列上建立了一个索引b,而人为删除这个列却是不允许的。
锁升级
锁升级(Lock Escalation)是指将当前锁的粒度降低。举例来说,数据库可以把一个表的1 000个行锁升级为一个页锁,或者将页锁升级为表锁。如果数据库的设计中认为锁是一种稀有资源,而且想避免锁的开销,那数据库中会频繁出现锁升级现象。
Microsoft SQL Server数据库的设计认为锁是一种稀有的资源,在适合的时候会自动地将行、键或者分页级锁升级为更粗粒度的表级锁。这种升级保护了系统资源,防止系统使用太多的内存来维护锁,从一定程度上提高了效率。
即使在Microsoft SQL Server 2005的版本之后,SQL Server数据库支持了行锁,但是其设计和InnoDB存储引擎完全不同,在以下情况下依然可能发生锁升级:
- 由一句单独的SQL语句在一个对象上持有的锁数量超过了阈值,默认的这个阈值为5000。值得注意的是,如果是不同对象的话,则不会发生锁升级。
- 锁资源占用的内存超过了激活内存的40%时,就会发生锁升级。
在Microsoft SQL Server数据库中,因为锁是一种稀有的资源,因此锁升级会带来一定的效率提高。但是锁升级带来的一个问题却是,因为锁粒度的降低而导致并发性能的降低。
InnoDB存储引擎不存在锁升级的问题。在InnoDB存储引擎中,1个锁的开销与1 000 000个锁是一样的,都没有开销。这一点和Oracle数据库比较类似。