银行转帐!张三转10000块到李四的帐户,这其实须要两条SQL语句:html
给张三的帐户减去10000元;java
给李四的帐户加上10000元。mysql
若是在第一条SQL语句执行成功后,在执行第二条SQL语句以前,程序被中断了(多是抛出了某个异常,也多是其余什么缘由),那么李四的帐户没有加上10000元,而张三却减去了10000元。这确定是不行的!web
你如今可能已经知道什么是事务了吧!事务中的多个操做,要么彻底成功,要么彻底失败!不可能存在成功一半的状况!也就是说给张三的帐户减去10000元若是成功了,那么给李四的帐户加上10000元的操做也必须是成功的;不然给张三减去10000元,以及给李四加上10000元都是失败的!sql
总结:事务指逻辑上的一组操做,组成这组操做的各个单元,要不所有成功,要不所有不成功。数据库
事务的四大特性是:并发
在默认状况下,MySQL每执行一条SQL语句,都是一个单独的事务。若是须要在一个事务中包含多条SQL语句,那么须要开启事务和结束事务。jsp
在执行SQL语句以前,先执行strat transaction,这就开启了一个事务(事务的起点),而后能够去执行多条SQL语句,最后要结束事务,commit表示提交,即事务中的多条SQL语句所作出的影响会持久化到数据库中(从开启事务到事务提交,中间的全部的sql都认为有效,真正的更新数据库)。或者rollback,表示回滚,即回滚到事务的起点,以前作的全部操做都被撤消了(从开启事务到事务回滚,中间的全部的sql操做都认为无效,数据库没有被更新)!post
下面演示zs给li转帐10000元的示例:性能
START TRANSACTION; UPDATE account SET balance=balance-10000 WHERE id=1; UPDATE account SET balance=balance+10000 WHERE id=2; -- 回滚结束,事务执行失败 ROLLBACK ; START TRANSACTION; UPDATE account SET balance=balance-10000 WHERE id=1; UPDATE account SET balance=balance+10000 WHERE id=2; -- 提交结束,事务执行成功 COMMIT ; START TRANSACTION; UPDATE account SET balance=balance-10000 WHERE id=1; UPDATE account SET balance=balance+10000 WHERE id=2; -- 退出,MySQL会自动回滚事务。 quit ;
在jdbc中处理事务,都是经过Connection完成的!当Jdbc程序向数据库得到一个Connection对象时,默认状况下这个Connection对象会自动向数据库提交在它上面发送的SQL语句。若想关闭这种默认提交方式,让多条SQL在一个事务中执行,可以使用下列的JDBC控制事务语句:
jdbc处理事务的代码格式:
try { con.setAutoCommit(false);//开启事务… …. … con.commit();//try的最后提交事务 } catch() { con.rollback();//回滚事务 }
事务1:张三给李四转帐100元
事务2:李四查看本身的帐户
t1:事务1:开始事务
t2:事务1:张三给李四转帐100元
t3:事务2:开始事务
t4:事务2:李四查看本身的帐户,看到帐户多出100元(脏读)
t5:事务2:提交事务
t6:事务1:回滚事务,回到转帐以前的状态
不可重复读:一个事务读到了另外一个事务已经提交的update的数据,致使在同一个事务中的屡次查询结果不一致。
事务1:酒店查看两次1048号房间状态
事务2:预订1048号房间
t1:事务1:开始事务
t2:事务1:查看1048号房间状态为空闲
t3:事务2:开始事务
t4:事务2:预约1048号房间
t5:事务2:提交事务
t6:事务1:再次查看1048号房间状态为使用
t7:事务1:提交事务
对同一记录的两次查询结果不一致!
幻读(虚读):一个事务读到了另外一个事务已经提交的insert的数据,致使在同一个事务中的屡次查询结果不一致。
事务1:对酒店房间预订记录两次统计
事务2:添加一条预订房间记录
t1:事务1:开始事务
t2:事务1:统计预订记录100条
t3:事务2:开始事务
t4:事务2:添加一条预订房间记录
t5:事务2:提交事务
t6:事务1:再次统计预订记录为101记录
t7:事务1:提交
对同一表的两次查询不一致!
【不可重复读和幻读的区别】
4个等级的事务隔离级别,在相同数据环境下,使用相同的输入,执行相同的工做,根据不一样的隔离级别,能够致使不一样的结果。不一样事务隔离级别可以解决的数据并发问题的能力是不一样的。
【SERIALIZABLE(串行化)】
【REPEATABLE READ(可重复读)(MySQL)】
【READ COMMITTED(读已提交数据)(Oracle)】
【READ UNCOMMITTED(读未提交数据)】
mysql数据库默认的事务隔离级别是:Repeatable read(可重复读)
【mysql数据库查询当前事务隔离级别】
select @@tx_isolation
例如:
【mysql数据库设置事务隔离级别】
set transaction isolation level 隔离级别名
例如:
A窗口
set transaction isolation level read uncommitted;--设置A用户的数据库隔离级别为Read uncommitted(读未提交)
start transaction;--开启事务
select * from account;--查询A帐户中现有的钱,转到B窗口进行操做
select * from account--发现a多了100元,这时候A读到了B未提交的数据(脏读)
B窗口
start transaction;--开启事务
update account set money=money+100 where name='A';--不要提交,转到A窗口查询
A窗口
set transaction isolation level read committed;
start transaction;
select * from account;--发现a账户是1000元,转到b窗口
select * from account;--发现a账户多了100,这时候,a读到了别的事务提交的数据,两次读取a账户读到的是不一样的结果(不可重复读)
B窗口
start transaction;
update account set money=money+100 where name='aaa';
commit;--转到a窗口
A窗口
set transaction isolation level repeatable read;
start transaction;
select * from account;--发现表有4个记录,转到b窗口
select * from account;--可能发现表有5条记录,这时候发生了a读取到另一个事务插入的数据(虚读)
B窗口
start transaction;
insert into account(name,money) values('ggg',1000);
commit;--转到a窗口
A窗口
set transaction isolation level Serializable;
start transaction;
select * from account;--转到b窗口
B窗口
start transaction;
insert into account(name,money) values('ggg',1000);--发现不能插入,只能等待a结束事务才能插入
con. setTransactionIsolation(int level)
参数可选值以下:
在开发中,对数据库的多个表或者对一个表中的多条数据执行更新操做时要保证对多个更新操做要么同时成功,要么都不成功,这就涉及到对多个更新操做的事务管理问题了。好比银行业务中的转帐问题,A用户向B用户转帐100元,假设A用户和B用户的钱都存储在Account表,那么A用户向B用户转帐时就涉及到同时更新Account表中的A用户的钱和B用户的钱,用SQL来表示就是:
update account set money=money-100 where name='A' update account set money=money+100 where name='B'
[transfer.jsp]
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>transfer</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="确认转帐"/> </form> </body> </html>
【web层】
public class TransferServlet extends HttpServlet { protected void doPost(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 doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } }
【service层】
public class TransferService { public boolean transfer(String out, String in, double money) { TransferDao dao = new TransferDao(); boolean isTransferSuccess = true; try { // 转出钱的方法 dao.out(out, money); int i = 1 / 0; // 转入钱的方法 dao.in(in,money); } catch (Exception e) { isTransferSuccess = false; e.printStackTrace(); } return isTransferSuccess; } }
【Dao层】
public class TransferDao { public void out(String out, double money) throws SQLException { QueryRunner qr = new QueryRunner(); Connection conn = JdbcUtils.getConnection(); String sql = "update t_account set money=money-? where name=?"; qr.update(conn, sql, money, out); } public void in(String in, double money) throws SQLException { QueryRunner qr = new QueryRunner(); Connection conn = JdbcUtils.getConnection(); String sql = "update t_account set money=money+? where name=?"; qr.update(conn, sql, money, in); } }
1 public class TransferService { 2 public boolean transfer(String out, String in, double money) { 3 TransferDao dao = new TransferDao(); 4 5 boolean isTransferSuccess = true; 6 Connection conn = null; 7 8 try { 9 // 开启事务 10 conn = JdbcUtils.getConnection(); 11 conn.setAutoCommit(false); 12 // 转出钱的方法 13 dao.out(out, money); 14 int i = 1 / 0; 15 // 转入钱的方法 16 dao.in(in, money); 17 // 事务的提交不建议放在这 18 // conn.commit(); 19 } catch (Exception e) { 20 isTransferSuccess = false; 21 // 回滚事务 22 try { 23 conn.rollback(); 24 } catch (SQLException e1) { 25 e1.printStackTrace(); 26 } 27 e.printStackTrace(); 28 } finally{ 29 try { 30 // 事务的提交建议放在finally里 31 conn.commit(); 32 } catch (SQLException e) { 33 e.printStackTrace(); 34 } 35 } 36 return isTransferSuccess; 37 } 38 }
这里可能会有一个疑问:若是把commit放在finally里面的话,若是程序报错,回滚完后还提交。
注意回滚与提交的区别:回滚自己内部不包含提交的功能,回滚是“滚”到事务开启的地方,即第11行。程序报错,“滚”到第11行,而后再提交,这时候能够认为第13-16行之间的代码都没执行过。
若是把commit放在第18行,那么finally的代码就能够不用写了。这时候的状况是:第14行代码报错,程序就进入catch代码块,接着进行回滚操做,”滚“到第11行,这时第18行的commit是没有执行的,事务尚未结束,可是回滚完成以后,catch的代码已经执行完毕,即方法结束了,方法结束后,链接池会自动帮你关闭connection,这时候事务就结束了。并且别人再拿到connection的时候,就从新开启了一个新的事务了。
修改完service层后,再次执行转帐功能(tom给lucy转100),这是发现,虽然咱们已经加入事务的处理了,可是转帐失败的时候,tom少了100元,lucy仍然不变,跟以前的状况同样。
问题的缘由是:开启事务的时候,是从链接池中拿的一个connection,而在操做TransferDao的时候,又从池子中拿了一个connection,这两个connection并非同一个!!而dao中转入、转出两个方法中的connection也不是同一个。以上的操做,就有了3个不一样的connection,这是不容许的。注意:控制事务、开启事务、以及操做每条sql的connection必须是同一个!下面对原始版进行改进。
思路:咱们在service中拿到了connection,为保证后面的操做是同一个connection,咱们能够将service中的connection以参数的形式传递给dao层,这样就保证了connection是同一个。
【service层】
public class TransferService { public boolean transfer(String out, String in, double money) { TransferDao dao = new TransferDao(); boolean isTransferSuccess = true; Connection conn = null; try { // 开启事务 conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); // 转出钱的方法 dao.out(conn,out, money); int i = 1 / 0; // 转入钱的方法 dao.in(in,conn, money); // 事务的提交不建议放在这 // conn.commit(); } catch (Exception e) { isTransferSuccess = false; // 回滚事务 try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); } finally{ try { // 事务的提交建议放在finally里 conn.commit(); } catch (SQLException e) { e.printStackTrace(); } } return isTransferSuccess; } }
public class TransferDao { public void out(Connection conn, String out, double money) throws SQLException { QueryRunner qr = new QueryRunner(); //Connection conn = JdbcUtils.getConnection(); String sql = "update t_account set money=money-? where name=?"; qr.update(conn, sql, money, out); } public void in(String in, Connection conn, double money) throws SQLException { QueryRunner qr = new QueryRunner(); //Connection conn = JdbcUtils.getConnection(); String sql = "update t_account set money=money+? where name=?"; qr.update(conn, sql, money, in); } }
在上面改进版的代码中,咱们是在service中调用dao的方法,并把connection对象传递过去,这就保证了connection是同一个。可是这种方式很差, 咱们在开发中使用分层的目的就是使每一个层之间的逻辑更清楚。在改进版中, service层出现了connection,connection是数据库的链接资源,它应该是在dao层出现的 ,这就涉及到了层与层之间的”污染“了,因此并很差。
如今的矛盾在于:开启事务,须要用connection开,执行sql,也要用connection执行,并且这两个connection必须是同一个。可是事务的操做又必须在service层控制,而在service层还不想看到connection。接下来介绍ThreadLocal。
首先咱们须要的一点是:全部的方法调用,用的都是同一个线程(除非你开启一个新的线程)。在本案例的转帐功能中,web层-service层-dao层之间的方法调用,都用的是同一个线程的。在操做事务时,咱们可用尝试将connection放在一个Map中,map的key就是当前线程,value就是connection,这样就能保证service层和dao层用的是同一个connection。其实ThreadLocal就是利用这样的原理。
ThreadLocal内部实际上是个Map来保存数据。虽然在使用ThreadLocal时只给出了值,没有给出键,其实它内部使用了当前线程作为键。
class MyThreadLocal<T> { private Map<Thread,T> map = new HashMap<Thread,T>(); public void set(T value) { map.put(Thread.currentThread(), value); } public void remove() { map.remove(Thread.currentThread()); } public T get() { return map.get(Thread.currentThread()); } }
ThreadLocal类只有三个方法:
【改进JdbcUtils】
public class JdbcUtils { // 得到Connection——从链接池中获取 private static ComboPooledDataSource dataSource = new ComboPooledDataSource(); // 建立ThreadLocal,<Connection>表示存放的值为Connection类 private static ThreadLocal<Connection> tl = new ThreadLocal(); // 得到当前线程上绑定的connection public static Connection getCurrentConnection() throws SQLException { // 从ThreadLocal寻找当前线程是否有对应的Connection Connection conn = tl.get(); if (conn == null) { // 得到新的connection conn = getConnection(); // 将conn资源绑定到ThreadLocal上 tl.set(conn); } return conn; } // 开启事务 public static void startTransaction() throws SQLException { Connection conn = getCurrentConnection(); conn.setAutoCommit(false); } // 回滚事务 public static void rollback() throws SQLException { getCurrentConnection().rollback(); } // 提交事务 public static void commit() throws SQLException { getCurrentConnection().commit(); } public static DataSource getDataSource() { return dataSource; } // 获取链接 public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } // 释放链接 public static void release(Connection connection, PreparedStatement pstmt, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (pstmt != null) { try { pstmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (connection != null) { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
【service层】
public class TransferService { public boolean transfer(String out, String in, double money) { TransferDao dao = new TransferDao(); boolean isTransferSuccess = true; try { // 开启事务 //conn = JdbcUtils.getConnection(); //conn.setAutoCommit(false); JdbcUtils.startTransaction(); // 转出钱的方法 dao.out(out, money); int i = 1 / 0; // 转入钱的方法 dao.in(in, money); // 事务的提交不建议放在这 // conn.commit(); } catch (Exception e) { isTransferSuccess = false; // 回滚事务 try { JdbcUtils.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); } finally{ try { // 事务的提交建议放在finally里 JdbcUtils.commit(); } catch (SQLException e) { e.printStackTrace(); } } return isTransferSuccess; } }
【Dao层】——数据库链接对象再也不须要service层传递过来,而是直接从JdbcUtils提供的getCurrentConnection方法去获取
public class TransferDao { public void out(String out, double money) throws SQLException { QueryRunner qr = new QueryRunner(); Connection conn = JdbcUtils.getCurrentConnection(); String sql = "update t_account set money=money-? where name=?"; qr.update(conn, sql, money, out); } public void in(String in, double money) throws SQLException { QueryRunner qr = new QueryRunner(); Connection conn = JdbcUtils.getCurrentConnection(); String sql = "update t_account set money=money+? where name=?"; qr.update(conn, sql, money, in); } }
这样在service层对事务的处理看起来就更加优雅了。ThreadLocal类在开发中使用得是比较多的,程序运行中产生的数据要想在一个线程范围内共享,只须要把数据使用ThreadLocal进行存储便可。