MySQL InnoDB存储引擎有七种锁
- 自增锁(Auto-inc Locks)
- 共享/排他锁(Shared and Exclusive Locks)
- 意向锁(Intention Locks)
- 插入意向锁(Insert Intention Locks)
- 记录锁(Record Locks)
- 间隙锁(Gap Locks)
- 临键锁(Next-Key Locks)
1. 自增锁(Auto-inc Locks)
1.1 自增实现
在InnoDB存储引擎内存结构中,对每个含有自增值的表都有一个自增长计数器,当对其进行插入操作时,计数器会被初始化,执行如下语句得到计数器值:
SELECT MAX(auto_inc_col) FROM t FOR UPDATE;
可以看到该SQL语句后面加了排它锁(FOR UPDATE),为了提高插入性能,这个锁不是在事务完成后才释放,而是完成对自增长值插入的SQL语句后就立即释放。
上面的方式称为传统的AUTO-INC Locking方式,它其实采用的是一种特殊的表锁机制,所以对于大量的数据的并发插入还是会有性能问题,试想一个事务执行insert…select的大量数据插入,另外一个事务的插入会被阻塞。
1.2 innodb_autoinc_lock_mode参数
从MySQL5.1.22版本开始,InnoDB存储引擎提供了一个参数innodb_autoinc_lock_mode,默认值为1。讨论该值的意义前,先对自增插入进行分类:
- INSERT-like: 指所有插入语句,如:insert、replace、insert…select、replace…select、load data等。
- Simple inserts:指在出入前就确定插入行数的语句,如:insert、replace。
- Bulk inserts:指在出入前不能确定插入行数的语句,如insert…select、replace…select、load data。
- Mixed-mode inserts:指插入中有一部分的值时自增,有一部分是确定的。
了解上面四种插入类型后,现在讨论innodb_autoinc_lock_mode参数的值的情况:
- innodb_autoinc_lock_mode=0: 5.1.22版本之前自增的实现方式
- innodb_autoinc_lock_mode=1: 默认值。对于“Simple inserts”,会用互斥量去对内存中的计数器进行累加操作。对于“Bulk inserts”采用传统的AUTO-INC Locking方式。如果已经使用了AUTO-INC Locking方式,再进行“Simple inserts”操作,还是要等到AUTO-INC Locking释放。
- innodb_autoinc_lock_mode=2: 对于所有“INSERT-like”都是使用互斥量产生自增值。
PS: MyISAM引擎的自增长是表锁;InnoDB引擎下,自增列必须是索引,并且是索引第一列。
2. 共享/排他锁(Shared and Exclusive Locks)
共享锁(Share Locks,记为S锁): 读取数据时加S锁
排他锁(eXclusive Locks,记为X锁): 修改数据时加X锁
S X
S 兼容 互斥
X 互斥 互斥
即:
(1)多个事务可以拿到一把S锁,读读可以并行;
(2)而只有一个事务可以拿到X锁,写写/读写必须互斥;
InnoDB支持多粒度锁(multiple granularity locking),它允许行级锁与表级锁共存,实际应用中,InnoDB使用的是意向锁。
意向锁是指,未来的某个时刻,事务可能要加共享/排它锁了,先提前声明一个意向。
3. 意向锁(Intention Locks)
意向锁有这样一些特点:
(1)首先,意向锁,是一个表级别的锁(table-level locking);
(2)意向锁分为:
- 意向共享锁(intention shared lock, IS),它预示着,事务有意向对表中的某些行加共享S锁
- 意向排它锁(intention exclusive lock, IX),它预示着,事务有意向对表中的某些行加排它X锁
意向锁协议(intention locking protocol)并不复杂:
事务要获得某些行的S锁,必须先获得表的IS锁
事务要获得某些行的X锁,必须先获得表的IX锁
举个例子:
select ... lock in share mode,要设置IS锁;
select ... for update,要设置IX锁;
对已有数据行的修改与删除,必须加强互斥锁X锁,那对于数据的插入,使用插入意向锁。
插入意向锁,是间隙锁(Gap Locks)的一种,专门针对insert操作的。多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会阻塞彼此。
— 锁算法 —
5. 记录锁(Record Locks)
记录锁,它封锁索引记录,例如:
select * from t where id=1 for update;
它会在id=1的索引记录上加锁,以阻止其他事务插入,更新,删除id=1的这一行
需要说明的是:
select * from t where id=1;
则是快照读(SnapShot Read),它并不加锁。
6. 间隙锁(Gap Locks)
间隙锁,它封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。
select * from t where id between 8 and 15 for update;
会封锁区间[8,15],以阻止其他事务把id=10的记录插入。
间隙锁的主要目的,就是为了防止其他事务在间隔中插入数据,以导致“不可重复读”。如果把事务的隔离级别降级为读提交(Read Committed, RC)及以下,间隙锁则会自动失效。
7. 临键锁(Next-Key Locks)
临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。
更具体的,临键锁会封锁索引记录本身,以及索引记录之前的区间。
临键锁的主要目的,是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级Read Committed及以下,临键锁则也会失效。
PS:
- 对于外键列,InnoDB引擎会自动对其添加一个索引,这样可以避免死锁,而且不能手动删除。
- InnoDB引擎中,事务阻塞时,innodb_lock_wait_timeout字段可用来控制等待时间(默认50秒)。innodb_rollback_on_timeout用来设定是否在等地超时时对进行中的事务进行回滚(默认为OFF,代表不回滚)。需要注意的是,InnoDB不会回滚超时引发的异常。
- 死锁(1213错误)发生后,InnoDB会马上回滚一个事务。InnoDB不会回滚大部分的错误异常,死锁除外。
- InnoDB存储引擎不存在锁升级问题,因为1个锁开销与100000个锁是一样的,都没有开销。
参考:
《MySQL技术内幕:InnoDB存储引擎》