Skip to content
MisakaTang's Blog
Go back

InnoDB官方文档--14.7.2 InnoDB事务模型|14.7.3 InnoDB中的不同SQL语句设置的锁

Edit page

14.7.2 InnoDB事务模型

在InnoDB事务模型里,其目标就是把多版本数据库的最佳属性和传统的两段锁相结合.InnoDB在行级进行加锁并且默认情况下将查询作为非锁定的一致性读取来运行,这是Oracle的风格.InnoDB中的锁信息是高效存储的所以不需要升级.通常允许多个用户锁定InnoDB表中的每一行,或行的任何随机子集而不会导致InnoDB内存耗尽.

14.7.2.1 事务隔离级别

事务隔离是数据库处理的基础之一.隔离(Isolation)是ACID中I的缩写;隔离级别是在多个事务同时进行更改和执行查询时对性能和可靠性,一致性和结果的可重复性进行平衡的细微调整的设置.

InnoDB提供SQL:1992标准描述的四种事务隔离级别: READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ SERIALIZABLE.InnoDB默认的事务隔离级别是 REPEATABLE READ.

用户可以改变单个会话(session)的隔离级别或使用SET TRANSACTION语句为连接后面所有的语句设置.为了对所有的连接设置服务器默认的隔离级别,在命令行使用 --transaction-isolation选项或在配置文件中.

InnoDB支持对每个事务隔离级别使用不同的锁策略.你可以对符合ACID规范很重要的关键数据的操作使用默认的REPEATABLE READ级别来强制执行高一致性.或者你可以使用 READ COMMITTED READ UNCOMMITTED放松一致性要求,在例如批量报告等情况下,精确的一致性和可重复结果不如最小化锁的开销那么重要. SERIALIZABLE REPEATABLE READ更严格,且主要用于特殊情况,例如XA事务以及并发和死锁的故障问题排除上.

下面列出了MySQL支持的不同事务隔离级别.排列顺序由最常使用到不经常使用:.

14.7.2.2 自动提交,提交和回滚

在InnoDB中,所有用户活动都发生在事务里.如果启用自动提交,每个SQL语句自己形成一个事务.默认情况下,MySQL为每个新的连接开启一个启用自动提交的会话,所以如果语句执行没有返回错误的话,MySQL会在每个SQL后执行一个提交(commit).如果语句返回错误,根据错误信息会进行提交或者回滚.

启用了自动提交的事务可以使用以 START TRANSACTION BEGIN开始,以COMMITROLLBACK结束的语句来显式执行多语句的事务.

如果在一个会话中SET autocommit = 0来禁用自动提交的话,会话会始终打开一个事务.COMMITROLLBACK语句结束当前的事务然后会打开一个新的.

如果禁用自动提交的事务没有显式的提交来结束事务的话,MySQL会回滚改事务.

某些语句会隐式的结束一个事务,就像你在执行完语句之前执行了一个COMMIT一样.

COMMIT表示当前事务中所做的更改是永久性的,并且对其他会话可见.另一方面,ROLLBACK语句取消当前事务所做的所有修改.COMMIT和ROLLBACK都释放在当前事务期间设置的所有InnoDB锁.

使用事务对DML操作分组(Grouping DML Operations with Transactions)

默认情况下,与MySQL服务器的连接始于启用自动提交模式,该模式会在你执行时自动提交每个SQL语句.如果你有其他数据库系统的经验,可能不熟悉此操作模式,其中标准做法是发出一系列DML语句并将它们提交或一起回滚.

为了使用多语句事务,使用SQL语句SET autocommit = 0关闭自动提交并且每个事务都以COMMIT或ROLLBACK结束.要启用自动提交,使用START TRANSACTION开始每个事务,使用COMMIT或ROLLBACK结束.下面的例子展示了两个事务.第一个被提交,第二个回滚了.

shell> mysql test
mysql> CREATE TABLE customer (a INT, b CHAR (20), INDEX (a));
Query OK, 0 rows affected (0.00 sec)
mysql> -- Do a transaction with autocommit turned on.
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO customer VALUES (10, 'Heikki');
Query OK, 1 row affected (0.00 sec)
mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
mysql> -- Do another transaction with autocommit turned off.
mysql> SET autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO customer VALUES (15, 'John');
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO customer VALUES (20, 'Paul');
Query OK, 1 row affected (0.00 sec)
mysql> DELETE FROM customer WHERE b = 'Heikki';
Query OK, 1 row affected (0.00 sec)
mysql> -- Now we undo those last 2 inserts and the delete.
mysql> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM customer;
+------+--------+
| a    | b      |
+------+--------+
|   10 | Heikki |
+------+--------+
1 row in set (0.00 sec)
mysql>

客户端语言中的事务(Transactions in Client-Side Languages)

在例如PHP,Perl DBI,JDBC,ODBC或其他MySQL标准C调用接口,你可以将事务控制语句(如COMMIT)作为字符串发送到MySQL服务器,就像任何其他SQL语句(如SELECT或INSERT)一样.某些API还提供单独的特殊事务提交和回滚功能或方法.

14.7.2.3 一致性非锁定读(Consistent Nonlocking Reads)

一致性读意味着InnoDB使用多版本控制(multi-versioning)在某个时间点向查询提供数据库的快照.查询将查看在该时间点之前提交的事务所做的更改,并且不会对以后或未提交的事务所做的更改进行更改.此规则的例外是查询会查看同一事务中更早提交的语句所做的更改.这个例外会导致下面的问题:如果你更新了表中的某些行,一个SELECT会查看最近更新的行,但它也可能查找到任何行的旧版本.如果其他会话同时更新相同的表,问题出现在你可能会看到一个从没在数据库中存在过的该表的状态.

如果事务的隔离级别是REPEATABLE READ(默认配置),同一事务中的所有一致性读都读取该事务第一次创建的快照.你可以在执行了新的查询之后提交该事务来获得一个更新的快照.

在READ COMMITTED隔离级别下,事务的每个一致性读都读取其自己的新快照.

一致性读是InnoDB的 READ COMMITTED REPEATABLE READ隔离级别默认的SELECT语句的处理模式.一致性读不在访问的表上设置任何锁,因此其他会话可以在一致性读在表上执行的同时自由地修改这些表.

假设你运行在默认的 REPEATABLE READ事务隔离级别下.当你发起一个一致性读(也就是普通的SELECT语句)的时候,InnoDB给你一个事务的时间点,根据这个时间点来查询你要查看的数据库.如果其他事务在你被授予的时间点之后删除了一行数据然后提交的话,你是不会看见这行数据被删除的.INSERT和UPDATE也是相似的.

你可以通过提交事务来更新你的时间点然后进行新的SELETE START TRANSACTION WITH CONSISTENT SNAPSHOT.

这被称为多版本并发控制(multi-versioned concurrency control).

在下面的例子里,会话A只有在B提交了insert数据并且A自己也进行了提交之后才能看到B插入的数据(因为时间节点更新到了B提交之后).

             Session A              Session B

           SET autocommit=0;      SET autocommit=0;
time
|          SELECT * FROM t;
|          empty set
|                                 INSERT INTO t VALUES (1, 2);
|
v          SELECT * FROM t;
           empty set
                                  COMMIT;

           SELECT * FROM t;
           empty set

           COMMIT;

           SELECT * FROM t;
           ---------------------
           |    1    |    2    |
           ---------------------

如果你想始终查看”最新鲜”的数据库,使用 READ COMMITTED事务隔离级别或者加锁读:

SELECT * FROM t FOR SHARE;

在READ COMMITTED隔离级别下,一个事务下的每个一致性读都会设置和读取自己最新的快照.使用LOCK IN SHARE MODE会发生加锁读:SELECT会阻塞直到包含有最新行的事务结束(才执行).

一致性读对某些DDL语句不起作用:

不指定FOR UPDATELOCK IN SHARE MODE的select的读取类型因INSERT INTO ... SELECT,UPDATE ...(SELECT)CREATE TABLE ... SELECT等子句中的选择而异.

14.7.2.4 加锁读(Locking Reads)

如果你在同一个事务里查询数据然后插入或更新相关的数据,常规SELECT语句没有提供足够的保护.其他事务可以更新或删除你刚刚查询的行.InnoDB支持两种类型的加锁读来提供额外的安全性:

这些子句在处理树形结构或图形结构数据时非常有用,无论是在单个表中还是在多个表中分割.你将边缘或树的分支从一个地方遍历到另一个地方,同时保留返回的权限并更改任何这些”指针”值.

在提交或回滚事务时,将释放由LOCK IN SHARE MODE和FOR UPDATE查询设置的所有锁.

外部语句中的锁定读取子句不会锁定嵌套子查询中的表行,除非在子查询中也指定了锁定读取子句.例如,以下语句不会锁定表t2中的行:

SELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2) FOR UPDATE;

要锁定表t2中的行,请在子查询中添加一个锁定读取子句:

SELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2 FOR UPDATE) FOR UPDATE;

加锁读的例子

假设你想要插入一个新行到子表中,确认对应的子表行在父表中有父行.你的应用程序代码可确保整个操作序列中的引用完整性.

首先,使用一个一致性读查询父表并且确认父表中的行存在.你可以将子行安全的插入到子表吗?不,因为其他会话可以在你的SELECT和INSERT语句之间删除父表中的行而你不会意识到.

为了避免上面的问题,使用LOCK IN SHARE MODE来执行SELECT:

SELECT * FROM parent WHERE NAME = 'Jones' LOCK IN SHARE MODE;

在LOCK IN SHARE MODE查询返回父表数据’Jones’之后,你可以安全的添加子记录到子表中然后提交事务.尝试获取父表中适用行的独占锁的任何事务都会等到完成后,即直到所有表中的数据都处于一致状态.

对另一个例子,考虑表CHILD_CODES中的一个整数计数字段,用于为添加到CHILD表中的数据分配唯一标识符.不要使用一致读取或共享模式读取来读取计数器的当前值,因为数据库的两个用户可以看到计数器的相同值,如果两个事务尝试将具有相同标识符的行添加到CHILD表,则会发生重复键错误.

在这里 LOCK IN SHARE MODE不是一个好的解决方法因为如果两个用户同时读取计数器,当它尝试更新计数器时,至少有一个用户会死锁.

要实现读取和递增计数器,首先使用FOR UPDATE执行计数器的锁定读取,然后递增计数器.例如:

SELECT counter_field FROM child_codes FOR UPDATE;
UPDATE child_codes SET counter_field = counter_field + 1;

SELECT ... FOR UPDATE读取最新的可用数据,在其读取的每一行上设置独占锁.因此,它设置搜索的SQL UPDATE将在行上设置的相同锁.

前面的描述仅仅是SELECT ... FOR UPDATE如何工作的一个例子.在MySQL中,生成唯一标识符的具体任务实际上只需对表进行一次访问即可完成:

UPDATE child_codes SET counter_field = LAST_INSERT_ID(counter_field + 1);
SELECT LAST_INSERT_ID();

SELECT语句仅检索标识符信息(特定于当前连接).它不访问任何表.

14.7.3 InnoDB中的不同SQL语句设置的锁

加锁读,UPDATE或DELETE会在正在处理的SQL语句中被扫描到的每个索引记录上设置锁.这跟WHERE子句中是否会排除行数据无关.InnoDB不会记住精确的WHERE语句,(它)只知道索引要扫描的范围.锁通常是next-key锁同时也会阻止插入到记录之前的间隙(gap)中.然而间隙锁可以被显式禁用,这会导致next-key锁不被使用.

如果在搜索中使用了二级索引并且索引记录被设置为独占,InnoDB也会检索相应的聚簇索引并且对其加锁.

如果对你的语句没有合适的索引,MySQL必须扫描整张表来处理语句,表中的每一行都会被加锁,这会使得其他用户对这张表的insert操作全部阻塞.创建一个好的索引非常重要,这样你的查询就不会扫描到很多不必要的行.

InnoDB设置特定类型的锁,如下:


Edit page
Share this post on:

Previous Post
InnoDB官方文档--14.7.4 幻影行|14.7.5 InnoDB的死锁
Next Post
InnoDB官方文档--14.7 InnoDB锁和事务模型|14.7.1 InnoDB锁