概述
不少人对于事务的使用局限于begin transaction:开始事务、commit transaction:提交事务、rollback transaction:回滚事务的初步运用。
并且知道使用事务后, 事务中所有操作命令必须作为一个整体提交或回滚,如果事务中任何操作命令失败,则整个事务将因失败而回滚。
除了这个概念性的东西后,其他就不怎么知道了,比如事务的隔离性,具体怎么隔离、有几种隔离方式、执行顺序是什么。
我们今天来聊一聊这一块的内容(主要是事务的隔离性)。
什么是事务(定义)
事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。
事务通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起,并用形如begin transaction和end transaction语句(或函数调用)来界定。
事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。
事务是作为单个逻辑单元执行的一系列操作,它是一个不可分割的工作逻辑单元。它包含了一组数据库操作命令,这组命令要么全部执行,要么全部不执行。
事务的4个特性(特性)
事务是恢复和并发控制的基本单位。
事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。
原子性(atomicity)
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,这和前面两篇博客介绍事务的功能是一样的概念,
因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
一致性(consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
比如转账,假设张三和李四两个人的钱加起来一共是2000,那么不管张三和李四之间怎么转账,转几次账,事务结束后两个人的钱加起来还得是2000,这就是事务的一致性。
隔离性(isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
持久性(durability)
持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,
即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。
事务的分类 (分类)
事务分为三类:显式事务、隐式事务、自动提交事务
(1) 显式事务:用 begin transaction 明确指定事务的开始,由 commit transaction 提交事务、rollback transaction 回滚事务到事务结束。
(2) 隐式事务:通过设置 set implicit_transactions on 语句,将隐式事务模式设置为打开。
当以隐式事务模式操作时,不必使用 begin transaction 开启事务,当一个事务结束后,这个模式会自动启用下一个事务,
只需使用 commit transaction 提交事务或 Rollback Transaction 回滚事务即可。
(3) 自动提交事务: 这是 SQL Server 的默认模式,它将每条单独的 T-SQL 语句视为一个事务。如果成功执行,则自动提交。如果错误,则自动回滚。
简单事务应用示例
我们先看一个简单的应用。
--这里指定不指定事务名称均可 BEGIN TRANSACTION tran_UpdateTable --开启事务(tran_UpdateTable:事务名称) DECLARE @tran_error int;--定义错误变量 SET @tran_error=0; --错误变量的初始值为0 --使用 try catch进行错误捕捉 BEGIN TRY UPDATE a_Students SET Name='孙悟空' WHERE Number='100001'; UPDATE a_StudentsScore SET C# ='我是分数,应该是数字' WHERE Number='100001'; END TRY BEGIN CATCH set @tran_error=@tran_error+1; --(计算捕捉到的错误数)加分号或不加都能正常执行 END CATCH --判断是否有执行错误 IF(@tran_error>0) BEGIN ROLLBACK TRANSACTION tran_UpdateTable --执行出错,回滚事务(tran_UpdateTable:指定事务名称) PRINT '有【'+CONVERT(VARCHAR(50),@tran_error)+'】条执行失败,进行回滚:'; END ELSE BEGIN COMMIT TRANSACTION tran_UpdateTable --没有异常,提交事务(tran_UpdateTable:指定事务名称) --事务执行成功后,查看修改后的数据 SELECT s.Number , s.Name , sc.ClassName , ss.C# , ss.SqlDB , ss.Java , ss.Python FROM a_Students s INNER JOIN a_StudentClass sc ON s.ClassId = sc.ClassId INNER JOIN a_StudentsScore ss ON s.Number = ss.Number WHERE s.Number='100001' END
上面事务表示:根据学生编号修改学生姓名和学生学科C#的成绩,如果有任何一条执行失败则全部返回不执行,否则执行修改成功。
事务不隔离导致的问题
以上介绍完事务的四大特性(简称ACID),现在重点来说明下事务的隔离性,
当多个线程都开启事务操作数据库中的数据时,数据库系统要能进行隔离操作,以保证各个线程获取数据的准确性,
在介绍数据库提供的各种隔离级别之前,我们先看看如果不考虑事务的隔离性,会发生的几种问题:
更新丢失(Lost update)、脏读(Dirty Reads)、不可重复读(Non-repeatable Reads)
1、更新丢失(Lost update)
两个事务都同时更新一行数据,但是第二个事务却中途失败退出,导致对数据的两个修改都失效了。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来。
2、脏读(Dirty Reads)
一个事务开始读取了某行数据,但是另外一个事务已经更新了此数据但没有能够及时提交。这是相当危险的,因为很可能所有的操作都被回滚。
当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。
例如:用户A向用户B转账1000元,对应SQL命令如下:
--用户A向用户B转账1000元,B的钱增加 update account set money=money+1000 where name='B'; --此时A通知B我给你转钱了,A的钱减少 update account set money=money - 1000 where name='A';
当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账(此时即发生了脏读),
而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转。
3、不可重复读(Non-repeatable Reads)
不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
(1) 虚读:事务T1读取某一数据后,事务T2对其做了修改,当事务T1再次读该数据时得到与前一次不同的值。
(2) 幻读:事务在操作过程中进行两次查询,第二次查询的结果包含了第一次查询中未出现的数据或者缺少了第一次查询中出现的数据(这里并不要求两次查询的SQL语句相同)。
这是因为在两次查询过程中有另外一个事务插入数据造成的。
事务的隔离级别
为了避免上面出现的几种情况,在标准SQL规范中,定义了4+2个事务隔离级别,不同的隔离级别对事务的处理不同
1、未提交读取(相当于with(nolock)):第一级别
也称为未授权读取:允许脏读取,但不允许更新丢失。
如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。
该隔离级别可以通过“排他写锁”实现。
缺点:会产生脏读、不可重复读、幻读。
案例解读:以前交易所炒股的时候,股民老王购买了5000股,操作员操作录入(此时开启事务),操作时手误,多输入了一个0,数据保存但是未提交。
此时老王查询自己的持股信息,发现自己居然有50000股,瞬间血压升高,昏倒在地。
然后操作员发现自己录入错误,修改成正确的信息,录入完成(事务结束)。
老王被救醒后,哆嗦这查询自己的持股,发现只有5000,没有增减,他之前看到的就是脏读数据。
解决方案:采用更高级的隔离机制,如提交读。
2、提交读取(Oracle和SQLServer默认的):第二级别
这是大多数数据库系统的默认隔离级别(Oracle和SQLServer默认的)。
也称为授权读取:允许不可重复读取,但不允许脏读取。
这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。
缺点:会产生不可重复读、幻读。
案例解读:股市升高后,老王查看自己持有5000股,就想卖掉4000股,在老王卖股票的时候,老王的老婆看股市太高,就登录老王的账号,卖掉3000股。
当老王想卖股票时,发现自己只有2000股,不是之前看到的5000股,这就是不可重复读问题。
解决方案:采用更高级的隔离机制,如可重复读。