- 处理思路
- 核心思路:
让转账的两个动作:减钱,加钱 必须同时成功或者是同时失败。
- 可选技术:
数据库的事务。
- 事务的概述
- 事务指的是逻辑上的一组操作,组成这组操作的各个单元要么全都成功,要么全都失败.
- 事务作用:保证一组操作要么全都成功,对数据库进行完整更新。要么在某一个动作失败的时候让数据恢复原状,不会引起不完整的修改。
- MySQL事务的操作
sql语句 |
描述 |
start transaction; |
开启事务 |
commit; |
提交事务(完整更新) |
rollback; |
回滚事务(恢复原状) |
MYSQL中可以有两种方式进行事务的管理:
自动提交:MySql默认自动提交。即执行一条sql语句提交一次事务。
手动提交:先开启,再提交
方式1:手动提交(当执行手动提交的时候自动提交会暂停)
start transaction; update account set money=money-1000 where name='守义'; update account set money=money+1000 where name='凤儿'; commit; #或者 rollback;
- 方式2:自动提交,通过修改mysql全局变量“autocommit”进行控制
show variables like '%commit%';
* 设置自动提交的参数为OFF:
set autocommit = 0; -- 0:OFF 1:ON
- &bsp;&bsp;测试利用事物实现转账1块钱 顺利完成情况
- START TRANSACTION; -- 开启事物
-- 执行一组操作
UPDATE account SET money=money-1 WHERE NAME='张三';
UPDATE account SET money=money+1 WHERE NAME='李四';
COMMIT; -- 提交事物
执行前:
执行后:
- &bsp;&bsp;测试利用事物实现转账1块钱 出现问题并回滚(恢复原状)
# 测试利用事物实现转账1块钱
START TRANSACTION; -- 开启事物
-- 执行一组操作
UPDATE account SET money=money-1 WHERE NAME='张三';
-- 下一句发生错误
UPDATE account SET money=money+1 WHERE NAME &……&%&……¥(*&* ='李四';
ROLLBACK; -- 回滚(恢复原状)
执行前:
执行如下两句:
执行效果:
执行下面一句
这一组操作的第二个给李四加钱执行失败了 ,李四的钱并不会改变
此时一组操作没有全部成功,需要回滚来让数据恢复原状
- &bsp;JDBC事务操作
Connection对象的方法名 |
描述 |
conn.setAutoCommit(false) |
设置关闭自动提交,(开启事务) |
conn.commit() |
提交事务 |
conn.rollback() |
回滚事务 |
利用如下模板解决问题
//事务模板代码 public void demo01() throws SQLException{ // 获得连接 Connection conn = ...; try { //#1关闭自动提交事物(开始事务) conn.setAutoCommit(false); //.... 加钱 ,减钱 //#2 手动提交事务 conn.commit(); } catch (Exception e) { //#3 手动回滚事务 conn.rollback(); } finally{ // 释放资源 conn.close(); } }
- 解决问题
- Dao层
把原来的两个方法进行修改,使用Service层传递过来的conn对象,并且执行完毕不要关闭链接
/* * 从指定的账户中取钱(减钱) * */ public void outMoney(String name,int money,Connection conn) throws Exception{ //2.发射器 String sql = "UPDATE account SET money=money-? WHERE NAME=?"; PreparedStatement pst = conn.prepareStatement(sql); pst.setInt(1, money); pst.setString(2, name); //3.发射 int num = pst.executeUpdate(); //4.处理结果 System.out.println("本次执行的影响的行数是: num="+num); //5.关闭资源 只关闭结果集不关闭链接 DruidUtil.closeAll(null, pst, null); } /* * 从指定的账户中存钱(加钱) * */ public void inMoney(String name,int money,Connection conn) throws Exception{ //2.发射器 String sql = "UPDATE account SET money=money+? WHERE NAME=?"; PreparedStatement pst = conn.prepareStatement(sql); pst.setInt(1, money); pst.setString(2, name); //3.发射 int num = pst.executeUpdate(); //4.处理结果 System.out.println("本次执行的影响的行数是: num="+num); //5.关闭资源 只关闭结果集不关闭链接 DruidUtil.closeAll(null, pst, null); }
- Service层
获取连接,关闭自动提交变成手动提交,一组动作成功则手动提交事务,一旦有异常则回滚,最后无论异常与否都要关闭连接
public void transfer(String srcName,String descName,int money){ AccountDao ad = new AccountDao(); Connection conn =null; try { //获取链接 conn = DruidUtil.getConn(); //把自动提交关闭,变成手动提交 conn.setAutoCommit(false); // -钱 传递连接对象 ad.outMoney(srcName, money,conn); //制造一个bug , 模拟转账出现问题 int a=1/0; // +钱 传递连接对象 ad.inMoney(descName, money,conn); //转账成功则手动提交事物 conn.commit(); System.out.println("转账完毕 "); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); if(conn!=null){ try { //操作失败 回滚 conn.rollback(); System.out.println("执行了回滚 ,把数据恢复原状 "); } catch (SQLException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } }finally { if(conn!=null){ try { //关闭链接 conn.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
- 测试
- 正常情况:
把 如下代码注释上
执行前:
转账2块钱执行完毕
执行结果
- 异常情况:
保留如下代码
执行前:
执行效果
执行后:数据恢复原状,问题解决
- 理论补充
事务特性:ACID
原子性(Atomicity)原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性(Consistency)事务前后数据的完整性必须保持一致。
隔离性(Isolation)事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。
持久性(Durability)持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
并发访问问题
如果不考虑隔离性,事务存在3种并发访问问题。
- 脏读:一个事务读到了另一个事务未提交的数据.
- 不可重复读:一个事务读到了另一个事务已经提交(update)的数据。引发另一个事务,在事务中的多次查询结果不一致。
虚读 /幻读:一个事务读到了另一个事务已经提交(insert)的数据。导致另一个事务,在事务中多次查询的结果不一致。(数据量不同)
严重性: 脏读 > 不可重复读 >虚读(幻读)
设置隔离级别:解决问题
数据库规范规定了4种隔离级别,分别用于描述两个事务并发的所有情况。
read uncommitted 读未提交,一个事务读到另一个事务没有提交的数据。
存在:3个问题(脏读、不可重复读、虚读)。
解决:0个问题
效率最高,引发所有读问题
基本不设置
read committed 读已提交,一个事务读到另一个事务已经提交的数据。
存放:2个问题(不可重复读、虚读)。
解决:1个问题(脏读)
如果要 效率,那么选择这个read committed
repeatable read :可重复读,在一个事务中读到的数据信息始终保持一致,无论另一个事务是否提交。
存放:1个问题(虚读)。
解决:2个问题(脏读、不可重复读)
如果 要求安全,选择这个repeatable read
虚读的问题可以通过程序来规避:
事务刚开启时,可以count(*)
事务要关闭时,可以count(*)
比对,如果两次数据一致,说明没有虚读
serializable 串行化,同时只能执行一个事务,相当于事务中的单线程。
存放:0个问题。
解决:1个问题(脏读、不可重复读、虚读)
没有效率,安全性最高,基本不设置
安全和性能对比
安全性:serializable > repeatable read > read committed > read uncommitted
性能 : serializable < repeatable read < read committed < read uncommitted
常见数据库的默认隔离级别:
MySql:repeatable read 安全,本身做的优化比较好
Oracle:read committed 效率