JDBC从入门到放弃
05-JDBC的事务
什么是事务
TCL:Transaction Control Language,事务控制语言
事务:在MySQL数据库中表示一条或多条Sql语句组合在一起的一个执行单元.这个执行单元要么全部执行,要么全部不执行,否则就会出现逻辑错误!
比如银行里的转账这个事情:
A账号余额:1000
B账号余额:1000
现在A转500元给B,那么要完成这个转账的事务,数据中的SQL应该是这样的执行过程:
①A账号上要减少500元
update 储蓄表 set A.余额=A.余额-500 where 账号名='A';
②B账号上要增加500元
update 储蓄表 set B.余额=B.余额+500 where 账号名='B';
如果没有事务处理这个功能,上面的情况下,很可能会发生这样的情况:
①执行成功 A的余额变为:500
刚开始执行②的时候,突然出现某系统系统错误,导致②执行失败!
①√②×:A的钱减少了,B的钱没增加!
所以在类似的场景需求中我们需要事务处理:实现将①和②的SQL语句绑定在一起,要么都执行成功,没的事! 要么不管是①执行出错还是②执行出错,数据库里的数据状态会回滚到没有执行任何①或②里的SQL语句之前!
在具体讲事务之前,还要说说MySQL数据库中的存储引擎: innoDB
- 什么是存储引擎:在mysql中的数据是用各种不同的技术来存储在磁盘文件(或内存)当中的,这种具体的存储技术就是我们说的存储引擎.
- 我们可以通过show engines;命令来查看mysql支持的存储引擎.
- 在mysql可以选择的这些存储引擎中,innodb,myisam,memory这个三个是最常用的,但是其中只有innodb支持事务处理,而其他是不支持事务处理的.
事务的ACID特点:
- 原子性(Atomicity):组成事务的SQL语句不可在分,要么都执行,要么都不执行.
- 一致性(Consistency):事务必须让数据的数据状态变化到另一个一致性的状态,比如:刚刚的例子中A和B的余额总和是2000,转账后,A和B的余额总和不能变.前后具有一致性.
- 隔离性(Isolation):一个事务的执行,不受其他事务的干扰,相互应该是隔离的,但是实际上是很难做到的,要通过隔离级别做选择!
- 持久性(Durability):一个事务被提交,并成功执行,那么它对数据的修改就是永久性的.接下来的其他操作或出现的故障,不能影响到它执行的结果!
MySQL的事务的创建
①隐视事务:事务没有明显的开始和结束的标记.这时候像insert语句,update语句和delete语句,每一条SQL语句就默认是一个事务.
显然,隐视事务在类似转账的逻辑业务需求的时候,就无法处理了!
②显示事务:说白了,这个事务模式,就要我们中程序中手动的用命令来开启事务,和结束事务,并让事务里的多条SQL语句去执行.
注意:默认MySQL是开启自动提交事务的,用show variables like 'autocommit';命令可以查看到. 所以开启显示事务前,要关掉它,用set autocommit=0;只对本身回话有效.
①:开始事务
set autocommit=0;
start transaction; #可选的,执行set autocommit=0已经默认开启了!
②:编写事务中的SQL语句(主要是:select update delete insert等语句)
语句1;语句2;........
③:结束事务
commit;提交事务去真正执行
rollback;回滚事务,恢复数据库执行前的状态!
JDBC的事务
为演示此部分知识:我们在数据库中新建一张表
外键关系
表有三列
主键:_id自增
外键:关联user表中的_id
账户余额:balance
可以看到小三和小四各有1000元余额
演示没有事务处理的情景
/** * 测试事务(无事务处理情况) */ @Test public void testTransAction1() { Connection con = null; PreparedStatement pst = null; ResultSet rs = null; try { con = DBUtils.getConnection(); int transBalance = 500; String userName1= "小三"; String userName2 = "小四"; /** * 更新某个用户名的用户账户余额减少一定的值 */ String sql1 = "UPDATE `account` a INNER JOIN `user` u ON a.user_id =u._id SET a.balance = a.balance-? WHERE u.name=?;"; pst= con.prepareStatement(sql1); pst.setInt(1, transBalance); pst.setString(2, userName1); pst.executeUpdate(); System.out.println("----1---"); int a = 10/0; System.out.println("----2---"); String sql2 = "UPDATE `account` a INNER JOIN `user` u ON a.user_id =u._id SET a.balance = a.balance+? WHERE u.name=?;"; pst= con.prepareStatement(sql2); pst.setInt(1, transBalance); pst.setString(2, userName2); pst.executeUpdate(); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } finally { DBUtils.close(con, pst, rs); } } |
程序运行后数据库中的值变为
很明显,小三的余额觉少了,而小四的余额并没有增加,钱被银行吞了,小三哭死在厕所里。
程序运行结果
演示有事务处理时的情景
/** * 测试事务(有事务处理情况) */ @Test public void testTransAction2() { Connection con = null; PreparedStatement pst = null; ResultSet rs = null; try { con = DBUtils.getConnection(); //开始事务 con.setAutoCommit(false); int transBalance = 500; String userName1 = "小三"; String userName2 = "小四"; /** * 更新某个用户名的用户账户余额减少一定的值 */ String sql1 = "UPDATE `account` a INNER JOIN `user` u ON a.user_id =u._id SET a.balance = a.balance-? WHERE u.name=?;"; pst = con.prepareStatement(sql1); pst.setInt(1, transBalance); pst.setString(2, userName1); pst.executeUpdate(); System.out.println("----1---"); int a = 10 / 0; System.out.println("----2---"); String sql2 = "UPDATE `account` a INNER JOIN `user` u ON a.user_id =u._id SET a.balance = a.balance+? WHERE u.name=?;"; pst = con.prepareStatement(sql2); pst.setInt(1, transBalance); pst.setString(2, userName2); pst.executeUpdate(); //没有异常,提交事务 con.commit(); } catch (Exception e) { e.printStackTrace(); //发生异常,回滚事务 try { con.rollback(); } catch (SQLException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } finally { DBUtils.close(con, pst, rs); } } |
将数据恢复,运行测试,虽然报了异常,但是用户的余额是正确的。
现在将int a = 10 / 0;修改为int a = 10 / 5;进行测试,结果如下,正确。
至此,关于数据库事务在JDBC中的应用和实践,这里介绍完毕。
需要注意事务并发问题
事务并发问题
当有多个事务同时访问数据库中的同一个数据时,如果没有采取必要的隔离机制,就会导致各种并发错误发生:
两个事务t1,t2:
①脏读: 当t2正在更新某个字段但还没有提交数据库执行时,t1在这个时刻正好读取这个字段的数据,然后t2在发生错误,然后回滚数据,导致t1读取到数据就是t2更新时的临时数据,而且最终没有更新成功的无效数据!
②不可重复读:t1读取一个字段是数值,然后t2更新了这个字段,之后t1在读取同一个字段,值发生了变化!
③幻读:t1读取一个字段是数值,然后t2对这个字段插入新数值,t1在读突然就多了几行数据.
数据库事务的隔离性:数据库必须具有隔离这种并发运行的事务的能力,避免这些个错误现象!
一个事务与其他事务隔离的程度称为隔离级别:数据库规定了事务隔离级别,不同隔离级别对应不同的干扰程度,级别越高,数据一致性越好,但并发性越弱!
MySQL数据库支持四个不同隔离级别,这四个有底到高的级别是:
read uncommitted; 读未提交数据
read commited; 读已提交数据
repeatable read; 可重复读(默认)
serialable; 串行化
结论
第一个级别,前面讲的问题现象都是存在的.
第二级别: 脏读没有了,但不可重复读和幻读仍然有!
第三级别:没有脏读,没有不可重复读,仍然有幻读!
第四个级别:可以解决所有问题! 会使性能十分低下!