本节内容:
- 事务概述
- JDBC事务操作
- DBUtils事务操作
- 使用ThreadLocal绑定连接资源
- 事物的特性和隔离级别
一、事务概述
1. 什么是事务
一件事情有n个组成单元,要不这n个组成单元同时成功,要不n个单元就同时失败。
就是将n个组成单元放到一个事务中。
2. mysql的事务
默认的事务:一条sql语句就是一个事务,默认就开启事务并提交事务
控制台手动控制事务:
1)显示的开启一个事务:start transaction
2)事务提交:commit代表从开启事务到事务提交,中间的所有的sql都认为有效,真正的更新数据库
3)事务的回滚:rollback 代表事务的回滚。从开启事务到事务回滚,中间的所有的sql操作都认为无效,数据库没有被更新。
二、JDBC事务操作
MySQL默认是自动事务:
执行sql语句:executeUpdate() --每执行一次executeUpdate方法 代表事务自动提交
通过jdbc的API手动事务:
开启事务:conn.setAutoComnmit(false);
提交事务:conn.commit();
回滚事务:conn.rollback();
注意:事务是通过connection来控制的。
控制事务的connnection必须是同一个。
执行sql的connection与开启事务的connnection必须是同一个才能对事务进行控制。
DBUtilsDemo.java
package com.itheima.jdbc; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; public class JDBCDemo { public static void main(String[] args) { //通过jdbc去控制事务 Connection conn = null; try { //1、注册驱动 Class.forName("com.mysql.jdbc.Driver"); //2、获得connection conn = DriverManager.getConnection("jdbc:mysql:///web19", "root", "root"); //手动开启事务 conn.setAutoCommit(false); //3、获得执行平台 Statement stmt = conn.createStatement(); //4、操作sql stmt.executeUpdate("update account set money=5000 where name='tom'"); //提交事务 conn.commit(); stmt.close(); conn.close(); } catch (Exception e) { try { //回滚事务 conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); } } }
DataSourceUtils.java
package com.itheima.utils; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import javax.sql.DataSource; import com.mchange.v2.c3p0.ComboPooledDataSource; public class DataSourceUtils { private static DataSource dataSource = new ComboPooledDataSource(); private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>(); // 直接可以获取一个连接池 public static DataSource getDataSource() { return dataSource; } public static Connection getConnection() throws SQLException{ return dataSource.getConnection(); } // 获取连接对象 public static Connection getCurrentConnection() throws SQLException { Connection con = tl.get(); if (con == null) { con = dataSource.getConnection(); tl.set(con); } return con; } // 开启事务 public static void startTransaction() throws SQLException { Connection con = getCurrentConnection(); if (con != null) { con.setAutoCommit(false); } } // 事务回滚 public static void rollback() throws SQLException { Connection con = getCurrentConnection(); if (con != null) { con.rollback(); } } // 提交并且 关闭资源及从ThreadLocall中释放 public static void commitAndRelease() throws SQLException { Connection con = getCurrentConnection(); if (con != null) { con.commit(); // 事务提交 con.close();// 关闭资源 tl.remove();// 从线程绑定中移除 } } // 关闭资源方法 public static void closeConnection() throws SQLException { Connection con = getCurrentConnection(); if (con != null) { con.close(); } } public static void closeStatement(Statement st) throws SQLException { if (st != null) { st.close(); } } public static void closeResultSet(ResultSet rs) throws SQLException { if (rs != null) { rs.close(); } } }
三、DBUtils事务操作
1. QueryRunner
有参构造:QueryRunner runner = new QueryRunner(DataSource dataSource);
有参构造将数据源(连接池)作为参数传入QueryRunner,QueryRunner会从连接池中获得一个数据库连接资源操作数据库,所以直接使用无Connection参数的update方法即可操作数据库。但是这种无法操作事务,因为无法控制connection是一致的。
无参构造:QueryRunner runner = new QueryRunner();
无参的构造没有将数据源(连接池)作为参数传入QueryRunner,那么我们在使用QueryRunner对象操作数据库时要使用有Connection参数的方法。
package com.itheima.dbutils; import java.sql.Connection; import java.sql.SQLException; import org.apache.commons.dbutils.QueryRunner; import com.itheima.utils.DataSourceUtils; public class DBUtilsDemo { public static void main(String[] args) { Connection conn = null; try { QueryRunner runner = new QueryRunner(); //runner.update("update account set money=15000 where name='tom'"); //获得一个Connection conn = DataSourceUtils.getConnection(); //开启事务 conn.setAutoCommit(false); runner.update(conn, "update account set money=15000 where name='tom'"); //提交或回滚事务 conn.commit(); } catch (SQLException e) { try { //回滚事务 conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); } } }
【示例】:完成转账。
三层开发,一般先写Dao层。Dao只管数据库操作,跟任何业务都不沾边。
在写web层。
事务控制在service层。最后才会写service层,service层最复杂。
表结构:
TransferDao.java
package com.itheima.transfer.dao; import java.sql.Connection; import java.sql.SQLException; import org.apache.commons.dbutils.QueryRunner; import com.itheima.utils.DataSourceUtils; public class TransferDao { //为转出账户减钱 public void out(String out, double money) throws SQLException { //异常抛出去,到service层处理 QueryRunner runner = new QueryRunner(); Connection conn = DataSourceUtils.getConnection(); //这是一个connection String sql = "update account set money=money-? where name=?"; runner.update(conn, sql, money,out); } //为转入账户加钱 public void in(String in, double money) throws SQLException { QueryRunner runner = new QueryRunner(); Connection conn = DataSourceUtils.getConnection(); //这又是一个connection String sql = "update account set money=money+? where name=?"; runner.update(conn, sql, money,in); } }
transfer.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> <form action="${pageContext.request.contextPath }/transfer" method="post"> 转出账户:<input type="text" name="out"><br> 转入账户:<input type="text" name="in"><br> 转账金额:<input type="text" name="money"><br> <input type="submit" value="确认转账"><br> </form> </body> </html>
TransferServlet.java
package com.itheima.transfer.web; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.itheima.transfer.service.TransferService; public class TransferServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //接受转账的参数 String out = request.getParameter("out"); String in = request.getParameter("in"); String moneyStr = request.getParameter("money"); double money = Double.parseDouble(moneyStr); //调用业务层的转账方法 TransferService service = new TransferService(); boolean isTransferSuccess = service.transfer(out,in,money); response.setContentType("text/html;charset=UTF-8"); if(isTransferSuccess){ response.getWriter().write("转账成功!!!"); }else{ response.getWriter().write("转账失败!!!"); } } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
TransferService.java
package com.itheima.transfer.service; import java.sql.Connection; import java.sql.SQLException; import com.itheima.transfer.dao.TransferDao; import com.itheima.utils.DataSourceUtils; import com.itheima.utils.MyDataSourceUtils; public class TransferService { public boolean transfer(String out, String in, double money) { TransferDao dao = new TransferDao(); boolean isTranferSuccess = true; Connection conn = null; try { //开启事务 conn = DataSourceUtils.getConnection(); //这又是一个connection conn.setAutoCommit(false); //转出钱的方法 dao.out(out,money); //int i = 1/0; //转入钱的方法 dao.in(in,money); //conn.commit(); //提交放在这,finally处代码就不用写了。 } catch (Exception e) { isTranferSuccess = false; //回滚事务 //如果提交放在上面,上面代码出现异常,进入下面的try-catch,回滚事务,然后事务并没有提交,只要没提交事务就没有结束,但是随着代码都执行完毕了,方法就结束了。 //方法结束了,conn会帮我们关上。但是为什么建议把commit放在finally中?因为这里conn关闭其实是归还到连接池中,别人再从连接池中获取可能会拿到这个连接,那么他就会 //使用刚才的事务 try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); } finally{ //建议把提交的代码写这 //注意:也许你会觉得上面如果出现异常,就已经回滚了,写在这最后肯定会执行commit提交的,这不是有问题吗? //这就涉及到提交和回滚的本质了。回滚本身内部不包含提交的功能,没有指定回滚点,默认滚到开启事务的地方,即conn.setAutoCommit(false);。这样开启了事务,什么都没干,我还是可以提交的。 //当然,也可以把提交代码写到 dao.in(in,money); 后面。 try { conn.commit(); } catch (SQLException e1) { e1.printStackTrace(); } } return isTranferSuccess; } }
工具类:DataSourceUtils.java
package com.itheima.utils; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import javax.sql.DataSource; import com.mchange.v2.c3p0.ComboPooledDataSource; public class DataSourceUtils { private static DataSource dataSource = new ComboPooledDataSource(); // 直接可以获取一个连接池 public static DataSource getDataSource() { return dataSource; } public static Connection getConnection() throws SQLException{ return dataSource.getConnection(); } // 关闭资源方法 public static void closeConnection() throws SQLException { Connection con = getCurrentConnection(); if (con != null) { con.close(); } } public static void closeStatement(Statement st) throws SQLException { if (st != null) { st.close(); } } public static void closeResultSet(ResultSet rs) throws SQLException { if (rs != null) { rs.close(); } } }
配置文件:c3p0-config.xml
<?xml version="1.0" encoding="UTF-8"?> <c3p0-config> <default-config> <property name="user">root</property> <property name="password">123456</property> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="jdbcUrl">jdbc:mysql:///web19</property> </default-config> </c3p0-config>
但是运行起这个项目,发现jsp页面返回“转账失败!!!”。这是因为项目中事务方面实际上用到了3个不同的connection,并不是同一个connection。
修改Transferservice代码,把connection传到Dao层中去,
Connection conn = null; try { //开启事务 conn = DataSourceUtils.getConnection(); //这又是一个connection conn.setAutoCommit(false); //开启事务 //MyDataSourceUtils.startTransaction(); //转出钱的方法 dao.out(conn, out,money); //int i = 1/0; //转入钱的方法 dao.in(conn,in,money); ...
修改TransferDao代码,
//为转出账户减钱 public void out(Connection conn, String out, double money) throws SQLException { //异常抛出去,到service层处理 QueryRunner runner = new QueryRunner(); String sql = "update account set money=money-? where name=?"; runner.update(conn, sql, money,out); } //为转入账户加钱 public void in(Connection conn, String in, double money) throws SQLException { QueryRunner runner = new QueryRunner(); String sql = "update account set money=money+? where name=?"; runner.update(conn, sql, money,in); }
这样,3处的connection就是同一个connection了。
虽然这样是成功了,但是我们在TransferService中调用Dao层方法时把connection传到Dao层了,
//转出钱的方法 dao.out(conn, out,money);
这种方式不好,connection是数据层的对象,应该是在Dao层出现,但是现在出现在了service层,也就产生了层与层之间“污染”。这样矛盾就来了,事务控制必须通过connection,而且事务控制是在service层进行的。但是在service层又不想看见connection。
这就引入了ThreadLocal。
四、使用ThreadLocal绑定连接资源
ThreadLocal:线程绑定。
1. 分析上面的实例
上面的示例是使用JavaEE三层开发的思想,请求到达web层,web层调service层,service层调dao层,在这个过程中,只有一个线程帮我们跑的代码。可以在每层打个断点,Eclipse debug运行这个项目:
浏览器输入localhost:8080/WEB19/transfer.jsp访问,输入账户名称和转账金额,点击“确认转账”
接着就进入Eclipse了
2. ThreadLocal
web层往公共区域存一个 key1:xxx 的东西,service和dao层是可以从这个公共区域里取这个 key1 的 value。前提是service层和dao层得知道公共区域存的这个key的name,才能取value。
这3层跑代码的线程是同一个,而且这个线程是有名字的,所以可以把key=线程名字。
这个map就是ThreadLocal底层代码。我们可以把ThreadLocal看成存数据的map,但是ThreadLocal没有键,因为键默认就是当前线程。
这样我们就可以修改工具类:
MyDataSourceUtils.java
package com.itheima.utils; import java.sql.Connection; import java.sql.SQLException; import com.mchange.v2.c3p0.ComboPooledDataSource; public class MyDataSourceUtils { //获得Connection ----- 从连接池中获取 private static ComboPooledDataSource dataSource = new ComboPooledDataSource(); //创建ThreadLocal private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>(); //开启事务 public static void startTransaction() throws SQLException{ Connection conn = getCurrentConnection(); conn.setAutoCommit(false); } //获得当前线程上绑定的conn public static Connection getCurrentConnection() throws SQLException{ //从ThreadLocal寻找 当前线程是否有对应Connection Connection conn = tl.get(); if(conn==null){ //获得新的connection conn = getConnection(); //将conn资源绑定到ThreadLocal(map)上,只不过key约定是的执行当前代码的线程 tl.set(conn); } return conn; } public static Connection getConnection() throws SQLException{ return dataSource.getConnection(); } //回滚事务 public static void rollback() throws SQLException { getCurrentConnection().rollback(); } //提交事务 public static void commit() throws SQLException { Connection conn = getCurrentConnection(); conn.commit(); //将Connection从ThreadLocal中移除 tl.remove(); conn.close(); } }
这样就可以调整service和dao的代码:
TransferService.java
package com.itheima.transfer.service; import java.sql.SQLException; import com.itheima.transfer.dao.TransferDao; import com.itheima.utils.MyDataSourceUtils; public class TransferService { public boolean transfer(String out, String in, double money) { TransferDao dao = new TransferDao(); boolean isTranferSuccess = true; //Connection conn = null; try { //开启事务 //conn = DataSourceUtils.getConnection(); //这又是一个connection //conn.setAutoCommit(false); // 开启事务 MyDataSourceUtils.startTransaction(); //转出钱的方法 dao.out(out,money); //int i = 1/0; //转入钱的方法 dao.in(in,money); //conn.commit(); //提交放在这,finally处代码就不用写了。 } catch (Exception e) { isTranferSuccess = false; //回滚事务 //如果提交放在上面,上面代码出现异常,进入下面的try-catch,回滚事务,然后事务并没有提交,只要没提交事务就没有结束,但是随着代码都执行完毕了,方法就结束了。 //方法结束了,conn会帮我们关上。但是为什么建议把commit放在finally中?因为这里conn关闭其实是归还到连接池中,别人再从连接池中获取可能会拿到这个连接,那么他就会 //使用刚才的事务 /*try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); }*/ try { MyDataSourceUtils.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); } finally{ //建议把提交的代码写这 //注意:也许你会觉得上面如果出现异常,就已经回滚了,写在这最后肯定会执行commit提交的,这不是有问题吗? //这就涉及到提交和回滚的本质了。回滚本身内部不包含提交的功能,没有指定回滚点,默认滚到开启事务的地方,即conn.setAutoCommit(false);。这样开启了事务,什么都没干,我还是可以提交的。 //当然,也可以把提交代码写到 dao.in(in,money); 后面。 /*try { conn.commit(); } catch (SQLException e1) { e1.printStackTrace(); }*/ try { MyDataSourceUtils.commit(); } catch (SQLException e) { e.printStackTrace(); } } return isTranferSuccess; } }
TransferDao.java
package com.itheima.transfer.dao; import java.sql.Connection; import java.sql.SQLException; import org.apache.commons.dbutils.QueryRunner; import com.itheima.utils.MyDataSourceUtils; public class TransferDao { //为转出账户减钱 public void out(String out, double money) throws SQLException { //异常抛出去,到service层处理 QueryRunner runner = new QueryRunner(); //Connection conn = DataSourceUtils.getConnection(); //这是一个connection Connection conn = MyDataSourceUtils.getCurrentConnection(); String sql = "update account set money=money-? where name=?"; runner.update(conn, sql, money,out); } //为转入账户加钱 public void in(String in, double money) throws SQLException { QueryRunner runner = new QueryRunner(); //Connection conn = DataSourceUtils.getConnection(); //这又是一个connection Connection conn = MyDataSourceUtils.getCurrentConnection(); String sql = "update account set money=money+? where name=?"; runner.update(conn, sql, money,in); } }
最终,整理下项目中可以用的工具类:DataSourceUtils.java
package com.itheima.utils; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import javax.sql.DataSource; import com.mchange.v2.c3p0.ComboPooledDataSource; public class DataSourceUtils { private static DataSource dataSource = new ComboPooledDataSource(); private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>(); // 直接可以获取一个连接池 public static DataSource getDataSource() { return dataSource; } public static Connection getConnection() throws SQLException{ return dataSource.getConnection(); } // 获取连接对象 public static Connection getCurrentConnection() throws SQLException { Connection con = tl.get(); if (con == null) { con = dataSource.getConnection(); tl.set(con); } return con; } // 开启事务 public static void startTransaction() throws SQLException { Connection con = getCurrentConnection(); if (con != null) { con.setAutoCommit(false); } } // 事务回滚 public static void rollback() throws SQLException { Connection con = getCurrentConnection(); if (con != null) { con.rollback(); } } // 提交并且 关闭资源及从ThreadLocall中释放 public static void commitAndRelease() throws SQLException { Connection con = getCurrentConnection(); if (con != null) { con.commit(); // 事务提交 con.close();// 关闭资源 tl.remove();// 从线程绑定中移除 } } // 关闭资源方法 public static void closeConnection() throws SQLException { Connection con = getCurrentConnection(); if (con != null) { con.close(); } } public static void closeStatement(Statement st) throws SQLException { if (st != null) { st.close(); } } public static void closeResultSet(ResultSet rs) throws SQLException { if (rs != null) { rs.close(); } } }
五、事物的特性和隔离级别
1. 事务的特性ACID
1)原子性(Atomicity)原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
2)一致性(Consistency)一个事务中,事务前后数据的完整性必须保持一致。
3)隔离性(Isolation)多个事务,事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。
4)持久性(Durability)持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
事实上,ACID机制需要背后有强大的机制才能实现的,而且一旦让一个存储引擎或关系型数据库支持事务,那么它在复杂程度上面是成几何倍上升的。并且如果事务做的完全隔离,事务就是串行的了。如果做到完全隔离,只有在两个事务压根不会涉及到同一张表的时候才会同时执行,否则只要涉及到同一个数据集,事务都只能以串行方式进行。数据安全性越高,其并发性就越低。所以就有了隔离级别的概念。
2. 并发访问问题 --由隔离性引起
如果不考虑隔离性,事务存在3种并发访问问题。
脏读:B事务读取到了A事务尚未提交的数据 -- 要求B事务要读取A事务提交的数据
不可重复读:一个事务中,两次读取的数据的内容不一致 --要求的是一个事务中多次读取时数据是一致的 --- unpdate
幻读/虚读:一个事务中,两次读取的数据的数量不一致 --要求在一个事务多次读取的数据的数量是一致的 --insert delete
3. 事务的隔离级别
read uncommitted : 读取尚未提交的数据 :哪个问题都不能解决
read committed:读取已经提交的数据 :可以解决脏读 -- oracle默认的
repeatable read:重读读取:可以解决脏读 和 不可重复读 --mysql默认的
serializable:串行化:可以解决 脏读、不可重复读 和 虚读 --相当于锁表
4. 查看MySQL默认的隔离级别
(1)查看当前会话隔离级别
select @@tx_isolation;
(2)查看mysql数据库当前隔离级别
select @@global.tx_isolation;
(3)设置当前会话隔离级别
set session transaction isolatin level repeatable read;
(4)设置mysql数据库当前隔离级别
set global transaction isolation level repeatable read;