事务
- 定义:
事务是指数据库中的一组逻辑操作,这个操作的特点就是在该组逻辑中,所有的操作要么全部成功,要么全部失败。在各个数据具有特别紧密的联系时,最好是使用数据库的事务来完成逻辑处理。
说的通俗点就是,我们执行一个事件时,光靠简简单单的一句sql语句是无法完成的,这时候我们就需要多条语句组合完成,但是我们不知道在哪一句sql执行时会发生错误,这时就必须用到事务;使得多条sql语句成为一个整体,只有它们全部都执行成功时,更改才会发生,否则就不成功,回滚到最初的状态;
这里我们来看一个例子,以帮助我们更好的理解事务;
例如路人甲给路人乙转账1000元,对应于如下两条SQL命令;
update money set balance=balance-1000 where name=’甲’;
update money set balance=balance+1000 where name=’乙’;
在上面两条SQL语句中,任意一条SQL执行过程中出现了错误,那么就有可能造成甲与乙两人最后总金额的错误。
但如果是使用事务来处理,即使上面的转账过程出现了错误,那么之前执行的数据库操作即使成功也会一并回滚,形成所有的SQL操作全部失败,保证所有人的金额不变;
- 事务执行语句:
在我们之前的sql语句执行时,我们好像没有使用事务,但其实在我们刚刚使用MySQL数据库的时候我们就已经在使用事务了;我们每执行一条sql语句,MySQL都会默认给我们开启一个事务,并且执行完毕后自动的提交,但其实事务的开启、提交、回滚都有特定的语句的;
事务开启: start transaction/begin;
事务提交:commit;
事务回滚:rollback;
那如果我们就要完成上面的例子,实现转账操作,用事务来做的代码就是这样:
比如甲,乙 两人的账户里本来就一人只有1000块,现在甲要给乙转账1000元;
开启事务,模仿中间发生错误;
首先甲向乙转账1000元;
start transaction;
update money set balance=balance-1000 where name=’甲’;
这时候我们看到事务正常开始,并执行了甲账户的钱转账出去的操作;
可比如我们这个时候发生了错误,我们用关闭MySQL模拟错误;
然后我们再回头查询甲乙的账户;
我们发现由于我们没有进行 commit(提交) 操作;MySQL自动帮我们rollback(回滚) 了,之前执行的SQL语句全部不算成功,因此即使数据库可能发生了错误,用户金额还是能由事务保证不出意外;
开启事务,成功完成转账;
这时我们要完成整个甲向乙转账的操作:
start transaction;
update money set balance=balance-1000 where name=’甲’;
update money set balance=balance+1000 where name=’乙’;
commit;
当上面的commit(提交)命令执行了,那就说明我们的整个操作完成了,这时就算我们关闭MySQL,模拟错误发生,回来再查询,甲乙两人的balance(余额)也已经发生了改变;
rollback(回滚)
回滚就时将之前所有已执行的SQL全部视为无效;
也就是说再没有执行commit(提交)命令之前,不管我们前面做了多少的增删改操作,一句rollback(回滚),之前的命令全部作废,而好就好在MySQL在我们发生错误时,会自动回滚,帮我们保存原来的状态,以防止数据的错误;
使用rollback命令会回滚该事务内所有之前执行的SQL命令,不会只回滚前面一条SQL命令,因此即使我们对A和B的金额操作了多次,最终还是回到事务开启前的金额数:
JDBC操作数据库事务;
上面的事务操作我们都是在mysql环境下操作的,现在我们学习一下在Java环境下,即jdbc操作数据库事务;
上面我们说过数据库对于事务是默认自动提交的,也就是发一条SQL命令则数据库就执行一条。而对于JDBC而言,当向数据库获取一个链接Connection对象,在默认情况下通过Connection对象发送的SQL命令也是默认自动提交事务的。那我们要模仿事务的开启、提交、回滚等就必须关闭 jdbc 的自动提交功能。
- 于是我们首先需要用 setAutoCommit(false); 关闭自动提交,同时这句语句也相当于 start transaction,开启事务的语句了;
- 然后我们执行一些sql语句,在JDBC已经将自动提交关闭的情况下需要提交事务,则调用Connection对象的commit()方法即可。
- 在JDBC已经将自动提交关闭的情况下需要回滚事务,则调用Connection对象的rollback(…)方法即可。
rollback方法如果是无参,则回滚前面所有已执行的SQL命令;如果是有参,则可以指定回滚点,保留前面部分指定的已执行的SQL命令。
了解了jdbc操作数据库的基本操作流程,下面我们就来看看具体代码和演示结果;
首先我们要和数据库建立连接,这里我们将取得连接的方法写成一个工具类,每次获得Connection的对象即可;
//driver=com.mysql.jdbc.Driver
static final String URL= "jdbc:mysql://localhost:3306/test";
static final String USERNAME = "root";
static final String PASSWORD = "root";
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USERNAME, PASSWORD);
}
这里有一个注册驱动的语句,在jdk 1.8以后,注册驱动这个操作就可以省去了,idea可以自动识别对应驱动;
然后我们就来尝试用jdbc来操作数据库的事务;
我们就以上面甲给乙转账1000 元做为例子来说吧:
首先我们还是准备好原始的数据,甲和乙两人分别都只有1000块:
然后我们来看看代码:
public static void main(String[] args) {
try(Connection conn = JdbcUtils.getConnection()){
conn.setAutoCommit(false); //事务开始,关闭自动提交;
try(Statement stat = conn.createStatement()){
//执行sql语句1
stat.executeUpdate("update money set balance=balance-1000 where name='甲'");
//执行sql语句2
stat.executeUpdate("update money set balance=balance+1000 where name='乙'");
}
conn.commit(); //手动提交事务;
System.out.println("转账完成!");
} catch (SQLException e) {
e.printStackTrace();
}
}
在这里我们用了一种try-with-resources的方法,这种特殊的try方法保证了每个声明了的资源在语句结束的时候都会被关闭。 这可以省去我们最后释放Connection 和 Statement对象的操作;如果你不想用这种方法,也可以使用原来的 try{} 方法,并且在最后的finally{ conn.close(); stat.close();};
我们看到上面简单的代码就完成了事务的一系列操作。可以看到用户甲向用户乙确实转账了1000元。
JDBC操作中遇到错误;
同样如果我们在操作的过程中,如果遇到了错误,同样数据库在会帮我们rollback(回滚);
在使用JDBC进行事务处理中,我们添加一个显而易见的错误:int i = 1/0 ;
public static void main(String[] args) {
try(Connection conn = JdbcUtils.getConnection()){
conn.setAutoCommit(false); //事务开始,关闭自动提交;
try(Statement stat = conn.createStatement()){
//执行sql语句1
stat.executeUpdate("update money set balance=balance-1000 where name='甲'");
int i = 1/0;
//执行sql语句2
stat.executeUpdate("update money set balance=balance+1000 where name='乙'");
}
conn.commit();
System.out.println("转账完成!");
} catch (SQLException e) {
e.printStackTrace();
}
}
当我们执行这个Java方法时,由于设置了int i = 1/0这个逻辑错误,程序会抛出异常,但是因为抛出异常后,后面的代码不再执行,也就是说程序无法执行到提交事务conn.commit()方法处,因此数据库将会回滚该事务所有的操作,因此金额还是原来那样:
回滚到指定位置
我们上面说过回滚的方法可以添加参数,这个参数就是我们可以在代码中添加回滚点;
通过链接Connection对象的setSavepoint() 方法即可在该方法所在的位置设置回滚点对象,当调用rollback(回滚点对象)方法即可将事务回滚到这个位置。这样在执行回滚之后,再次调用提交事务(Commit), 则回滚点之前的SQL还是执行的。
public static void main(String[] args) {
Connection conn = null;
try{
conn = JdbcUtils.getConnection()
conn.setAutoCommit(false); //事务开始,关闭自动提交;
try(Statement stat = conn.createStatement()){
//执行sql语句1
stat.executeUpdate("update money set balance=balance-1000 where name='甲'");
Savepoint sp = conn.setSavepoint(); //设置回滚点;
int i = 1/0; //模拟错误发生
//执行sql语句2
stat.executeUpdate("update money set balance=balance+1000 where name='乙'");
}
conn.commit();
System.out.println("转账完成!");
} catch (SQLException e) {
conn.rollback();
conn.commit();
}finally{
stat.close();
conn.close();
}
}
可以看到结果正如我们希望的那样,由于甲的操作在回滚点之前,又因为执行回滚之后还执行了提交事务,因此回滚点之前的SQL命令还是可以被执行成功的。因此切记,要想使用回滚点,一定要在回滚之后再次提交事务,否则设置回滚点是没有意义的。
数据库的四大特性和隔离级别
-
事务的四大特性:
⑴ 原子性(Atomicity)
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,这和前面两篇博客介绍事务的功能是一样的概念,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
⑵ 一致性(Consistency)
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
⑶ 隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。
⑷ 持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。 -
隔离级别:
隔离级别是事务的隔离性中的特点,关于事务的隔离性数据库提供了多种隔离级别;
有不同的隔离级别,
隔离级别越低,并发性越好,但数据的一致性差
隔离级别越高,并发性差,但数据的一致性高
由低到高四种:
读未提交 < 读提交 < 可重复读(mysql默认) < 序列化读
错误的级别由高到低
脏读 , 不可重复读, 幻读
脏读(读未提交)
7369的工资1000
事务1 事务2
begin begin
修改7369的工资为8000
select * sal from emp where empno=7369;// 8000
rollback
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
将隔离级别提高到读提交
,可以避免脏读
不可重复读
一边查询,另一边做update操作
一个事务内多次查询结果不一致
7369的工资1000
事务1 事务2
begin begin
select * sal from emp where empno=7369; // 1000
修改7369的工资为8000
commit;
select * sal from emp where empno=7369; // 8000
事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发送了不可重复读。
要避免脏读、不可重复读:将隔离级别提高到可重复读
隔离级别
幻读
一边查询,另一边做insert操作
如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。
要避免脏读、不可重复读、幻读:将隔离级别提高到序列化读
所谓的序列化读
就是把多版本并发退化到锁的并发控制:select语句上会被偷偷加上共享锁