0%

mysql的锁和事务机制

1. 非锁定和锁定一致性读

1.1 非锁定一致性读(普通select,mvcc)

就是普通的 SELECT 语句(没有加锁后缀), innodb 不会加锁, 就算别人锁了你也可以读。

InnoDB存储引擎通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB存储引擎会去读取行的一个快照数据。

1

非一致性读,因为不需要等待访问的行上X锁的释放。快照数据是指该行的之前版本的数据,该实现是通过undo段来完成。此外,读取快照数据是不需要上锁的,极大地提高了数据库的并发性。

快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。由此带来的并发控制,称之为多版本并发控制(Multi Version Concurrency Control,MVCC)。

1.2 RC 和 RR 非一致性读区别

在事务隔离级别READ COMMITTED和REPEATABLE READ(InnoDB存储引擎的默认事务隔离级别)下,InnoDB存储引擎使用非锁定的一致性读。然而,对于快照数据的定义却不相同

在READ COMMITTED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。

  • session A
1
2
3
SELECT*FROM parent WHERE id=1;

1
  • session B
1
UPDATE parent SET id=3 WHERE id=1;

在会话 B 中将事务表 parent 中 id 为 1 的记录修改为 id=3,但是事务同样没有提交,这样 id=1 的行其实加了一个 X 锁。

回到之前的会话 A,接着上次未提交的事务,执行 SQL 语句 SELECT*FROM parent WHERE id=1 的操作,这时不管使用 READ COMMITTED 还是 REPEATABLE READ 的事务隔离级别,显示的数据应该都是 1。

  • session A
1
2
3
SELECT*FROM parent WHERE id=1;

1
  • session B
1
commit

在会话 B 提交事务后,这时在会话 A 中再运行 SELECT*FROM parent WHERE id=1 的 SQL 语句。
对于 READ COMMITTED 的事务隔离级别,它总是读取行的最新版本。(其他事务修改后的值,读到的是NULL,因而被修改了)
对于 REPEATABLE READ 的事务隔离级别,总是读取事务开始时的行数据。(当前事务最初的值,读到的还是1)

1.3 锁定一致性读(select后面加锁语法查询)

即使是对于SELECT的只读操作。InnoDB存储引擎对于SELECT语句支持两种一致性的锁定读(locking read)操作:

  • SELECT…FOR UPDATE

  • SELECT…LOCK IN SHARE MODE

SELECT…FOR UPDATE 对读取的行记录加一个 X 锁,其他事务不能对已锁定的行加上任何锁。SELECT…LOCK IN SHARE MODE 对读取的行记录加一个 S 锁,其他事务可以向被锁定的行加 S 锁,但是如果加 X 锁,则会被阻塞。

SELECT…FOR UPDATE,SELECT…LOCK IN SHARE MODE必须在一个事务中,当事务提交了,锁也就释放了。因此在使用上述两句SELECT锁定语句时,务必加上BEGIN,START TRANSACTION或者SET AUTOCOMMIT=0。

2. Mysql的锁

2.1 不同数据库的锁实现

对于MyISAM引擎,其锁是表锁设计。并发情况下的读没有问题,但是并发插入时的性能就要差一些了。

Microsoft SQL Server开始支持乐观并发和悲观并发,在乐观并发下开始支持行级锁,但是其实现方式与InnoDB存储引擎的实现方式完全不同。用户会发现在Microsoft SQL Server下,锁是一种稀有的资源,锁越多开销就越大,因此它会有锁升级。在这种情况下,行锁会升级到表锁,这时并发的性能又回到了以前。

2.2 行锁

InnoDB 存储引擎实现了如下两种标准的行级锁:

  • 共享锁(S Lock),允许事务读一行数据。( select…lock in share mode)

  • 排他锁(X Lock),允许事务删除或更新一行数据。( select…for update)

如果一个事务T1已经获得了行r的共享锁,那么另外的事务T2可以立即获得行r的共享锁,因为读取并没有改变行r的数据,称这种情况为锁兼容(Lock Compatible)。但若有其他的事务T3想获得行r的排他锁,则其必须等待事务T1、T2释放行r上的共享锁——这种情况称为锁不兼容。

X 锁与任何的锁都不兼容,而 S 锁仅和 S 锁兼容。需要特别注意的是,S 和 X 锁都是行锁,兼容是指对同一记录(row)锁的兼容性情况。

2.3 意向锁(和其他锁可共存)

InnoDB存储引擎支持多粒度(granular)锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,称之为意向锁(Intention Lock)。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度(fine granularity)上进行加锁。

InnoDB存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁:

1)意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁

2)意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁

1

若将上锁的对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。

例如图6-3,如果需要对页上的记录r进行上X锁,那么分别需要对数据库A、表、页上意向锁IX,最后对记录r上X锁。若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的完成。举例来说,在对记录r加X锁之前,已经有事务对表1进行了S表锁,那么表1上已存在S锁,之后事务需要对记录r在表1上加上IX,由于不兼容,所以该事务需要等待表锁操作的完成。

2.4 行锁和意向锁的兼容

类别使用
共享锁 (S 锁)行锁(读锁)select * from table where id = 1 lock in share mode;
排它锁 (X 锁)行锁(写锁)select * from table where id = 1 for update;
意向共享锁 (IS 锁)表锁数据引擎自己维护,用户无法手动操作
意向排他锁 (IX 锁)表锁数据引擎自己维护,用户无法手动操作
  • 兼容性
ISIX
IS兼容兼容
IX兼容兼容
SX
IS兼容互斥
IX互斥互斥

2.5 锁升级

InnoDB存储引擎不存在锁升级的问题。因为其不是根据每个记录来产生行锁的,相反,其根据每个事务访问的每个页对锁进行管理的,采用的是位图的方式。因此不管一个事务锁住页中一个记录还是多个记录,其开销通常都是一致的。

假设一张表有300 0000个数据页,每个页大约有100条记录,那么总共有3 0000 0000条记录。若有一个事务执行全表更新的SQL语句,则需要对所有记录加X锁。若根据每行记录产生锁对象进行加锁,并且每个锁占用10字节,则仅对锁管理就需要差不多需要3GB的内存。

而InnoDB存储引擎根据页进行加锁,并采用位图方式,假设每个页存储的锁信息占用30个字节,则锁对象仅需90MB的内存。由此可见两者对于锁资源开销的差距之大。

3.行锁算法

3.1 行锁的 3 种算法

InnoDB 存储引擎有 3 种行锁的算法,其分别是:

  • Record Lock:单个行记录上的锁。

  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。

  • Next-Key Lock∶ Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。

Record Lock 总是会去锁住索引记录,如果 InnoDB 存储引擎表在建立的时候没有设置任何一个索引,那么这时 InnoDB 存储引擎会使用隐式的主键来进行锁定。

在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。例如一个索引有10,11,13和20这四个值,那么该索引可能被Next-Key Locking的区间为:

1
2
3
4
5
(-∞,10]
(10,11]
(11,13]
(13,20]
(20,+∞)

当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围。

行锁举例

1
2
3
4
5
6
7
CREATE TABLE t(a INT PRIMARY KEY);
INSERT INTO t SELECT 1;
INSERT INTO t SELECT 2;
INSERT INTO t SELECT 5;

SELECT * FROM t WHERE a = 5 FOR UPDATE;
INSERT INTOt SELECT 4; # 可以插入

表t共有1、2、5三个值。在上面的例子中,在会话A中首先对a=5进行X锁定。而由于a是主键且唯一,因此锁定的仅是5这个值,这样在会话B中插入值4而不会阻塞,可以立即插入并返回。即锁定由Next-Key Lock算法降级为了Record Lock,从而提高应用的并发性。

Next-Key Lock降级为Record Lock仅在查询的列是唯一索引的情况下。若是辅助索引,则情况会完全不同。

1
2
3
4
5
6
7
8
9
10
CREATE TABLE z(a INT,b INT,PRIMARY KEY(a),KEY(b));
INSERT INTO z SELECT 1,1;
INSERT INTO z SELECT 3,1;
INSERT INTO z SELECT 5,3;
INSERT INTO z SELECT 7,6;
INSERT INTO z SELECT 10,8;

(-无穷,1](1,3](3,6](6,8](8,+无穷)

SELECT*FROM z WHERE b=3 FOR UPDATE;

由于有两个索引,其需要分别进行锁定。

对于聚集索引,其仅对列a等于5的索引加上Record Lock。而对于辅助索引,其加上的是Next-Key Lock,锁定的范围是(1,3),特别需要注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上gap lock,即还有一个辅助索引范围为(3,6)的锁。因此,若在新会话B中运行下面的SQL语句,都会被阻塞:

1
2
3
SELECT*FROM z WHERE a=5 LOCK IN SHARE MODE; # 已经对聚集索引中列a=5的值加上X锁,因此执行会被阻塞。
INSERT INTO z SELECT 4,2; # 但是插入的辅助索引值2在锁定的范围(1,3)中,因此执行同样会被阻塞。
INSERT INTO z SELECT 6,5; # 插入的值5在另一个锁定的范围(3,6)中,故同样需要等待。

而下面的SQL语句,不会被阻塞,可以立即执行:

1
2
3
INSERT INTO z SELECT 8,6;
INSERT INTO z SELECT 2,0;
INSERT INTO z SELECT 6,7;

间隙锁

在InnoDB存储引擎中,对于INSERT的操作,其会检查插入记录的下一条记录是否被锁定,若已经被锁定,则不允许查询。对于上面的例子,会话A已经锁定了表z中b=3的记录,即已经锁定了(1,3)的范围,这时若在其他会话中进行如下的插入同样会导致阻塞:

1
2
3
4
5
SELECT*FROM z WHERE b=3 FOR UPDATE;

INSERT INTO z SELECT 2,2; # 阻塞

INSERT INTO z SELECT 2,0; # 可以执行

对于唯一键值的锁定,Next-Key Lock降级为Record Lock仅存在于查询所有的唯一索引列。若唯一索引由多个列组成,而查询仅是查找多个唯一索引列中的其中一个,那么查询其实是range类型查询,而不是point类型查询,故InnoDB存储引擎依然使用Next-Key Lock进行锁定。

3.2 RR 用 Next-Key Locking 解决幻读

即 REPEATABLE READ 下,InnoDB 存储引擎采用 Next-Key Locking 机制来避免 Phantom Problem(幻像问题)。这点可能不同于与其他的数据库,如 Oracle 数据库,因为其可能需要在 SERIALIZABLE 的事务隔离级别下才能解决 Phantom Problem。

表 t 由 1、2、5 这三个值组成,对于 SQL 语句 SELECT*FROM t WHERE a>2 FOR UPDATE,其锁住的不是 5 这单个值,而是对(2,+∞)这个范围加了 X 锁。因此任何对于这个范围的插入都是不被允许的,从而避免 Phantom Problem。

InnoDB存储引擎默认的事务隔离级别是REPEATABLE READ,在该隔离级别下,其采用Next-Key Locking的方式来加锁。而在事务隔离级别READ COMMITTED下,其仅采用Record Lock。

4. 事务问题

4.1 脏读

脏读指的就是在不同的事务下,当前事务可以读到另外事务未提交的数据,简单来说就是可以读到脏数据。违反了事务的隔离性。

脏读现象在生产环境中并不常发生,从上面的例子中就可以发现,脏读发生的条件是需要事务的隔离级别为READ UNCOMMITTED,而目前绝大部分的数据库都至少设置成READ COMMITTED。

4.2 不可重复读(包扩幻读)

不可重复读是指在一个事务内多次读取同一数据集合,两次读到的数据是不一样的情况,这种情况称为不可重复读。其违反了数据库事务一致性的要求。

不可重复读和脏读的区别是:脏读是读到未提交的数据,而不可重复读读到的却是已经提交的数据(提交是指别的事务 commit)。

在 InnoDB 存储引擎中,通过使用 Next-Key Lock 算法来避免不可重复读的问题。在 MySQL 官方文档中将不可重复读的问题定义为 Phantom Problem,即幻像问题。

在 Next-Key Lock 算法下,对于索引的扫描,不仅是锁住扫描到的索引,而且还锁住这些索引覆盖的范围(gap)。因此在这个范围内的插入都是不允许的。这样就避免了另外的事务在这个范围内插入数据导致的不可重复读的问题。

因此,InnoDB存储引擎的默认事务隔离级别是READ REPEATABLE,采用Next-Key Lock算法,避免了不可重复读的现象。

4.3 丢失更新(业务问题)

脏读,幻读,不可重复读是读的问题,丢失更新是写的问题。

mysql丢失更新问题英文叫lost update。指的是两个事务同时更新一条数据,后更新的覆盖了前面更新的结果,从结果上看第一次的更新丢失了的现象。

其实现在数据库本身并没有丢失更新的问题,因为当一个事务更新一条记录时,就会加排他锁,另外一个的更新就会阻塞住。所以丢失更新大多是业务本身的问题。(查出来数据,算后再改)

  • 悲观锁解决
1
2
3
4
5
6
begin;
// 所有的会话都并发执行这个SQL,只有一个可以获取 X 锁成功,其他的都等待
select id, name, balance from account where id = 1 for update;
// 假设返回:balance = 100,代码里 balance += 10; balance = 110
update account set balance = 110 where id = 1;
commit;
  • 调整更新策略解决
1
2
3
4
5
6
7
8
9
begin;
// 执行快照读
select id, balance from account where id = 1;
// 这个数据有可能会被其他的并发更新给修改掉,不一定是最新的数据

// 执行update操作,注意这里的不同,是直接对余额做 增/减 操作,
// 利用一致性读视图在update时执行当前读的特点,也就是说如果有多个会话执行 update,其余的会被阻塞
update account set balance = balance + 10 where id = 1;
commit;

5. Mysql的事务

InnoDB存储引擎中的事务完全符合ACID的特性。ACID是以下4个词的缩写:

  • 原子性(atomicity)

  • 一致性(consistency)

  • 隔离性(isolation) // 锁主要解决的问题

  • 持久性(durability)

5.1 事务分类

对于InnoDB存储引擎来说,其支持扁平事务、带有保存点的事务、链事务、分布式事务。

  • 扁平事务(Flat Transaction)是事务类型中最简单的一种,但在实际生产环境中,这可能是使用最为频繁的事务。在扁平事务中,所有操作都处于同一层次,其由BEGIN WORK开始,由COMMIT WORK或ROLLBACK WORK结束,其间的操作是原子的,要么都执行,要么都回滚。
  • 分布式事务(Distributed Transactions)通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。节点A不能通过调用一台数据库就完成任务。其需要访问网络中两个节点的数据库,而在每个节点的数据库执行的事务操作又都是扁平的。对于分布式事务,其同样需要满足ACID特性,要么都发生,要么都失效。

5.2 事务的实现

事务隔离性由锁来实现。原子性、一致性、持久性通过数据库的redo log和undo log来完成。redo log称为重做日志,用来保证事务的原子性和持久性。undo log用来保证事务的一致性。

有的DBA或许会认为undo是redo的逆过程,其实不然。redo和undo的作用都可以视为是一种恢复操作,redo恢复提交事务修改的页操作,而undo回滚行记录到某个特定版本。因此两者记录的内容不同,redo通常是物理日志,记录的是页的物理修改操作。undo是逻辑日志,根据每行记录进行记录。

5.3 事务隔离级别

类型备注
Read Uncommitted(RU)未提交读不加锁
Read Committed(RC)已提交读解决脏读
Repeatable Read(RR)可重复读解决脏读,不可重复读,innodb 还解决了幻读(因为间隙锁不让别人插入),默认级别
Serializable(SE) 串行化select隐式转为lock in share mode, 会和 update,delete 互斥 解决脏读, 不可重复读, 幻读

InnoDB存储引擎默认支持的隔离级别是REPEATABLE READ,但是与标准SQL不同的是,InnoDB存储引擎在REPEATABLE READ事务隔离级别下,使用Next-Key Lock锁的算法,因此避免幻读的产生。

这与其他数据库系统(如Microsoft SQL Server数据库)是不同的。所以说,InnoDB存储引擎在默认的REPEATABLE READ的事务隔离级别下已经能完全保证事务的隔离性要求,即达到SQL标准的SERIALIZABLE隔离级别。

隔离级别越低,事务请求的锁越少或保持锁的时间就越短。这也是为什么大多数数据库系统默认的事务隔离级别是READ COMMITTED。

5.4 MySQL数据库分布式事务

InnoDB存储引擎提供了对XA事务的支持,并通过XA事务来支持分布式事务的实现。在使用分布式事务时,InnoDB存储引擎的事务隔离级别必须设置为SERIALIZABLE。

XA事务由一个或多个资源管理器(Resource Managers)、一个事务管理器(Transaction Manager)以及一个应用程序(Application Program)组成。

  • 资源管理器:提供访问事务资源的方法。通常一个数据库就是一个资源管理器。
  • 事务管理器:协调参与全局事务中的各个事务。需要和参与全局事务的所有资源管理器进行通信。
  • 应用程序:定义事务的边界,指定全局事务中的操作。

在MySQL数据库的分布式事务中,资源管理器就是MySQL数据库,事务管理器为连接MySQL服务器的客户端。图7-22显示了一个分布式事务的模型。

1

分布式事务使用两段式提交(two-phase commit)的方式。

在第一阶段,所有参与全局事务的节点都开始准备(PREPARE),告诉事务管理器它们准备好提交了。在第二阶段,事务管理器告诉资源管理器执行ROLLBACK还是COMMIT。如果任何一个节点显示不能提交,则所有的节点都被告知需要回滚。可见与本地事务不同的是,分布式事务需要多一次的PREPARE操作,待收到所有节点的同意信息后,再进行COMMIT或是ROLLBACK操作。

Mysql内部XA事务

最为常见的内部XA事务存在于binlog与InnoDB存储引擎之间。二进制日志和重做日志必须同时写入。若二进制日志先写了,而在写入InnoDB存储引擎时发生了宕机,那么slave可能会接收到master传过去的二进制日志并执行,最终导致了主从不一致的情况。

当事务提交时,InnoDB存储引擎会先做一个PREPARE操作,将事务的xid写入,接着进行二进制日志的写入,如图7-24所示。如果在InnoDB存储引擎提交前,MySQL数据库宕机了,那么MySQL数据库在重启后会先检查准备的UXID事务是否已经提交,若没有,则在存储引擎层再进行一次提交操作。

1

6. log

6.1 redo log

1. redo log 和 undo log的区别

redo log用来保证事务的持久性,undo log用来帮助事务回滚及MVCC的功能。redo log基本上都是顺序写的,在数据库运行时不需要对redo log的文件进行读取操作。而undo log是需要进行随机读写的。

2. redo log 和 bin log的区别

MySQL数据库中还有一种二进制日志(binlog),其用来进行POINT-IN-TIME(PIT)的恢复及主从复制(Replication)环境的建立。从表面上看其和重做日志非常相似,都是记录了对于数据库操作的日志。然而,从本质上来看,两者有着非常大的不同。

首先,重做日志是在InnoDB存储引擎层产生,而二进制日志是在MySQL数据库的上层产生的,并且二进制日志不仅仅针对于InnoDB存储引擎,MySQL数据库中的任何存储引擎对于数据库的更改都会产生二进制日志。

其次,两种日志记录的内容形式不同。MySQL数据库上层的二进制日志是一种逻辑日志,其记录的是对应的SQL语句。而InnoDB存储引擎层面的重做日志是物理格式日志,其记录的是对于每个页的修改。

3. 恢复

InnoDB存储引擎在启动时不管上次数据库运行时是否正常关闭,都会尝试进行恢复操作。因为重做日志记录的是物理日志,因此恢复的速度比逻辑日志,如二进制日志,要快很多。

INSERT操作在二进制日志中就不是幂等的,重复执行可能会插入多条重复的记录。而上述INSERT操作的重做日志是幂等的。

6.2 undo log

用户通常对undo有这样的误解:undo用于将数据库物理地恢复到执行语句或事务之前的样子——但事实并非如此。

用户执行了一个INSERT 10W条记录的事务,这个事务会导致分配一个新的段,即表空间会增大。在用户执行ROLLBACK时,会将插入的事务进行回滚,但是表空间的大小并不会因此而收缩。因此,当InnoDB存储引擎回滚时,它实际上做的是与先前相反的工作。对于每个INSERT,InnoDB存储引擎会完成一个DELETE;对于每个DELETE,InnoDB存储引擎会执行一个INSERT;对于每个UPDATE,InnoDB存储引擎会执行一个相反的UPDATE,将修改前的行放回去。

除了回滚操作,undo的另一个作用是MVCC,即在InnoDB存储引擎中MVCC的实现是通过undo来完成。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo读取之前的行版本信息,以此实现非锁定读取。

最后也是最为重要的一点是,undo log会产生redo log,也就是undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久性的保护。

7. 头脑风暴

  • 行锁包括共享锁 (lock in share mode),排他锁(for update)
  • 意向锁,和其他锁可以共存。本身也是一个表锁。
  • 行锁三种算法:行记录锁, 间隙锁,next-key 锁(行 + 间)
  • 脏读是读到其他未提交的数据。不可重复度能读到其他事务提交的数据(RR 通过快照读创建一次解决)。

8. 参考资料

给作者打赏,可以加首页微信,咨询作者相关问题!