事务通用概念
为什么需要事务?
在许多大型、关键的应用程序中,计算机每秒钟都在执行大量的任务。更为经常的不是这些任务本身,而是将这些任务结合在一起完成一个业务要求,称为事务。如果能成功地执行一个任务,而在第二个或第三个相关的任务中出现错误,将会发生什么?这个错误很可能使系统数据处于不一致状态。这时事务变得非常重要,它能使系统数据摆脱这种不一致的状态。 如何理解事务呢?例如在某家银行的银行系统中,如果没有事务对数据进行控制和管理,很可能出现 A 从企业账户中取出一笔钱,同时 B 和 C 也从同一企业账户中取钱。每一笔转账涉及到最少两个账户信息的变化(例如,A 的钱到账,企业账户出账;B 的钱到账,企业账户出账;C 的钱到账,企业账户出账),如果没有事务,那么账面金额的具体数值将无法确定。在引入事务这一业务要求之后,事务的基本特性(ACID)确保了银行账面的资金操作是原子性(不可再分割)的,其他人看到的金额是具备隔离性的,每一次操作都是具有一致性的,所有操作是持久性的,这样保证了银行系统数据出入账保持一致。
什么是事务?
数据库事务(即,Transaction,事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。 事务就是作为一个逻辑单位提交或者回滚一系列的 SQL 语句。
事务的特征
通常事务需要具备 ACID 四个特征:
- 原子性(Atomicity):事务的原子性是指事务是一个不可分割的单位,在一个事务中的操作要么都发生,要么都不发生。
例如,我们现在有如下事务:
start transaction;
insert into t1 values(1,2,3),(4,5,6);
update t2 set c1='b' where c1='B';
commit;
如果对 t1 插入数据或修改 t2 数据中的任意一条发生错误,整个事务都会回滚,而只有两条语句同时成功时,才会提交成功,不会出现一个操作成功而另一个操作失败。
- 一致性(Consistency):事务的一致性是指在事务前后,数据必须是保持正确并遵守所有数据相关的约束。
例如,我们在数据库中建立一个新表
create table t1(a int primary key,b varchar(5) not null);
此处为了确保数据一致性,我们在插入数据时,你要保证 a 和 b 列的数据类型与范围,同时还要满足 a 列的主键约束与 b 列的非空约束:
insert into t1 values(1,'abcde'),(2,'bcdef');
- 隔离性(Isolation):事务的隔离性是在多个用户并发访问时,事务之间要遵守规定好的隔离级别,在确定的隔离级别范围内,一个事务不能被另一个事务所干扰。
例如,如下事务示例,会话隔离级别是读已提交,在会话 1 能看到的数据如下:
select * from t1;
+------+------+
| a | b |
+------+------+
| 1 | a |
| 2 | b |
| 3 | c |
+------+------+
此时在会话 2 中,做如下操作:
begin;
delete from t1 where a=3;
在会话 1 中,你可以看到的数据仍然不会有变化:
select * from t1;
+------+------+
| a | b |
+------+------+
| 1 | a |
| 2 | b |
| 3 | c |
+------+------+
直到在会话 2 中提交当前事务:
commit;
在会话 1 中才会看到已提交事务的结果:
select * from t1;
+------+------+
| a | b |
+------+------+
| 1 | a |
| 2 | b |
+------+------+
- 持久性(Durability):事务的持久性是指,在数据库中一个事务被提交时,它对数据库中的数据的改变是永久性的,无论数据库软件是否重启。
事务的分类
在数据库中,事务分为以下几类:
- 按照是否有明确的起止分为显示事务和隐式事务。
- 按照对资源锁的使用阶段分为乐观事务和悲观事务。
这两大类事务的分类彼此不受对方限制,一个显式事务可以是乐观事务或悲观事务,同时一个悲观事务可能是显式事务也可能是隐式事务。
显式事务和隐式事务
-
显式事务:一般来说,可以通过执行 BEGIN 语句显式启动事务。可以通过执行 COMMIT 或 ROLLBACK 显式结束事务。MatrixOne 的显示事务启动和执行方式略有不同,可以参见 MatrixOne 的显式事务。
-
隐式事务:即事务可以隐式开始和结束,无需使用 BEGIN TRANSACTION、COMMIT 或者 ROLLBACK 语句就可以开始和结束。隐式事务的行为方式与显式事务相同。但是,确定隐式事务何时开始的规则不同于确定显式事务何时开始的规则。MatrixOne 的隐式事务启动和执行方式略有不同,可以参见 MatrixOne 的隐式事务。
乐观事务和悲观事务
无论是乐观事务,还是悲观事务,其事务的执行结果都是一样的,即一个事务中的操作,对 ACID 级别的要求,完全一样,无论是原子性、一致性、隔离性或者持久性,都是完全一致,不存在乐观事务就宽松一些,悲观事务就严格一些的情况。
乐观事务与悲观事务的区别,它只是两阶段提交基于待处理业务状态的不同执行策略,其选择基于执行者的判断,其效率高低基于被处理业务的实际状态(并发事务的写冲突频繁度)。即在于对于事务相关资源的状态做出不同的假设,从而将写锁放在不同的阶段中。
在乐观事务开始时,会假定事务相关的表处于一个不会发生写冲突的状态,把对数据的插入、修改或删除缓存在内存中,在这一阶段不会对数据加锁,而在数据提交时对相应的数据表或数据行上锁,在完成提交后解锁。
而在悲观事务中,一个事务在开始时,会假定事务相关的表一定会发生写冲突,预先对相关表或行加锁。然后才在内存中,对相关数据进行插入、修改或删除并提交。只有在提交或回滚完成后才对数据进行解锁。
乐观事务与悲观事务在使用过程中,有着如下的优缺点:
- 乐观事务对于写操作较少,读操作较多的系统更为友好,避免了死锁的出现。
- 乐观事务在较大的事务提交时,可能会因为出现冲突导致反复重试却最终失败。
- 悲观事务对于写操作较多的系统更加友好,从数据库层面避免了写写冲突。
- 悲观事务在并发较大的场景下,如果出现一个执行时间较长的事务,可能会导致系统阻塞并影响吞吐量。
MatrixOne 的乐观事务详情可以参见 MatrixOne 的乐观事务。
MatrixOne 的悲观事务详情可以参见 MatrixOne 的悲观事务。
事务隔离
关于事务特征中有一条是隔离性,我们通常称之为事务隔离。
在数据库事务的 ACID 四个属性中,隔离性是一个限制最宽松的。为了获取更高的隔离等级,数据库系统通常使用锁机制或者多版本并发控制机制。应用软件也需要额外的逻辑来使其正常工作。
很多数据库管理系统(DBMS)定义了不同的“事务隔离等级”来控制锁的程度。在很多数据库系统中,多数的事务都避免高等级的隔离等级(如可串行化)从而减少锁的开销。程序员需要小心的分析数据库访问部分的代码来保证隔离级别的降低不会造成难以发现的代码错误。相反的,更高的隔离级别会增加死锁发生的几率,同样需要在编程过程中去避免。
由于更高的隔离级别中不存在被一个更低的隔离级别禁止的操作,DBMS 被允许使用一个比请求的隔离级别更高的隔离级别。
ANSI/ISO SQL 定义的标准隔离级别共有四个:
隔离级别 | 脏写 (Dirty Write) | 脏读 (Dirty Read) | 不可重复读 (Fuzzy Read) | 幻读 (Phantom) |
---|---|---|---|---|
READ UNCOMMITTED | Not Possible | Possible | Possible | Possible |
READ COMMITTED | Not Possible | Not Possible | Possible | Possible |
REPEATABLE READ | Not Possible | Not Possible | Not Possible | Possible |
SERIALIZABLE | Not Possible | Not Possible | Not Possible | Not Possible |
-
读未提交:读未提交(READ UNCOMMITTED)是最低的隔离级别。允许“脏读”(dirty reads),事务可以看到其他事务“尚未提交”的修改。
-
读已提交:读已提交(READ COMMITTED)级别中,基于锁机制并发控制的 DBMS 需要对选定对象的写锁一直保持到事务结束,但是读锁在 SELECT 操作完成后马上释放。和前一种隔离级别一样,也不要求“范围锁”。
-
可重复读:在可重复读(REPEATABLE READS)隔离级别中,基于锁机制并发控制的 DBMS 需要对选定对象的读锁(read locks)和写锁(write locks)一直保持到事务结束,但不要求“范围锁”,因此可能会发生“幻读”。MatrixOne 实现了快照隔离(即 Snapshot Isolation),为了与 MySQL 隔离级别保持一致,MatrixOne 快照隔离又叫做可重复读(REPEATABLE READS)。
-
可串行化:可串行化(SERIALIZABLE)是最高的隔离级别。在基于锁机制并发控制的 DBMS 上,可串行化要求在选定对象上的读锁和写锁直到事务结束后才能释放。在 SELECT 的查询中使用一个“WHERE”子句来描述一个范围时应该获得一个“范围锁”(range-locks)。
通过比低一级的隔离级别要求更多的限制,高一级的级别提供更强的隔离性。标准允许事务运行在更强的事务隔离级别上。
Note
MatrixOne 的事务隔离与通用的隔离定义的隔离级别的划分略有不同,可以参见 MatrixOne 的隔离级别。