事务概述
- 一组要么同时执行成功,要么同时失败的SQL语句。是数据库操作的一个不能分割执行单元。
- 数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。 事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。事务是数据库运行中的逻辑工作单位,由DBMS中的事务管理子系统负责事务的处理。
事务开始于
- 连接到数据库上,并执行一条DML语句insert、update或delete
- 前一个事务结束后,又输入了另一条DML语句
事务结束于
- 执行
commit
或rollback语
句。- 执行一条DDL语句,例如create table语句,在这种情况下,会自动执行commit语句。
- 执行一条DDL语句,例如grant语句,在这种情况下,会自动执行commit。
- 断开与数据库的连接
- 执行了一条DML语句,该语句却失败了,在这种情况中,会为这个无效的DML语句执行rollback语句。
事务的四大特点
Atomicity(原子性)
- 表示一个事务内的所有操作是一个整体,要么全部成功,要么全部失败
Consistency(一致性)
- 表示一个事务内有一个操作失败时,所有的更改过的数据都必须回滚到修改前状态
Isolation(隔离性)
- 事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。
Durability(持久性)
- 持久性事务完成之后,它对于系统的影响是永久性的。
事务隔离级别
- SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
Read Uncommitted(读取未提交内容)
- 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
- 脏读(Drity Read):某个事务已更新一份数据未提交前,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
- 解决方法:隔离级别设置为
Read Committed
,读取到的数据提交后的数据
- SQL代码示例(客户端sdh1):
#sdh1 #脏读 #查看隔离级别 SELECT @@tx_isolation; #修改隔离级别 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; #开始事务 START TRANSACTION; #查看数据 SELECT * FROM sdh1 WHERE id =1;
- SQL代码示例(客户端sdh2):
#sdh2 #查看隔离级别,默认`REPEATABLE READ`隔离级别 SELECT @@tx_isolation; #开始事务 START TRANSACTION; UPDATE sdh1 SET money=money+1000 WHERE id=1; UPDATE sdh1 SET money=money-1000 WHERE id=2; #提交 COMMIT; #回滚 ROLLBACK;
- 解释:当客户端sdh2开启事务,对表进行修改后。那么客户端sdh1查到的数据就位修改数据。然后客户端2这时不提交,使用回滚后。那么客户端1查到的数据就不准确。
- 解决方法:就是把隔离级别设置为
READ COMMITTED
即可.
Read Committed(读取提交内容)
- 这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别出现不可重复读(Nonrepeatable Read)问题,因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
Repeatable Read可重读
- 这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读(Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻读” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
Serializable 可串行化
- 这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上
共享锁
。在这个级别,可能导致大量的超时现象和锁竞争。效率最低的
。
- 这四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。
- 查看和设置隔离级别代码示例:
#修改事务的隔离级别: SET [SESSION||GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED||READ COMMITTED||REPEATABLE READ||SERIALIZABLE] #查看事务隔离级别 SELECT @@tx_isolation;
- 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。 不可重读读,需要设置隔离级别为 REPEATABLE READ
- 代码示例:
- SQL代码示例(客户端sdh1):
#sdh1 #查看隔离级别 SELECT @@tx_isolation; #修改隔离级别 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; #开始事务 START TRANSACTION; #不可重读读,需要设置隔离级别为REPEATABLE READ START TRANSACTION; SELECT SUM(money) FROM sdh1; SELECT SUM(money) FROM sdh1; SELECT SUM(money) FROM sdh1; COMMIT;
- SQL代码示例(客户端sdh2):
#sdh2 #查看隔离级别 SELECT @@tx_isolation; #修改隔离级别 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; #开始事务 START TRANSACTION; UPDATE sdh1 SET money=money+1000 WHERE id=1; UPDATE sdh1 SET money=money-1000 WHERE id=2; #提交 COMMIT;
- 解释:上面代码模仿了一个银行统计帐的案例,银行需要统计三次,三次结果一样时才会提交数据,否则将会出错。
- 所以当隔离级别比较低时,银行正在统计账单,但在此时一位用户存钱,那么这时银行统计账单就会出现不一致的情况。
- 解决方法:把隔离级别设置为Repeatable Read,也就是数据库默认的级别。
JDBC中事务应用
- JDBC连接默认处于
自动提交
模式,则每个SQL语句在完成后都会提交到数据库。
- 事务使您能够控制是否和何时更改应用于数据库。它将单个SQL语句或一组SQL语句视为一个逻辑单元,如果任何语句失败,则整个事务将失败。
- 要启用手动事务支持,而不是JDBC驱动程序默认使用的自动提交模式,请使用Connection对象的setAutoCommit()方法。如果将
boolean false传递给setAutoCommit()
,则关闭自动提交。我们可以传递一个布尔值true来重新打开它。
事务的提交和回滚
- 完成更改后,我们要提交更改,然后在连接对象上调用commit()方法,
- 代码示例:
connection.commit();
- 否则,要使用连接名为conn的数据库回滚更新.
- 代码示例:
connection.rollback();
Savepoint
- 新的JDBC 3.0 Savepoint接口为您提供了额外的事务控制。
- 设置保存点时,可以在事务中定义逻辑回滚点。如果通过保存点发生错误,则可以使用回滚方法来撤消所有更改或仅保存在保存点之后所做的更改。
- Connection对象有两种新的方法来帮助您管理保存点:
- setSavepoint(String savepointName):定义新的保存点。它还返回一个Savepoint对象。
- releaseSavepoint(Savepoint savepointName):删除保存点(该方法我测试时没有效果)。
请注意
,它需要一个Savepoint对象作为参数。此对象通常是由setSavepoint()方法生成的保存点。
- 代码示例:
Class.forName("com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/xxx", "xxx", "xxx"); Connection connection=null; Savepoint sp1=null; Savepoint sp2=null; Statement stat=null; try { //1获取连接 connection= DbUtils.getConnection(); //2开启事务 connection.setAutoCommit(false); //3创建命令对象 stat = connection.createStatement(); //4执行 stat.executeUpdate("insert into account(id,name,money) values(3,'aaa',1000)"); //创建保存点1 sp1=connection.setSavepoint("savepoint1"); stat.executeUpdate("insert into account(id,name,money) values(4,'bbb',1000)"); //创建保存点2 sp2=connection.setSavepoint("savepoint2"); stat.executeUpdate("insert into account(id,name,money) values(5,'ccc',1000)"); connection.releaseSavepoint(sp1); int i=10/0; //成功提交 connection.commit(); System.out.println("添加成功"); } catch (Exception e) { e.printStackTrace(); try { connection.rollback(sp1);//回滚到保存点 connection.commit();//回滚没有回滚到事务开始的位置,需要提交 System.out.println("添加失败"); } catch (SQLException e1) { e1.printStackTrace(); } }finally { try { stat.close(); connection.close(); } catch (SQLException e) { e.printStackTrace(); } }