InnoDB并发控制

版权声明:转载请注明出处! https://blog.csdn.net/litianxiang_kaola/article/details/83003190

并发控制

并发的事务对同一行记录进行读写操作, 如果不采取措施, 可能会导致数据不一致, 所以必须进行并发控制(Concurrency Control).

InnoDB实现并发控制的方法是: 锁(Locking)和多版本并发控制(Multiversion Concurrency Control).

锁主要是共享锁和排他锁, 读操作时加共享锁, 写操作时加排他锁, 两者的兼容关系如下:

是否兼容 共享锁 排他锁
共享锁
排他锁

从中可以看到, 当一个事务对记录进行写操作时, 存储引擎会自动加个排他锁, 其他事务要想读取该记录就只能等待第一个事务写操作完成, 这对并发度有较大的影响, 那有没有可能写操作和读操作并发进行呢? 这就引出了多版本并发控制.

多版本并发控制(MVCC)

当写任务发生时, 将旧版本的数据copy一份作为新版本, 写任务操作新版本的数据, 并发的读任务操作旧版本的数据, 这样就可以实现读写任务并发的进行.

旧版本的数据存储undo日志中, undo日志储存在回滚段(rollback segment)中, 当事务提交时会清空undo日志.

InnoDB给表中的每行数据添加了三个属性:

  • DB_TRX_ID
    6字节, 最近一次修改该行的事务ID.
  • DB_ROLL_PTR
    7字节, 指向回滚段undo日志的指针.
  • DB_ROW_ID
    6字节, 单调递增的行ID. 用于构建聚集索引, 当不存在主键和唯一非空索引时.

undo日志的作用

  • 回滚未提交的事务.
  • 快照读(也可以说用于实现MVCC).
    undo日志里的旧版本数据也叫快照(snapshot), 所以从undo日志里读数据也叫快照读.

什么样的select是快照读?

普通的select语句都是快照读, 快照读是不加锁的, 例如:

select * from t where id>2;

非快照读是指显示加锁的select, 例如:

select * from t where id>2 lock in share mode;

select * from t where id>2 for update;

redo日志

提到undo日志就要说下redo日志, redo日志用来保证事务的持久性.

数据库事务提交后,并不会立即将更新后的数据刷到磁盘上, 而是先缓存在内存中(Buffer Pool), 再定期的刷到磁盘上. 如果这个时候数据库崩溃, 就会导致这部分修改后的数据丢失, 为了保证事务的持久性, 就引出了redo日志.

当用户开启一个数据库事务时, redo日志缓冲区(redo log buffer)会缓存修改后的数据, 定期的写入到磁盘的redo日志文件中, 并且保证在持久化数据文件前,之前的redo日志已经写到磁盘. 这样当数据库崩溃时, 如果内存(Buffer Pool)中的数据没有持久化到磁盘上, 只需重启数据库, 就能从redo日志文件中恢复数据.

InnoDB的七种锁

InnoDB共有七种类型的锁:

  • 共享/排他锁(Shared and Exclusive Locks)
  • 意向锁(Intention Locks)
  • 记录锁(Record Locks)
  • 间隙锁(Gap Locks)
  • 临键锁(Next-key Locks)
  • 插入意向锁(Insert Intention Locks)
  • 自增锁(Auto-inc Locks)

其中意向锁和自增锁是表锁, 其余都是行锁. 记录锁、间隙锁和临键锁是共享/排他锁的三种实现方式. 插入意向锁是间隙锁的一种. 下面是七种锁的详细介绍:

共享/排他锁

InnoDB实现了两种标准的行锁, 分别是共享锁和排他锁.

  • 事务拿到某一行记录的共享锁,才可以读取这一行.
  • 事务拿到某一行记录的排它锁,才可以修改或者删除这一行.

共享锁和排他锁的兼容关系如下:

是否兼容 共享锁 排他锁
共享锁
排他锁

意向锁

InnoDB支持多重粒度锁, 允许行锁和表锁共存. 意向锁是表锁, 包括意向共享锁(intention shared lock)和意向排他锁(intention exclusive lock).

意向锁仅仅表明意向, 并不会锁住数据. 当事务对一行记录加共享锁之前必须先添加意向共享锁, 同样, 在加排他锁之前要先添加意向排他锁. 他们的兼容关系如下:

是否兼容 共享锁 排他锁 意向共享锁 意向排他锁
共享锁 × ×
排他锁 × × × ×
意向共享锁 ×
意向排他锁 × ×

记录锁

记录锁封锁索引记录, 例如:

SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE

它会在c1=10的索引记录上加锁,以阻止其他事务插入, 更新, 删除这一行.

注意, 普通的查询是快照读, 不加锁:

SELECT c1 FROM t WHERE c1 = 10

间隙锁

间隙锁封锁索引记录之间的间隔, 或者第一条索引记录之前的范围, 或者最后一条索引记录之后的范围. 例如一个索引包含10, 20:

SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE

上面这条语句封锁了c1在10和20之间的这个范围, 插入一个c1=15的数据就会失败.

临键锁

临键锁是记录锁和间隙锁的组合, 临键锁会封锁索引记录和该索引记录之前的间隙. 例如有一个id索引包含10, 11, 13 和20, 临键锁可能封锁的区间如下:

(negative infinity, 10]	--封锁索引10时 
(10, 11]		--封锁索引11时
(11, 13]		--封锁索引13时
(13, 20]		--封锁索引20时
(20, positive infinity)		--封锁索引20时

当使用临建锁封锁20时, 因为20是最后一个索引值, 所以会封锁两个区间: (13, 20] 和 (20, positive infinity).

在REPEATABLE READ隔离级别下, InnoDB使用临键锁解决幻读的问题, 如下所示

SELECT * FROM t WHERE id > 12 FOR UPDATE;

上面的这条语句会锁定(11, 13], (13, 20], (20, positive infinity) 三个区间, 这样其他事务就不能在这个范围内插入行了.

插入意向锁

插入意向锁是间隙锁的一种, 是专门针对insert操作的. 在进行insert操作时, 会先在插入间隙加上插入意向锁, 然后对具体的插入行加上排他锁.

插入意向锁仅仅表示一种意向, 当多个事务在同一索引间隔内插入数据时, 如果插入的位置不冲突就不会阻塞彼此. 例如有一索引间隔[10,15]:

先执行事务A, 未提交:

insert into t values(11);

再执行事务B:

insert into t values(12);

因为两个事务插入的位置不冲突, 所以事务B并不会被阻塞.

自增锁

自增锁是表锁, 针对AUTO_INCREMENT类型的列. 例如主键ID是AUTO_INCREMENT类型的, 如果一个事务正在往表中插入记录, 其他所有事务的插入必须等待, 以便第一个事务插入的行拥有连续的主键值.

并发控制在SQL语句上的应用

在MySQL默认的隔离级别(REPEATABLE READ)下:

下面实例用到的表结构:

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `flag` int(11) DEFAULT NULL,
  `num` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `t_flag` (`flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

SELECT

(1)普通select

普通的select都是快照读, 不加锁, 如:

select * from t where id>2;

(2)加锁select

  • select … lock in share mode
    加共享锁. 在唯一索引(PRIMARY KEY和UNIQUE KEY) 上使用唯一的查询条件时, 加共享记录锁, 如:

    select * from t where id=1 lock in share mode;
    

    其他情况下加共享临键锁. 如:

    select * from t where id >1 lock in share mode;
    select * from t where flag =1 lock in share mode;
    
  • select … for update
    加排他锁. 在唯一索引(PRIMARY KEY和UNIQUE KEY) 上使用唯一的查询条件时, 加排他记录锁. 其他情况下加排他临键锁.

UPDATE/DELETE

和加排他锁的select类似, 在唯一索引(PRIMARY KEY和UNIQUE KEY) 上使用唯一的查询条件时, 加排他记录锁. 其他情况下加排他临键锁.

INSERT

在插入索引间隔内加插入意向锁, 对具体的插入行加排他记录锁. 例如有一索引间隔[10,15], 用户将要插入两行记录11和12. InnoDB首先在[10,15]的索引间隙内加插入意向锁, 然后再对11和12这两行记录加排他记录锁.

注意:

InnoDB存储引擎的行锁是通过索引实现的, 当语句没有用到索引时, InnoDB会放弃使用行锁而改用表锁. 

上面这句话其实是有点问题的, 当语句没有用到索引时, InnoDB并没有把行锁升级为表锁, 仍然使用的是行锁, 只是把表中的所有行都锁定了.

如下面这条UPDATE语句会给整张表的所有记录都锁定:

update t set num = 5 where num =3;

猜你喜欢

转载自blog.csdn.net/litianxiang_kaola/article/details/83003190