每天10分钟学习事务与并发(Part 1)

【数据库技术】|作者 / Edison Zhou

上一篇介绍了SQL Server的一些可编程对象,本篇会介绍一个你可能必须要关注的主题:事务与并发,考虑到文章长度,本篇文章会重点介绍事务的基本概念、特性、锁和阻塞,下篇文章会介绍事务的几个隔离级别(面试常考点)。

1事务的概念

事务是作为单个工作单元而执行的一系列操作,比如查询和修改数据等。

事务是数据库并发控制的基本单位,一条或者一组语句要么全部成功,对数据库中的某些数据成功修改; 要么全部不成功,数据库中的数据还原到这些语句执行之前的样子。

举个例子,比如网上订火车票,要么你定票成功,余票显示就减一张; 要么你定票失败获取取消订票,余票的数量还是那么多。不允许出现你订票成功了,余票没有减少或者你取消订票了,余票显示却少了一张的这种情况。

这种不被允许出现的情况就要求购票和余票减少这两个不同的操作必须放在一起,成为一个完整的逻辑链,这样就构成了一个事务。

2事务的特性及定义

想必我们在大学学习数据库原理的时候都学过事务的ACID特性,现在我们复习一下:

原子性

事务的原子性是指一个事务中包含的一条语句或者多条语句构成了一个完整的逻辑单元,这个逻辑单元具有不可再分的原子性。这个逻辑单元要么一起提交执行全部成功,要么一起提交执行全部失败。

一致性

可以理解为数据的完整性,事务的提交要确保在数据库上的操作没有破坏数据的完整性,比如说不要违背一些约束的数据插入或者修改行为。一旦破坏了数据的完整性,SQL Server 会回滚这个事务来确保数据库中的数据是一致的。

隔离性

与数据库中的事务隔离级别以及锁相关,多个用户可以对同一数据并发访问而又不破坏数据的正确性和完整性。但是,并行事务的修改必须与其它并行事务的修改相互独立,隔离。 但是在不同的隔离级别下,事务的读取操作可能得到的结果是不同的。

持久性

数据持久化,事务一旦对数据的操作完成并提交后,数据修改就已经完成,即使服务重启这些数据也不会改变。相反,如果在事务的执行过程中,系统服务崩溃或者重启,那么事务所有的操作就会被回滚,即回到事务操作之前的状态。

在极端断电或者系统崩溃的情况下,一个发生在事务未提交之前,数据库应该记录了这个事务的"ID"和部分已经在数据库上更新的数据。供电恢复数据库重新启动之后,这时完成全部撤销和回滚操作。如果在事务提交之后的断电,有可能更改的结果没有正常写入磁盘持久化,但是有可能丢失的数据会通过事务日志自动恢复并重新生成以写入磁盘完成持久化。

如何定义一个事务

显示定义:

以BEGIN TRAN开始,提交的话则COMMIT提交事务,否则以ROLLBACK回滚事务。

--定义事务

BEGIN TRAN;

INSERT INTO dbo.T1(keycol, col1, col2) VALUES(4,101,'C'); 

INSERT INTO dbo.T1(keycol, col1, col2) VALUES(4,201,'X');

COMMIT TRAN;

隐式定义:

SQL Server中默认把每个单独的语句作为一个事务。

换句话说,SQL Server默认在执行完每个语句之后就自动提交事务。当然,我们可以通过IMPLICIT_TRANSACTIONS会话选项来改变SQL Server处理默认事务的方式,该选项默认情况下是OFF。如果将其设置为ON,那么就不必用BEGIN TRAN语句来表明事务开始,但仍然需要以COMMIT或ROLLBACK来标明事务完成。

3锁

锁是事务获取的一种控制资源,用于保护数据资源,防止其他事务对数据进行冲突的或不兼容的访问。

锁有哪几种模式及其兼容性

主要有两种主要的锁模式:排它锁(Exclusive Lock) 和共享锁(Shared Lock)。

当试图修改数据时,事务会为所依赖的数据资源请求排它锁,一旦授予,事务将一直持有排它锁,直至事务完成。在事务执行过程中,其他事务就不能再获得该资源的任何类型的锁。

当试图读取数据时,事务默认会为所依赖的数据资源请求共享锁,读操作一完成,就立即释放共享锁。在事务执行过程中,其他事务仍然能够获得该资源的共享锁。

排它锁和共享锁的兼容性请求模式已经授予排它锁(X) 已经授予共享锁(S)授予请求的排它锁? 否 否授予请求的共享锁? 否 是

可锁定资源的类型

MS SQL Server可以锁定不同类型或粒度的资源,这些资源类型包括RID或KEY(行),PAGE(页)、对象(例如:表)及数据库等。

4阻塞

如果一个事务持有某一数据资源上的锁,而另一事务请求相同资源上的不兼容的锁,则对新锁的请求将被阻塞,发出请求的事务进入等待状态。默认情况下,被阻塞的请求会一直等待,直到原来的事务释放相关的锁

只要能够在合理的时间范围内满足请求,系统中的阻塞就是正常的。但是,如果一些请求等待了太长时间,可能就需要手工排除阻塞状态,看看能采取什么措施来防止这样长时间的延迟。

近距离观测阻塞

Step1.打开两个独立的查询窗口,这里称之为Connection A,Connection B

Step2.在Connection A中运行以下代码(这里productid=2的unitprice本来为19)

BEGIN TRAN; 

UPDATE Production.Products SET unitprice=unitprice+1.00  WHERE productid=2;

为了更新这一行,会话必须先获得一个排它锁,如果更新成功,SQL Server会向会话授予这个锁。

Step3.在Connection B中运行以下代码

SELECT productid, unitpriceFROM Production.ProductsWHERE productid=2;

默认情况下,该会话需要一个共享锁,但因为共享锁和排它锁是不兼容的,所以该会话被阻塞,进入等待状态。

如何解除阻塞

假设我们的系统里边出现了阻塞,而且被阻塞了很长时间,如何去检测和排除呢?

① 继续上例,打开一个新的会话,称之为Connection C,查询动态管理视图(DMV)sys.dm_tran_locks:

-- Lock info

SELECT

      -- use * to explore

      request_session_id            AS spid, 

      resource_type                AS restype, 

      resource_database_id          AS dbid, 

      DB_NAME(resource_database_id) AS dbname, 

      resource_description          AS res, 

      resource_associated_entity_id AS resid, 

      request_mode                  AS mode,

      request_status                AS status

  FROM sys.dm_tran_locks;

② 运行上面的代码,可以得到以下输出:

③ 每个会话都有唯一的服务器进程标识符(SPID),可以通过查询@@SPID函数来查看会话ID。另外,当前会话的SPID还可以在查询窗口的标题栏中找到。

④ 在前面查询的输出中,可以观察到进程53正在等待请求TSQLFundamental2008数据库中一个行的共享锁。但是,进程52持有同一个行上的排它锁。沿着52和53的所层次结构向上检查:(查询sys.dm_exec_connections的动态管理视图,筛选阻塞链中涉及到的那些SPID)

-- Connection info

SELECT

  -- use * to explore 

      session_id AS spid, 

      connect_time,  last_read,  last_write, 

        most_recent_sql_handle

FROM sys.dm_exec_connections

WHERE session_id IN(52, 53);

查询结果输出如下:

⑤ 借助交叉联接,和sys.dm_exec_sql_text表函数生成查询结果:

-- SQL text

SELECT

session_id,

        text

FROM sys.dm_exec_connections 

CROSS APPLY sys.dm_exec_sql_text(most_recent_sql_handle) AS STWHERE session_id IN(52, 53);

查询结果如下,我们可以达到阻塞链中涉及到的每个联接最后调用的批处理代码:

以上就显示了进程53正在等待的执行代码,因为这是该进程最后执行的一个操作。对于阻塞进程来说,通过这个例子能够看到是哪条语句导致了问题。

如何解除阻塞

① 设置超时时间

首先取消掉原来Connection B中的查询,然后执行以下代码:这里我们限制会话等待释放锁的时间为5秒

-- Session BSET LOCK_TIMEOUT 5000;

SELECT

productid, unitprice

FROM Production.Products

WHERE productid=2;

然后5秒之后我们可以看到以下执行结果:

注意:锁定超时不会引发事务回滚

② KILL掉引起阻塞的进程

在Connection C中执行以下语句,终止SPID=52中的更新事务而产生的效果,于是SPID=52中的事务的回滚,同时释放排它锁。

--KILL SPID=52KILL 52;

这时再在Connection B中执行查询,便可以查到回滚后的结果(仍然是19):

5小结

本文介绍了在事务的基本概念、ACID特性 及 如何在Microsoft SQL Server中定义一个事务,还介绍了锁 和 阻塞,及在Microsoft SQL Server中的示例,重点介绍了如何解除阻塞。下一篇Part 2会介绍在Microsoft SQL Server中事务的几种不同的隔离级别及其示例。

参考资料

[美] Itzik Ben-Gan 著,成保栋 译,《Microsoft SQL Server 2008技术内幕:T-SQL语言基础》

你可能感兴趣的:(每天10分钟学习事务与并发(Part 1))