什么是事务?
举个常见的例子:
张三要转给李四100元钱。
首先张三的账户上要扣除100元钱,然后李四的账户上增加100元钱。
但是在这期间,如果出了问题,比如,张三账户上扣掉100元,但是李四的账户上没能加上100元;张三的账户上没能成功扣掉100元,而李四的账户上增加了100元等等。
这都不能达成我们预期的目的。
在这个转账的例子中,
1.首先我们要做到的就是张三和李四转账这个事是一个整体,要么全做,要么都不做。像在化学中的原子是不可分割的。(即原子性 atomicity)
2.我们要保证这个事情是关于100元的事情,不是200元,也不是50元,是100元!这100元在程序运行中是不变的,执行前后是不变的。(即一致性 consistency)
3.假如在张三和李四交易的同时,王五凑巧在给张三转200元。假设两个交易是并发执行的。不能出现以下情况:
即两个交易之间必须是互相隔离的,不能会出现信息错误。(隔离性 isolation)
4.假设在交易完成后,银行系统坏了,那么必须保证,修复之后的系统记录和系统坏掉之前记录一致。(持久性 durability)
那么到底什么是事务?
事务(Transaction)是并发控制的基本单位,指作为单个逻辑工作单元执行的一系列操作,而这些逻辑工作单元需要满足ACID特性(即上面四种特性的英文首字母)。
JDBC的事务控制:
connection对象提供了三个方法实现事务逻辑:
Connection conn=getConnection();//自定义获取Connection对象的方法 conn.setAutoCommit(false);//开启事务 conn.commit();//提交事务 conn.rollback();//回滚事务
其中,
conn.setAutoCommit(false);//开启事务
在方法参数中填入了false关键字,关闭了JDBC的自动提交事务(默认的)。本来如果是默认的自动提交,那么每一条语句操作都将产生结果,这就破坏了事务的原子性,因为这就使得这一系列操作存在了中间状态,这一系列操作也就不是所谓的事务。
所以,关闭JDBC默认的自动提交,才是开启事务管理。
conn.commit();//提交事务
表示事务提交,整个事务结束,整个事务中的sql语句都将生效产生结果。
conn.rollback();//回滚事务
表示”回滚“表示我们将会回到事务开始之前的状态。
关于张三李四转账的示例代码:
package com.java.transaction; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; public class Transaction { private static String Driver="com.mysql.jdbc.Driver"; //数据库驱动 //连接数据库的URL地址 private static String url="jdbc:mysql://localhost:3306/hellojdbc?useUnicode=true&characterEncoding=UTF-8"; private static String username="root";//数据库连接用户名 private static String password="123456";//数据库连接密码 private static Connection conn=null;//数据库连接对象 private static PreparedStatement pst=null;//预编译语句 //使用静态块的方式加载驱动 static { try { //调用Class对象的静态forName()方法加载数据库驱动类 Class.forName(Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } //使用单例模式返回数据库连接对象 public static Connection getConnection() throws SQLException{ if(conn==null){ conn=DriverManager.getConnection(url, username, password); return conn; } return conn; } public static void doTransaction(){ try { conn=getConnection(); conn.setAutoCommit(false);//开启事务 pst=conn.prepareStatement("update logintable account = ? where name = ? "); pst.setInt(1, 0); pst.setString(2, "zhangsan"); pst.execute(); pst.setInt(1, 100); pst.setString(2, "lisi"); pst.execute(); conn.commit();//提交事务 } catch (SQLException e) { if(conn!=null) try { conn.rollback();//回滚事务 } catch (SQLException e1) { e1.printStackTrace(); } }finally{ try { if(pst!=null) pst.close(); if(conn!=null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } public static void main(String[] args) { doTransaction(); } }
检查点:
JDBC还提供了断点处理和控制的功能。
我们在事务内部设置断点,当在断点之后的部分出现异常错误的时候,我们还能回到断点,去执行断点后面的另外一条路,而这个过程也被当作一个事务来处理。
Savepoint sp=null;//声明检查点 sp=conn.setSavepoint(); //保存检查点 conn.rollback(sp);//回滚到断点
将上面示例代码修改为当向李四转账出现错误时,改转给王五。
package com.java.transaction; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Savepoint; public class Transaction { private static String Driver="com.mysql.jdbc.Driver"; //数据库驱动 //连接数据库的URL地址 private static String url="jdbc:mysql://localhost:3306/hellojdbc?useUnicode=true&characterEncoding=UTF-8"; private static String username="root";//数据库连接用户名 private static String password="123456";//数据库连接密码 private static Connection conn=null;//数据库连接对象 private static PreparedStatement pst=null;//预编译语句 //使用静态块的方式加载驱动 static { try { //调用Class对象的静态forName()方法加载数据库驱动类 Class.forName(Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } //使用单例模式返回数据库连接对象 public static Connection getConnection() throws SQLException{ if(conn==null){ conn=DriverManager.getConnection(url, username, password); return conn; } return conn; } public static void doTransaction(){ Savepoint sp=null;//声明检查点 try { conn=getConnection(); conn.setAutoCommit(false);//开启事务 pst=conn.prepareStatement("update logintable account = ? where name = ? "); pst.setInt(1, 0); pst.setString(2, "zhangsan"); pst.execute(); sp=conn.setSavepoint(); //保存检查点 pst.setInt(1, 100); pst.setString(2, "lisi"); pst.execute(); //conn.commit();//此处不再提交事务 throw new SQLException(); //为了检验检查点,人工抛一个异常 } catch (SQLException e) { if(conn!=null){ try { conn.rollback(sp);//回滚到断点 //不给李四转了,改转给王五 pst.setInt(1, 100); pst.setString(2, "wangwu"); pst.execute(); conn.commit();//提交事务 } catch (SQLException e1) { e1.printStackTrace(); } } }finally{ try { if(pst!=null) pst.close(); if(conn!=null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } public static void main(String[] args) { doTransaction(); } }
事务并发执行的几个概念:
1)脏读:
一个事务读取了另外一个事务未提交的信息。
例子:
张三在向李四转账100元的同时又存上了200元。
因为事务T1未能提交执行成功,所以本来应该拥有300元的张三,由于脏读(事务T2读取了T1未提交的信息),张三实际上只拥有200元。
2)不可重复读:
同一事务中两次读取相同记录,结果不一样。(行记录的值)
例子:
事务T1张三连续两次读取自己账户的余额;事务T2张三读取自己账户上的余额后又存上200元。
3)幻读:
两次读取的结果包含的行记录不一样。(行记录数)
事务隔离级别:
1)读未提交(read uncommitted)
允许出现脏读
2)读提交(read committed)
不允许出现脏读,但是可以允许不可重复读。
3)重复读(repeatable read)
不允许出现不可重复读,可能会出现幻读。
4)串行化(serializable)
最高事务隔离级别。不允许出现幻读。因为它的级别最高,并发控制最严格。所有的事务都是串行执行的。导致数据库的性能变差。
注意:
MySQL默认事务隔离级别为repeatable read。
事务隔离级别越高,数据库性能就越差,但是编程的难度越低。
设置JDBC中的隔离级别:
Connection conn=getConnection();//自定义获取Connection对象的方法 conn.getTransactionIsolation();//获取事务的隔离级别 conn.setTransactionIsolation(level);//设置事务的隔离级别