首页 > 技术文章 > 事务控制和锁定语句

outstandingjie 2017-12-13 15:42 原文

一、LOCK TABLE 和 UNLOCK TABLE

lock tables 锁定当前线性表。如果表被其他线程锁定,则当前线程会等待,直到可以获取所有锁定为止。

unlock tables 释放当前线程获得的任何锁定。当前线程执行另一个 LOCK TABLES 时, 或当与服务器的连接被关闭时,所有由当前线程锁定的表被隐含地解锁 

 

一个获得表锁和释放表锁的简单例子

session_1 session_2

获得表film_text的Read锁定

lock table film_text read

 

当前session可以查询该表纪录

select id,title from film_text where id = 1;

+-------+-----------

   id.        title

+--------+----------

    1          标签

其他session也可以查询该表的纪录

select id,title from film_text where id = 1;

+-------+-----------

   id.        title

+--------+----------

    1          标签

 

 

 其他session更新锁定表会等待获得锁

update film_text set title='lalamei' where id=1;

......等待

释放锁:unlock tables

......等待

 

session 获得锁,更新操作完成

完成

 

 

二、事务控制

默认情况下,MySQL 是自动提交(Autocommit)的,如果需要通过明确的 Commit 和 Rollback 来提交和回滚事务,那么需要通过明确的事务控制命令来开始事务 。

* start transaction 或 begin语句可以开始一项新的事务

* commit 和 rollback 用来提交或者回滚事务

* chain 和 release 字句分别用来定义在事务提交或者回滚之后的操作,chain会立即启动一个新事物,并且和刚才的事物具有相同的隔离级别,release则会断开和客户端连接

* set autocommit 可以修改当前连接的提交方式,如果设置了set autocommit=0,则设置之后的所有事务都需要通过明确的命令进行提交或者回滚

 

演示使用start transaction 开始的事务在提交后自动回到自动提交的方式;如果在提交的时候使用commit and chain,那么会在提交后立即开启一个新的事务。

start transaction和commit and chain 的使用例子

 

 

session_1 session_2

从表actor中查询actor_id=201的记录,结果为空:

mysql> select * from actor where actor_id = 201;

Empty set (0.00 sec)

 

从表actor中查询actor_id=201的记录,结果为空:

mysql> select * from actor where actor_id = 201;

Empty set (0.00 sec)

用start transaction 命令启动一个事务,往表actor中插入一条纪录,没有commit:

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

 

mysql> insert into actor(actor_id,first_name,last_name) values(201,'lisa','tom');

Query OK, 1 row affected (0.00 sec)

查询表actor,结果仍然为空:

mysql> select * from actor where actor_id=201;

Empty set (0.00 sec)

 

 执行提交:

mysql> commit;

Query OK, 0 rows affected (0.00 sec)

 
 

 再次查询表actor,可以查询到结果:

mysql> select * from actor where actor_id=201;

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

|      201 | lisa       | tom       |

+----------+------------+-----------+

1 row in set (0.00 sec)

 这个事务是按照自动提交执行的:

mysql> insert into actor(actor_id,first_name,last_name) values(202,'lisa','lan');

Query OK, 1 row affected (0.01 sec)

 
 

 可以从actor表中查询到session1刚刚插入的数据。

mysql> select * from actor where actor_id in (201,202);

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

|      201    | lisa           | tom       |

|      202    | lisa           | lan        |

+----------+------------+-----------+

2 rows in set (0.00 sec)

 重新用start transaction 启动一个事务:

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

 

往表actor中插入一条纪录:

mysql> insert into actor(actor_id,first_name,last_name) values(203,'lisa','tt');

Query OK, 1 row affected (0.00 sec)

 

用commit and chain命令提交:

mysql> commit and chain;

Query OK, 0 rows affected (0.00 sec)

 

此时自动开始一个新的事务:

mysql> insert into actor (actor_id,first_name,last_name) values(204,'Lisa','Mou');

Query OK, 1 row affected (0.00 sec) 

 
 

 session1刚插入的纪录无法查看:

mysql> select * from actor where first_name = 'lisa';

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

|      201   | lisa           | tom       |

|      202   | lisa           | lan         |

|      203   | lisa           | tt           |

+----------+------------+-----------+

3 rows in set (0.00 sec)

 用commit命令提交:

mysql> commit;

Query OK, 0 rows affected (0.01 sec)

 
 

 session1插入的新记录可以看到:

mysql> select * from actor where first_name = 'lisa';

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

|      201    | lisa          | tom       |

|      202    | lisa          | lan        |

|      203    | lisa          | tt          |

|      204    | lisa         | mou       |

+----------+------------+-----------+

4 rows in set (0.00 sec)

 

 

如果在锁表期间,用 start sransaction 命令开始一个新事务,会造成一个隐含的unlock tables 被执行

start transaction 导致的 unlock tables

 

 

session_1 session_2

从表actor中查询actor_id=301的记录,结果为空:

mysql> select * from actor where actor_id = 301;

Empty set (0.00 sec)

 

从表actor中查询actor_id=301的记录,结果为空:

mysql> select * from actor where actor_id = 301;

Empty set (0.00 sec)

 对表actor加写锁:

mysql> lock table actor write;

Query OK, 0 rows affected (0.00 sec)

 
 

对表 actor 的读写操作被阻塞:

mysql> select * from actor where actor_id=301;

....等待 

 插入一条记录

mysql> insert into actor(actor_id,first_name,last_name) values(301,'Lisa','Tom');

Query OK, 1 row affected (0.00 sec)

 ....等待 

 回滚刚才的纪录:

mysql> rollback;

Query OK, 0 rows affected (0.00 sec)

 ....等待 

 用start transaction 命令重新开始一个事务:

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

 ....等待  
 

session1 开始一个事务时,表锁被释放,可以查询:

mysql> select * from actor where actor_id=301;

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

|      301   | Lisa          | Tom         |

+----------+------------+-----------+

1 row in set (14 min 58.07 sec)

对lock方式加的表锁,不能通过rollback进行回滚。

 

 

/*

* 1、commit、rollback只能对事务类型的表进行提交和回滚

* 2、通常对提交的事务纪录到二进制文件中

* 3、所有的DDL语句是不能回滚的,部分DDL语句会造成隐式提交

*/

在事务中可以通过定义 SAVEPOINT,指定回滚事务的一个部分,但是不能指定提交事务 的一个部分。对于复杂的应用,可以定义多个不同的 SAVEPOINT,满足不同的条件时,回滚 不同的 SAVEPOINT。需要注意的是,如果定义了相同名字的 SAVEPOINT,则后面定义的 SAVEPOINT 会覆盖之前的定义。对于不再需要使用的 SAVEPOINT,可以通过 RELEASE SAVEPOINT 命令删除 SAVEPOINT,删除后的 SAVEPOINT,不能再执行 ROLLBACK TO SAVEPOINT 命令。 

 

模拟回滚事务

 

session_1 session_2

从表actor中查询 first_name='Simon' 的纪录,

结果为空:

mysql> select * from actor where first_name = 'Simon';

Empty set (0.00 sec)

 

从表actor中查询 first_name='Simon' 的纪录,

结果为空:

mysql> select * from actor where first_name = 'Simon';

Empty set (0.00 sec)

启动一个事务,往表 actor 中插入一条纪录;

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

 

mysql> insert into actor(actor_id,first_name,last_name) values(501,'Simon','Tom');

Query OK, 1 row affected (0.00 sec)

 

可以查询到刚刚插入的纪录:

mysql> select * from actor where first_name = 'Simon';

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

|      501   | Simon       | Tom       |

+----------+------------+-----------+

1 row in set (0.00 sec)

 

无法从actor表中查询到session1刚插入的纪录:

mysql> select * from actor where first_name = 'Simon';

Empty set (0.00 sec)

 

 定义 savepoint,名称为test;

mysql> savepoint test;

Query OK, 0 rows affected (0.00 sec)

 

继续插入一条纪录:

mysql> insert into actor(actor_id,first_name,last_name) values(502,'Simon','Cof');

Query OK, 1 row affected (0.00 sec)

 

可以查询到两条纪录

mysql> select * from actor where first_name = 'Simon';

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

|      501   | Simon      | Tom        |

|      502   | Simon      | Cof         |

+----------+------------+-----------+

2 rows in set (0.00 sec)

仍然无法查询到结果:

mysql> select * from actor where first_name = 'Simon';

Empty set (0.00 sec)

 

回滚到刚才定义的savepoint:

mysql> rollback to savepoint test;

Query OK, 0 rows affected (0.00 sec)

 

只能从表 actor 中查询到第一条记录,因为第二条记录已经回滚:

mysql> select * from actor where first_name = 'Simon';

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

|      501   | Simon       | Tom       |

+----------+------------+-----------+

1 row in set (0.00 sec)

仍然无法查询到结果:

mysql> select * from actor where first_name = 'Simon';

Empty set (0.00 sec)

 

用commit命令提交:

mysql> commit;

Query OK, 0 rows affected (0.00 sec)

 
 

只能从表 actor 中查询到第一条记录:

mysql> select * from actor where first_name = 'Simon';

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

|      501   | Simon       | Tom       |

+----------+------------+-----------+

1 row in set (0.00 sec)

只能从表 actor 中查询到 session1 插入的第一条记录:

mysql> select * from actor where first_name = 'Simon';

+----------+------------+-----------+

| actor_id | first_name | last_name |

+----------+------------+-----------+

|      501   | Simon       | Tom       |

+----------+------------+-----------+

1 row in set (0.00 sec)

 

 

 

三、分布式事务的使用

简介

1、分布式事务只支持innoDB存储引擎

2、一个分布式事务会涉及多个行动,这些行动本身是事务性的。所有行动都必须一起成功完成,或者一起被回滚

 

分布式事务的原理

 

分布式事务语法

XA {START|BEGIN} xid [JOIN|RESUMF]

XA START xid 用于启动一个带给xid值的XA事务。每个XA事务必须有一个唯一的xid值,因此该值当前不能被其他的XA事务使用。

xid是一个XA事务标识符,用来唯一标识一个分布式事务。xid值由客服端提供,或由mysql服务器生成。

xid值包含1~3个部分

xid : gtrid [, bqual [, formatId ] ]

* gtrid 是一个分布式事务标识符,相同的分布式事务应该使用相同的gtrid,这样可以明确知道xa事务属于那个分布式事务。

* bqual 是一个分支限定符,默认值是空串。对于一个分布式事务中的每个分支事务,bqual值必须是唯一的。

* formatID 是一个数字,用于标识由gtrid 和 bqual 值使用的格式,默认值是1。

 

下面其他 XA 语法中用到的 xid 值,都必须和 START 操作使用的 xid 值相同,也就是表示 对这个启动的 XA 事务进行操作。

XA END xid [ SUSPEND  [ FOR MIGRATE ] ]

XA PREPARE xid 

使事务进入 PREPARE 状态,也就是两阶段提交的第一个提交阶段 

 

XA COMMIT xid [ ONE PHASE ]
XA ROLLBACK xid 

这两个命令用来提交或者回滚具体的分支事务。也就是两阶段提交的第二个提交阶段, 分支事务被实际的提交或者回滚。 

 

XA RECOVER 

XA RECOVER返回当前数据库中处于PREPARE 状态的分支事务的详细信息。 

 

分布式的关键在于?

如何确保分布式事务的完整性,

在某个分支出现问题时的故障解决。

 

演示了一个简单的分布式事务的执行,事务的内容是在 DB1 中插入一条记录, 同时在 DB2 中更新一条记录,两个操作作为同一事务提交或者回滚。 

分布式事务例子

 

session_1 in DB1 session_2 in DB2

在数据库 DB1 中启动一个分布式事务的一个分支事务,xid 的 gtrid 为test,bqual 为 db1:

mysql> xa start 'test','db1';

Query OK, 0 rows affected (0.00 sec)

 

分支事务1在表 actor 中插入一条纪录:

mysql> insert into actor(actor_id,first_name,last_name) values(601,'Simon','Tom');

Query OK, 1 row affected (0.00 sec)

 

对分支事务1进行第一阶段提交,进入 prepare 状态:

mysql> xa end 'test','db1';

Query OK, 0 rows affected (0.00 sec)

 

mysql> xa prepare 'test','db1';

Query OK, 0 rows affected (0.00 sec)

在数据库DB2中启动分布式事务test的另一个分支事务,xid 的 gtrid 为 test, bqual 为 db2: 

mysql> xa start 'test','db2';

Query OK, 0 rows affected (0.00 sec)

 

分支事务 2 在表 film_actor 中更新了21条记录:

mysql> update film_actor set last_update=now() where actor_id=178;

Query OK, 21 rows affected (0.00 sec)

Rows matched: 21  Changed: 21  Warnings: 0

 

对分支事务 2 进行第一阶段提交,进入 prepare 状态:

mysql> xa end 'test','db2';

Query OK, 0 rows affected (0.00 sec)

 

mysql> xa prepare 'test','db2';

Query OK, 0 rows affected (0.01 sec)

用 xa recover 命令查看当前分支事务状态:

mysql> xa recover \G;

***************************

***************************

    formatID: 1

gtrid_length: 4

bqual_length: 3

        data: testdb1

1 rows in set (0.00 sec)

用 xa recover 命令查看当前分支事务状态:

mysql> xa recover \G;

***************************

***************************

    formatID: 1

gtrid_length: 4

bqual_length: 3

        data: testdb2

 1 rows in set (0.00 sec)

                                      两个事务都进入准备提交阶段,如果之前遇到任何错误,都应该回滚所有的分支,以确保分布事务的正确。

 提交分支事务 1:

mysql> xa commit 'test','db1';

Query OK, 0 rows affected (0.00 sec)

 提交分支事务2:

mysql> xa commit 'test','db2';

Query OK, 0 rows affected (0.00 sec)

 

 两个事务都到达准备提交阶段后,一旦开始进行提交操作,就需要确保全部的分支都提交成功。  

 

 

存在的问题1

条件:

分支事务在达到prepare状态时,数据库异常重新启动,重启以后可以继续执行事务进行提交回滚的操作。

问题:

提交事务没有写binlog,存在隐患,可能导致使用binlog恢复丢失部分数据。如果存在复制的数据库,则有可能导致主从数据库的数据不一致。

 

存在问题二

如果分支事务的客户端连接异常中止,那么数据库会自动回滚未完成的分支事务,如果 此时分支事务已经执行到 prepare 状态,那么这个分布式事务的其他分支可能已经成功提交, 如果这个分支回滚,可能导致分布式事务的不完整,丢失部分分支事务的内容。 

推荐阅读