3分钟了解InnoDB锁机制与事务隔离

锁机制

共享排他锁

InnoDB实现了两种标准行级锁

  • 共享锁(S)
    持有S锁的事务可以读取行
  • 排他锁(X)
    持有X锁的事务可以修改或者删除行

共享锁与排他锁的兼容性:

S X
S 兼容 互斥
X 互斥 互斥

即:

  1. 多个事务可以同时获取S锁,读读可以并行
  2. 只有一个事务能够获取X锁,写写不能并行
  3. 一条记录要么是S锁要么是X锁,读写不能并行

意向锁

意向锁是表级锁,它表明一个事务需要在这个表的行上添加的锁类型。

意向锁有两种:

  • 意向共享锁(IS)
    表明一个事务有意向表中的行添加S锁,加锁方式SELECT …… LOCK IN SHARE MODE
  • 意向排他锁(IX)
    表明一个事务有意向表中的行添加X锁,加锁方式 SELECT …… FOR UPDATE

意向锁的协议:

  • 一个事务获取S锁之前,必须获取IS锁
  • 一个事务获取X锁之前,必须获取IX锁

意向锁与共享排他锁的兼容性:

S IS X IX
S 兼容 兼容 互斥 互斥
IS 兼容 兼容 互斥 兼容
X 互斥 互斥 互斥 互斥
IX 互斥 兼容 互斥 兼容

记录锁、间隙锁、临键锁

假设有一张表:

id name no
1 dxy1 10
2 dxy2 20
3 dxy3 30

PK:id
Key:name
Key:no

  • 记录锁
    记录锁是对记录行加锁,只对主键或者唯一索引生效,并且必须是精确匹配
    例如:
    事务A先执行,不提交:
    select * from t where id = 1 for update;
    事务B后执行:
    select * from t where id = 1 for update;
    如图,事务B阻塞,因为id=1的记录已经有了记录锁
  • 间隙锁
    间隙锁是一种开区间锁,用来解决插入的问题
    例如:
    事务A先执行,不提交:
    select * from t where id between 1 and 10 for update;
    事务B后执行:
    insert into t values(8,'dxy8',8);

如图:事务B阻塞,因为区间(1,10)已经加锁


  • 临键锁
    临键锁是由记录锁以及索引记录之前的间隙组成
    例如:
    事务A先执行,不提交:
    select * from t where no = 20 for update;
    事务B后执行:
    insert into t values(19,'dxy19',19);
    如图:事务B被阻塞,因为区间(10,20]已经加锁

需要注意的是

  1. 如果no是唯一索引,那么事务A支持有id=20的记录锁
  2. 如果no是普通索引,那么会持有(10,20]的临键锁和(20,30)的间隙锁

插入意向锁

插入意向锁是一种特殊的间隙锁,由插入操作设置
多个事务插入记录,如果在同一区间内插入位置不冲突,那么不会相互阻塞

例如:

id name no
1 dxy1 10
2 dxy2 20
3 dxy3 30

事务A先执行,不提交:
insert into t values(11,'dxy11',11);
事务B后执行:
insert into t values(12,'dxy12',12);
事务B不会被阻塞

但是如果事务B执行:
insert into t values(11,'dxy11',11);
如图:事务B依然会被阻塞,因为事务A占有记录锁

自增锁

自增锁是一种特殊的表级锁,是由向包含自增列的表中插入数据的事务获取的。
注意:自增锁的生命周期就是一条语句,而不是事务
例如:
事务A先执行,不提交:
insert into t(name,no) values('dxy11',11),('dxy12',12);
事务B后执行,不提交:
insert into t(name,no) values('dxy13',13);
事务A后执行,不提交:
insert into t(name,no) values('dxy14',14);

如图:事务A两条语句插入的记录,id是不连续的

可以通过innodb_autoinc_lock_mode设置自增锁模式:

  1. innodb_autoinc_lock_mode=0(传统锁模型)
  2. innodb_autoinc_lock_mode=1(连续锁模型)
  3. innodb_autoinc_lock_mode=2(交叉锁模型)
    更多关于自增锁模式的描述,参考官网文档

事务隔离性

我们知道事务有ACID四大特性,其中I就是代表的隔离性(isotation)
什么是事务的隔离性?
隔离性是指多个事务并发访问同一个表数据时,一个事务不会受到另一个事务对数据操作的影响。

并发事务之间的干扰存在哪些问题?
假设表t有数据:

id name no
1 dxy1 10
  • 场景1

事务A先执行,不提交:
insert into t values(4,'dxy11',11);

事务B后执行,不提交:
select * from t;

id name no
1 dxy1 10
4 dxy11 11

最后事务A回滚

此时,事务B就读取并处理了一条不存在的数据,这种情况就是脏读

  • 场景2

事务A先执行,不提交:
select * from t where id >= 1;

id name no
1 dxy1 10

事务B后执行,并提交:
update t set name = 'wuming' where id = 1;

事务A再次执行:
select * from t where id >= 1;

id name no
1 wuming 10

此时,事务A先后两次查询的记录数不一致,这种情况就是不可重复读

  • 场景3

事务A先执行,不提交:
select * from t where id = 1;

id name no
1 dxy1 10

事务B后执行,并提交:
insert into t values(4,'dxy11',11);

事务A再次执行:
select * from t where id = 1;

id name no
1 dxy1 10
4 dxy11 11

此时,事务A先后两次查询的记录数不一致,这种情况就是幻读

因此,并发事务可能导致

  1. 脏读:一个事务读取了另一个事务未提交的数据
  2. 不可重复读:一个事务读取了另一个事务修改并提交的数据
  3. 幻读:一个事务读取了另一个事务增加或删除并提交的数据

为了平衡并发事务的性能和数据一致性、可靠性、再现性,InnoDB支持了SQL:1992表准中全部四种隔离级别

  1. 读未提交(Read Uncommitted)
    Select语句不加锁,事务可能读取其他事务未提交的数据,即脏读。因此,这种级别的并发性最好,一致性最差

  2. 读已提交(Read Committed)

    • 对于非加锁读,每次都读取最新的快照
    • 对于加锁读、更新、删除,只对记录加锁而不对间隙加锁,即这种级别不支持间隙锁,因此会导致幻读

    因此,RC的隔离级别会出现不可重复读和幻读

  3. 可重复读(Repeatable Read)(默认级别)

    • 对于非加锁读,同一个事务只在第一次查询时生成快照。
    • 对于加锁读、更新、删除存在两种情况
      1. 唯一索引精确匹配:只对数据加记录锁
      2. 范围查询或非唯一索引:使用间隙锁和临键锁

    因此,RR级别下可以完全避免幻影行和不可重复读

  4. 序列化(Serializable)
    最严格的隔离级别,所有select全部转换成 select …… lock in share mode。这就导致,如果其他事务修改记录,那么select会被阻塞。

全表扫描在RC和RR级别下的加锁策略是怎样的呢?
假设有一张表t:

id name no
1 dxy1 10
2 dxy2 20
  • RR级别
    事务A先执行,不提交:
    select * from t where no = 10 for update;
    事务B后执行:
    select * from t where no = 20 for update
    如图:事务B被阻塞,因为RR级别下会对所有扫描的数据加锁
  • RC级别
    事务A先执行,不提交:
    select * from t where no = 10 for update;
    事务B后执行:
    select * from t where no = 20 for update
    如图:事务B不会被阻塞,因为只会对no=10的行加锁
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容