数据库事务(Database Transaction)
是指作为单个逻辑工作单元执行的一系列数据操作,要么完全地执行,要么完全地不执行。事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。事务是数据库运行中的一个逻辑工作单位,由DBMS中的事务管理子系统负责事务的处理。
简而言之数据库是我们操作数据库时一系列执行操作的控制单元,当我们只执行一条sql语句时并没有感觉到事务的作用,但一旦需要多条sql配合使用时,我们往往希望所有的sql语句都能够正常执行,全部执行成功后能够得到预期的结果,如果失败则不希望产生什么副作用,此时的数据库就像没有操作过一样。也就是说通过事务把几条sql 绑到了一起,把它们看成了一个整体。
已以下例子为例,我们有一张学生表:
其中STUDENT 表中包含几个基本字段,用来表示主键,姓名,班级名称,学号。
1.当学校开学时会有新的一年级同学入学,这样就会在STUDENT 表中添加新学生的信息:
- insert into STUDENT (STUDENT_ID, NAME, CLASS, "NO") values (1, '小明', '一年级一班', 1);
- insert into STUDENT (STUDENT_ID, NAME, CLASS, "NO") values (2, '小红', '一年级一班', 2);
- insert into STUDENT (STUDENT_ID, NAME, CLASS, "NO") values (3, '小强', '一年级一班', 3);
- insert into STUDENT (STUDENT_ID, NAME, CLASS, "NO") values (4, '小胖', '一年级一班', 4);
正常情况下一年级一班有4个座位,可以容纳4名新同学,开学报到的那天,小强却没来,于是老师没办法只好先跳过小强让其他同学入学。此时班级里只有3名同学:小明,小红,小胖。
第二天小强来了,于是老师给他办理了入学手续,小强也顺利的加入了一年级一班,这样一年级一班就已经坐满了,已经成功加入了4名新同学。
2.就这样大家愉快的度过了1个学期,一年级下学期的时候小胖要转校了,于是老师又招收了新的同学“小花”,当小胖离开后就把小胖的座椅学号等都留给小花用,于是就有了下面的sql 语句:
- delete from STUDENT where NAME='小胖';
- insert into STUDENT (STUDENT_ID, NAME, CLASS, "NO") values (4, '小花', '一年级一班', 4);
可是此时又出现了问题,小胖要晚几天才能退学,可是小花却已经来报到了,小胖还占着教室里的位置,小花就没法加入新的班级上课。
所以老师想要的最终结果就是小胖转学,空出位置,小花加入班级。
3.此时小花的姑姑出来解决问题了,她要协调这件事情,姑姑要保证小胖转走还必须保证小花能正常来报到入学。小胖不转走侄女小花就不能入学,姑姑跟小胖家长商量让小胖先转走,而小花如果不来上学那自己岂不是白忙活,于是姑姑两件事必须一块办并且结果大家都得满意,此时姑姑的角色就是事务。
总结:在刚开学的时候新同学报到,此时难免会有没法准时报到的同学,所以老师就不能等所有人都到齐了再给分配座位入学,即使有未到的老师只需要预留或者跳过即可,此时老师的角色就不是事务。当下学期时,小胖转学小花入学,姑姑必须保证侄女小花能正常进入班级学习,所以她需要协调小胖的家长让小胖先退学,然后把小花送进班级学习,姑姑必须把两件事全部完成,所以姑姑的角色就是事务。
ACID特性
之前已经提到一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)这四个属性,下面就分别了解下ACID属性:
原子性(Atomic、Atomicity)
事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。通常,与某个事务关联的操作具有共同的目标,并且是相互依赖的。如果系统只执行这些操作的一个子集,则可能会破坏事务的总体目标。原子性消除了系统处理操作子集的可能性。
原子性也就是要视同一事务中所有的操作为一整体,就像原子一样不能再分,要么成功要么失败没有其他结果。
小花的姑姑就必须保证小胖转学,小花入学上课,两者为一整体,就是原子性的体现。
一致性(Consistent、Consistency)
事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构(如 B 树索引或双向链表)都必须是正确的。某些维护一致性的责任由应用程序开发人员承担,他们必须确保应用程序已强制所有已知的完整性约束。例如,当开发用于转帐的应用程序时,应避免在转帐过程中任意移动小数点。
所谓一致性就是事务需要保证事务结束后结果的一致和正确性。
姑姑忙活了半天小胖也没转走,小花还进错了班级,那姑姑还不得哭死,所以结果与预期一致是一致性的体现。
隔离性(Insulation、Isolation)
由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。称为隔离性,因为它能够重新装载起始数据,并且重播一系列事务,以使数据结束时的状态与原始事务执行的状态相同。当事务可序列化时将获得最高的隔离级别。在此级别上,从一组可并行执行的事务获得的结果与通过连续运行每个事务所获得的结果相同。由于高度隔离会限制可并行执行的事务数,所以一些应用程序降低隔离级别以换取更大的吞吐量。
隔离性体现了多个事务间是否可以相互察觉,是否可以相互影响,隔离级别越高影响程度越小。隔离级别分为:读未提交、读已提交、可重复读取、序列化。序列化隔离级别最高。
隔离级别好似姑姑与小花的老师同时都在为小胖和小花的事忙碌着,当隔离级别比较低的时候姑姑和老师都知道对方在做事,并且都了解到了对方的部分操作结果。而姑姑和老师约定彼此按顺序来,姑姑先忙这件事,然后老师再来,这样不至于都白忙活还办不成事,这就是序列化隔离级别。
持久性(Duration、Durability)
事务完成之后,它对于系统的影响是永久性的。该修改即使出现致命的系统故障也将一直保持。
持久性就是将事务完成后的操作结果持久保持,结果不会因为其他原因而失效。
姑姑忙活了一天终于把转学申请,入学申请都拿到了手,并且上面已经盖上了大大的红色公章,这下姑姑终于可以休息休息了。俩张申请单就是姑姑这一天忙碌的最终结果,它们被不会有任何改变了。
数据库事务必须遵循这四个原则,缺一不可。
数据库隔离级别
之前还提到了多个事务之间具有隔离级别,事务间的资源或数据更改相隔离的程度称为事务的隔离级别。在标准SQL规范中,定义了4种事务隔离级别,分别为:读未提交,读已提交,可重复读取,序列化,从左至右级别依次提高。
读未提交(Read Uncommitted):允许脏读取,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。
读已提交(Read Committed):允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。
可重复读取(Repeatable Read):禁止不可重复读取和脏读取,但是有时可能出现幻读数据。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。
序列化(Serializable):提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为可重复读或读已提交。
事务的隔离级别设定是为了多事务并发运行的情况下达到数据隔离和数据一致的目的,还是用姑姑和老师的例子举例,姑姑和老师同时来到校长办公室办理各自的申请(此时老师给其他人办理入学,姑姑给小花办理入学),当设置隔离级别较低时,她们同时进入了校长办公室,并且校长同时接待了她们,此时她们就很清楚的了解到了对方各自所办事情的状况。当设置隔离级别较高时,校长没办法同时接待两个人,所以只能一个一个排队进入办公室办理,此时大家彼此就不会了解到各自都要办理那些事情了。当然例子比较极端,但表达意思应该比较容易理解。
下面我们来看不同的隔离级别会产生什么怎样不同的数据隔离效果
1.读未提交:在事务级别设定为读未提交的级别时,数据库事务间的数据程度最低,此时会产生脏读,幻读,不可重复读的现象。
脏读就是多个事务同时执行时,一事务对数据进行了增删改操作,但未提交该事务,该事务还有可能回滚操作,此时另一事务却读取了未提交的数据,被读取的数据就是脏数据是不一定正确的。所以此种情况被称为脏读(以转账取款两个事务为例)。
时间
|
转账事务A
|
取款事务B
|
1
|
|
开始事务
|
2
|
开始事务
|
|
3
|
|
查询账户余额为100元
|
4
|
|
取出50元把余额改为50元
|
5
|
查询账户余额为50元(脏读)
|
|
6
|
|
撤销事务余额恢复为100元
|
7
|
汇入100元把余额改为150元
|
|
8
|
提交事务
|
|
不可重复读是在同一事务中做多次相同查询,而多次查询的结果却不同。这是因为在几次查询的过程中,系统中其他事务修改了所查询数据,并将事务提交所造成的。不可重复读重点在于无法重读读取相同的数据,因为每次读取的数据值都是有可能不同的,而这条数据依然存在,只是值有可能改变。
时间
|
取款事务A
|
转账事务B
|
1
|
|
开始事务
|
2
|
开始事务
|
|
3
|
|
查询账户余额为100元
|
4
|
查询账户余额为100元
|
|
5
|
|
取出10元把余额改为90元
|
6
|
|
提交事务
|
7
|
查询账户余额为90元(和4读取的不一致)
|
|
幻读也是在同一事务中做多次相同查询,而多次查询的结果却不同。但是幻读的重点在于该事务几次查询过程中,系统中其他事务对数据进行了删除或者修改操作,并提交了事务。此时之前查询的事务几次查询的结果集就会不同,不是多就是少,仿佛出现了幻觉一样。幻读与不可重复读的区别在于幻读重点在于查询的结果集不同,而不可重复读重点在于查询结果的值不同。
时间
|
统计金额事务A
|
转账事务B
|
1
|
|
开始事务
|
2
|
开始事务
|
|
3
|
统计总存款数为10000元
|
|
4
|
|
新增一个存款账户,存款为100元
|
5
|
|
提交事务
|
6
|
再次统计总存款数为10100元(幻象读)
|
|
2.读已提交:当事务隔离级别设置为读已提交时可以避免脏读的发生,但是依然会出现不可重复读和幻读现象。从字面理解上可以理解为此时的事务可以读取其他事务已经提交的数据,而读未提交则为可以读取其他事务未提交的数据。为了避免脏读的发生,可以通过临时共享锁和排它锁来实现。
共享锁(share Locks):共享锁又称为读锁(简称为S锁),若事务T对数据对象A加上共享锁,则事务T只能读A;其他事务只能再对A加共享锁,而不能加排他锁,直到T释放A上的共享锁。这就保证了其他事务可以读A,但在T释放A上的共享锁之前不能对A做任何修改。也就是说被共享锁锁定的数据其他事务只能读取。
排他锁(exclusive locks):排他锁又称为写锁(简称为X锁),若事务T对数据对象A加上排他锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的排他锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。
3.可重复读:可重复读是比较常用的隔离级别,该级别下避免了脏读和不可重复读,但依然会出现幻读,可重复读下通过共享锁和排它锁来实现。
4.序列化:序列化是最高的事务隔离级别,在此级别下所有事务以串行方式运行,也就是按一个序列排队执行,序列化级别下事务是无法并发执行的,是比较严格的隔离级别,一般并不常用,因为设定此级别时执行效率较低,所以使用时需要慎重考虑。该级别下无脏读,不可重复读,幻读的现象。
不同隔离级别下数据的读取情况
隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交 | 是 | 是 | 是 |
读已提交 | 否 | 是 | 是 |
可重复读 | 否 | 否 | 是 |
序列化 | 否 | 否 | 否 |
下图是一张动态图片(来源网络),可以很清晰的了解到锁在不同隔离级别下锁的应用:
事务的类型
事务一般分为显式事务、隐式事务、自动提交事务几种或更多种,但这几种是最常见也是最常用的事务类型。
显式事务:显式事务是指有显式的开始和结束标记的事务,每个显式事务都有显式的开始和结束标记。
显式事务的事务开始必须以BEGIN TRANSACTION 或 BEGIN DISTRIBUTED TRANSACTION开始,并以COMMIT TRANSACTION、COMMIT WORK、ROLLBACK TRANSACTION、ROLLBACK WORK、SAVE TRANSACTION其一结束。
隐式事务:隐式事务是指每一条SQL操作语句都自动地成为一个事务,事务的开始是隐式的,事务的结束有明确的标记。
自动事务:自动事务一般为系统自动默认,开始和结束都不用标记。
以JDBC为例,以下是一个简单的事务使用的代码示例:
- Connection con = null;
- PreparedStatement ps = null;
- String url = "数据库地址";
- String user = "用户名";
- String password = "密码";
- try {
- Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver");
- con = DriverManager.getConnection(url, user, password);
- // 1.设置事务的提交方式为非自动提交:
- con.setAutoCommit(false);
- // 创建执行语句
- String sql1 = "";
- String sql2 = "";
- String sql3 = "";
- // 2,分别执行事务
- ps = con.prepareStatement(sql1);
- ps.executeUpdate();
- ps = con.prepareStatement(sql2);
- ps.executeUpdate();
- ps = con.prepareStatement(sql3);
- ps.executeUpdate();
- // 3.提交事务
- con.commit();
- } catch (SQLException e) {
- try {
- // 3.操作失败时回滚事务
- con.rollback();
- } catch (SQLException e1) {
- e1.printStackTrace();
- }
- e.printStackTrace();
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- try {
- // 4.设置事务提交方式为自动提交:
- con.setAutoCommit(true);
- ps.close();
- con.close();
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }