事务:事务是形成一个逻辑工作单位的数据库操作的汇集。也就是说,它能以整体的原子操作形式完成的一系列操作,而且还能保证一个“全有或者全无”的命题成立。百科上解释:它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。
主要作用:保证数据的一致性。
特点:原子性、一致性、隔离性、持久性。ACID
l 原子性:一个事务中所有的数据库操作,是一个不可分割的整体,这些操作要么全部执
行,要么全部无效果。
l 一致性:一个事务独立执行的结果,将保持数据库的一致性,即数据不会因事务的执行
而被破坏。在事务执行过程中,可以违反一致性原则,并产生一个临时的不一致状态。
比如在转账过程中,会出现暂时的数据不一致的情况。当事务结束后,系统又回到一致
的状态。不过临时的一致性不会导致问题,因为原子性会使得系统始终保持一致性。
l 隔离性:在多个事务并发执行的时候,系统应该保证与这些事务先后单独执行时的结果
一样,即并发执行的任务不必关心其他事务。对于每一个事务来讲,那一刻看起来好像
只有它在修改数据库一样。事务系统是通过对后台数据库数据使用同步协议来实现隔离
性的。同步协议使一个事务与另外一个事务相分离。如果事务对数据进行了锁定,可以
使并发的事务无法影响该数据,直到锁定解除为止。
l 持久性:一个事务一旦完成全部操作以后,它对数据库的所有操作将永久地反映在
数据库中。持久性保证了系统在操作的时候免遭破坏。持久性主要是为了解决机器
故障、突然断电、硬盘损坏等问题而出现的。为了达到持久性,系统一般都保留了
一份日志。一旦出现故障,就可以通过日志将数据重建。
举例
用一个常用的“A账户向B账号汇钱”的例子来说明如何通过数据库事务保证数据的准确性和完整性。熟悉关系型数据库事务的都知道从帐号A到帐号B需要6个操作:
1、从A账号中把余额读出来(500)。
2、对A账号做减法操作(500-100)。
3、把结果写回A账号中(400)。
4、从B账号中把余额读出来(500)。
5、对B账号做加法操作(500+100)。
6、把结果写回B账号中(600)。
原子性:
保证1-6所有过程要么都执行,要么都不执行。一旦在执行某一步骤的过程中发生问题,就需要执行回滚操作。 假如执行到第五步的时候,B账户突然不可用(比如被注销),那么之前的所有操作都应该回滚到执行事务之前的状态。
一致性
在转账之前,A和B的账户*有500+500=1000元钱。在转账之后,A和B的账户*有400+600=1000元。也就是说,数据的状态在执行该事务操作之后从一个状态改变到了另外一个状态。同时一致性还能保证账户余额不会变成负数等。
隔离性
在A向B转账的整个过程中,只要事务还没有提交(commit),查询A账户和B账户的时候,两个账户里面的钱的数量都不会有变化。
如果在A给B转账的同时,有另外一个事务执行了C给B转账的操作,那么当两个事务都结束的时候,B账户里面的钱应该是A转给B的钱加上C转给B的钱再加上自己原有的钱。
持久性
一旦转账成功(事务提交),两个账户的里面的钱就会真的发生变化(会把数据写入数据库做持久化保存)!
事务的语句
开始事物:BEGIN TRANSACTION
提交事物:COMMIT TRANSACTION
回滚事务:ROLLBACK TRANSACTION
事务的隔离
数据库事务的隔离级别:四种
事务的隔离级别分为四种:读未提交(Read uncommitted)、读已提交(Read committed)、可重复读(Repeatable read)、可串行化(Serializable)。
要理解这些隔离级别的差异必须首先弄清如下几个概念:脏读、写覆盖、不可重复读、幻影读取。
举例:
假设同一个A和B两个同时并发操作数据库,A和B执行的任务如下:从数据库中读取整数N,将N 加上10,将新的N 更新回数据库。这两个并发执行的实例可能发生下面的执行顺序。
(1)A从数据库中读取整数N,当前数据库中N=0;
(2)N 加上10,并将其更新到数据库中,当前数据库中N=10。然而由于A 的事务还没有提交,所以数据库更新还没有称为持久性的;
(3)B从数据库中读取整数N,当前数据库中N=10;
(4)A回滚了事务,所以N 恢复到了N=0;
(5)B将N 加上10,并将其更新到数据库中,当前数据库中N=20;
这里出现了B在A提交之前读取了A所更新的数据,由于A回滚了事务,所以数据库中出现了错误的数据20。尽管A回滚了事务,但是A更新的数据还是间接的通过B被更新到了数据库中。这种读取了未提交的数据的方法就叫脏(dirty)读问题。
当一个用户从数据库中读取数据的时候,另外一个用户修改了这条数据,所以数据发生了改变,当再次读取的时候就出现了不可重复读取问题。比如:
(1)A从数据库中读取整数N;
(2)B以一个新的值更新N;
(3)当A再次从数据库中读取N 的时候,会发现N 的值变了;
幻影读取指的是在两次数据库操作读取操作之间,一组新的数据会出现在数据库中。比如:
(1)A从数据库检索到了一些数据;
(2)B通过Insert语句插入了一些新数据;
(3)A再次查询的时候,新的数据就会出现;
隔离级别 |
脏读 (Dirty Read) |
写覆盖 (Write Cover) |
不可重复读 (NonRepeatable Read) |
幻读 (Phantom Read) |
读未提交 (Read uncommitted) |
可能 |
可能 |
可能 |
可能 |
读已提交 (Read committed) |
不可能 |
可能 |
可能 |
可能 |
可重复读 (Repeatable read) |
不可能 |
|
不可能 |
可能 |
可串行化 (Serializable) |
不可能 |
|
不可能 |
不可能 |
举例:
假设同一个A和B两个同时并发操作数据库,A和B执行的任务如下:从数据库
中读取整数N,将N 随机加上10 或者20,将新的N 更新回数据库。这两个并发执行
的实例可能发生下面的执行顺序:
(1) A从DB中读取N,当前DB中N=0;
(2) B从DB中读取N,当前DB中N=0;
(3) A将N加10,更新DB,当前DB中的N=10;
(4) B将N加10,更新DB,当前DB中的N=20;
这里因为数据库出现了交叉存取的操作,B 所读取的N是过期的版本,即A在写回数据之前的版本。这样当B更新的时候,将会覆盖A的操作,这就是著名的“更新丢失”问题。那么应该如何避免这种情况的发生呢?
解决此类问题的方法就是为数据库加锁,以防止多个组件读取数据,通过锁住事务所用的数据,能保证在打开锁之前,只有本事务才能访问数据。这样就避免了交叉存取的问题。
由于锁将其他并发的事务排除在数据库更新之外,所以这会导致性能的严重下降。为了提高性能,事务将锁分为两种类型:只读锁和写入锁。只读锁是非独占的,多个并发的事务都能获得只读锁;写入锁是独占的,任意时间只能有一个事务可以获得写入锁。