10.8 事务处理
一、事务的概念和MySQL事务支持
1、概念:事务是由一步或几步数据库操作序列组成的逻辑单元,这系列的操作要么全部执行,要么全部放弃执行。程序和事务是两个不同的概念。一般而言,一段程序包含多个事务。
2、事务的四个特性(简称为ACID性):原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持续性(Durability)。
(1)原子性:事务是应用中最小的执行单位,就如原子是自然界最小颗粒,具有不可再分的特征一样,事务是应用中不可再分的最小逻辑执行体。
(2)一致性:事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成就被迫中断,而该未完成的事务对数据库所做的修改已被写入数据库。此时,数据库就处于一种不正确的状态。
(3)隔离性:各个事务的执行互不干扰,任意一个事务的内部操作对其他并发的事务都是隔离的。也就是说,并发执行的事务之间不能看到对方的中间状态,并发执行的事务之间不能互相影响。
(4)持续性:持续性也称为持久性,指事务一旦提交,对数据所做的任何改变都要记录到永久存储器中,通常就是保存进物理数据库。
3、数据库的事务由下列语句组成:
(1)一条DML语句,经过这组DML语句修改后的数据将保持较好的一致性。
(2)一条DDL语句。
(3)一条DCL语句。
DDL和DCL语句最多只有一条,因为DDL和DCL语句都会导致事务立即提交。
4、事务提交方式
当事务所包含的全部数据库操作都执行后,应该提交(commit)事务,这些修改永久生效。
★显式提交:使用commit
★自动提交:执行DDL或DCL语句,或者程序正常退出。
不管是显式提交还是隐式提交,都会结束当前事务。
5、事务回滚
当事务所包含的任意一个数据库操作执行失败后,应该回滚(rollback)事务,使事务中所做的修改全部失效。事务回滚有两种方式:
★显式回滚:使用rollback。
★自动回滚:系统错误或者强行退出。
不管是显式回滚还是隐式回滚,都会结束当前事务。
7、关闭自动提交、开启事务
MySQL默认关闭事务(即打开自动提交),在默认情况下,用户在MySQL控制台输入一条DML语句,这条DML语句将会立即保存到数据库里。为了开启MySQL事务支持,可以显式调用如下命令:
1 SET AUTOCOMMIT={0|1} 0为关闭自动提交,即开启事务
提示:自动提交和事务开启刚好相反,如果开启自动提交就是关闭事务;关闭自动提交就是开启事务。
一旦在MySQL命令行窗口输入SET AUTOCOMMIT=0开启事务,该命令行窗口里所有DML语句都不会立即生效,上一个事务结束后第一条DML语句将开始一个新的事务,而后续执行的所有SQL语句都处在该事务中,除非显式使用Commit来提交事务,或者正常退出,或者运行DDL、DCL语句导致事务隐式提交。当然也可以使用rollback回滚来结束事务,使用rollback结束事务将导致本次DML语句所做的修改全部失效。
一个MySQL命令窗口代表一次连接Session,在该窗口设置SET AUTOCOMMIT=0相当于关闭该连接Session的自动提交,对其他连接不会有影响。
如果不想关闭整个命令窗口的自动提交,而只是想暂时性地开始一次事务,则可以使用MySQL提供的star transaction或begin两个命令,它们都表示临时性地开始一次事务,处于star transaction或begin后的DML语句不会立即生效,除非使用commit显式提交事务,或者执行DDL、DCL语者来隐式提交事务。
如下SQL语句不会对数据库产生任何影响:
1 #临时开启事务 2 begin; 3 #向student_table1插入三条记录 4 insert into student_table1 5 values(null,'xx',1); 6 insert into student_table1 7 values(null,'yy',1); 8 insert into student_table1 9 values(null,'zz,1); 10 #查询student_table1表的记录 11 select *from student_table1;#① 12 #回滚事务 13 rollback; 14 #再次查询 15 select * from student_table1 ;#②
上面的第①条查询语句将会看到刚刚插入的3条记录,如果打开MySQL的其他命令窗口将看不到这三条记录——这正体现了事务的隔离性。接着程序rollback了事务中的全部修改,执行第②条查询语句将看到数据库又恢复到了事务开始前的状态。
不管是显式提交还是隐式提交,都会结束当前事务;不管是显式回滚还是隐式回滚,都会结束当前事务。
除此之外,MySQL还提供了savepoint来设置事务的中间点,通过使用savepoint设置事务中间点还可以让事务回滚到指定中间点,而不是回滚全部事务。如下SQL语句设置了一个中间点:
1 savapoint a;
回滚到指定中间点:
1 rollback a;
普通的提交、回滚都会结束当前事务,但回滚到指定中间点因为依然处于事务中,所以不会结束当前事务。
二、JDBC事务支持
JDBC也提供了事务支持,JDBC连接的事务支持由Connection提供,Connection默认打开自动提交,即关闭事务,在这种情况下,每条SQL语句一旦执行,并会立即提交到数据库中,永久有效,无法对其进行回滚操作。
1、关闭自动提交,开启事务
可以调用Connection的setAutoCommite()方法来关闭自动提交,开启事务,代码如下:
1 //关闭自动提交,开启事务 2 conn.setAutoCommit(false);
2、返回连接的自动提交模式
调用Connection提供的getAutoCommit()方法来返回该连接的自动提交模式。
3、提交事务
一旦事务开始以后,程序就可以像平常一样创建Statement对象,创建Statement对象之后,可以执行任意多条DML语句,代码如下:
1 stmt.executeUpdate(...); 2 stmt.executeUpdate(...); 3 stmt.executeUpdate(...);
上面的SQL语句虽然被执行,但这些语句所做的修改不会生效,因为事务还未结束。如果所有的SQL语句都执行成功,程序可以调用Connection的commit()方法来提交事务,代码如下:
1 //提交事务 2 conn.commit();
4、回滚事务
如果任意一条SQL语句执行失败,则应该使用Connection的rollback()方法来回滚事务。
1 #回滚事务 2 conn.rollback();
下面程序示范了当程序出现了未处理的SQLException异常时,系统将自动回滚事务。实际上,当Connection遇到一个未处理的SQLException异常时,系统会非常正常退出,事务也会自动回滚。但如果程序不活了该异常,则需要在程序中显式回滚事务。
1 package section7; 2 3 import java.io.FileInputStream; 4 import java.sql.Connection; 5 import java.sql.DriverManager; 6 import java.sql.Statement; 7 import java.util.Properties; 8 9 public class TransactionTest 10 { 11 private String driver; 12 private String url; 13 private String user; 14 private String pass; 15 public void initParam(String paramFile) 16 throws Exception 17 { 18 //使用Properties类加载属性 19 Properties props=new Properties(); 20 props.load(new FileInputStream(paramFile)); 21 driver=props.getProperty("driver"); 22 user=props.getProperty("user"); 23 url=props.getProperty("url"); 24 pass=props.getProperty("pass"); 25 } 26 public void insertInTransaction(String[] sqls) 27 throws Exception 28 { 29 //加载驱动 30 Class.forName(driver); 31 try( 32 Connection conn = DriverManager.getConnection(url,user,pass); 33 ) 34 { 35 //关闭自动提交 36 conn.setAutoCommit(false); 37 try( 38 //使用Connection创建一个Statementd对象 39 Statement stmt=conn.createStatement() 40 ) 41 { 42 //循环多次执行SQL语句 43 for(var sql:sqls) 44 { 45 stmt.executeUpdate(sql); 46 47 } 48 } 49 //提交事务 50 conn.commit(); 51 } 52 } 53 public static void main(String[] args) 54 throws Exception 55 { 56 var tt=new TransactionTest(); 57 tt.initParam("src\\mysql.ini"); 58 var sqls=new String[]{ 59 "insert into student_table values(null,'aaa',1);", 60 "insert into student_table values(null,'bbb',1);", 61 "insert into student_table values(null,'ccc',1);", 62 //下面这条语句将会违反外键约束 63 //因为teacher_table表中没有ID为5的记录 64 "insert into student_table1 values(null,'aaa',5);"//① 65 }; 66 tt.insertInTransaction(sqls); 67 } 68 }
上面程序中执行到第4条sql语句时,这条语句将出现外键约束异常,该异常没有得到处理,引起程序的非正常结束,所事务自动回滚。
Connection也提供了设置中间点的方法:
1、Savepoint setSavePoint():在当前事务中创建一个未命名的中间点,并返回该中间点的Savepoint 对象。
2、Savepoint setSavePoint(String name):在当前事务中创建一个指定名称的中间点,并返回该中间点的Savepoint 对象。(没有太大必要指定中间点名称)
Connection提供rollback(Savepoint savepoint)方法回滚到指定中间点。
三、使用批量更新
JDBC还提供了一个批量更新功能,使用批量更新时,多条SQL语句将被作为一批操作被同时收集,并同时提交。
提示:批量更新批量必须得到底层数据库的支持,可以通过调用MetabaseMetaData的supportsBatchUpdates()方法来查看底层数据库是否支持批量操作。
批量跟新步骤:
①先创建一个Statement对象,然后利用该对象的addBatch()方法将多条SQL语句收集起来。
②最后调用Java 8为Statement对象新增的executeLargeBatch()(或原有的executeBatch())方法同时执行这些SQL语句。
下面代码示范了如何执行这些批量更新:
1 Statement stmt=conn.createStatement(); 2 //使用Statement同时收集多条SQL语句 3 stmt.addBatch(sql1); 4 stmt.addBatch(sql2); 5 stmt.addBatch(sql3); 6 ... 7 //同时执行所有的SQL语句 8 stmt.executeLargeBatch();
执行executeLargeBatch()方法将返回一个long[]数组,因为使用Statement执行DDL、DML语句都将返回一个long值,而执行多条DDL、DML语句将返回多个long值,多个long值就组成这个long[]数组。如果在批量更新的addBatch()方法中添加了select查询语句,程序将直接出现错误。
为了让批量操作可以正确地处理错误,必须把批量执行的操作视为单个事务,如果批量更新在执行过程中失败,则让事务回滚到批量操作开始之前的状态。为了达到这种效果,程序应该在开始批量操作之前先关闭自动提交,然后开始收集更新语句,当批量操作执行结束后,提交事务,并恢复之间的提交模式。如下代码示范了JDBC的批量更新:
1 package section7; 2 3 import java.io.FileInputStream; 4 import java.sql.Connection; 5 import java.sql.Driver; 6 import java.sql.DriverManager; 7 import java.sql.Statement; 8 import java.util.Properties; 9 10 public class BatchTest 11 { 12 private String driver; 13 private String url; 14 private String user; 15 private String pass; 16 public void initParam(String paramFile) 17 throws Exception 18 { 19 //使用Properties类加载属性 20 Properties props=new Properties(); 21 props.load(new FileInputStream(paramFile)); 22 driver=props.getProperty("driver"); 23 user=props.getProperty("user"); 24 url=props.getProperty("url"); 25 pass=props.getProperty("pass"); 26 } 27 public void insertBatch(String[] sqls) 28 throws Exception 29 { 30 //加载驱动 31 Class.forName(driver); 32 try( 33 Connection conn= DriverManager.getConnection(url,user,pass) 34 ) 35 { 36 //保存当前的自动提交模式 37 boolean autoCommit=conn.getAutoCommit(); 38 //关闭自动提交 39 conn.setAutoCommit(false); 40 try( 41 //使用Connection创建一个Statement对象 42 Statement stmt=conn.createStatement() 43 ) 44 { 45 //for循环多次执行SQL语句 46 for(var sql:sqls) 47 { 48 stmt.addBatch(sql); 49 } 50 //提交所有的SQL语句 51 stmt.executeLargeBatch(); 52 //提交修改 53 conn.commit(); 54 //恢复原有的自动提交模式 55 conn.setAutoCommit(autoCommit); 56 } 57 } 58 } 59 public static void main(String[] args) 60 throws Exception 61 { 62 var bt=new BatchTest(); 63 bt.initParam("src\\mysql.ini"); 64 var sqls=new String[]{ 65 "insert into student_table values(null,'aaa',1)", 66 "insert into student_table values(null,'bbb',1)", 67 "insert into student_table values(null,'ccc',1)" 68 }; 69 bt.insertBatch(sqls); 70 } 71 }