事务
1.事务概述
-
什么是事务
Transaction 其实指的是一组操作,里面包含许多个单一的逻辑。只要有一个逻辑没有执行成功,那么都算失败。 所有的数据都回归到最初的状态(回滚)。
-
为社么要有事务
为了确保逻辑的成功。如银行的转账。
-
事务的特性
-
原子性
指事务中包含的逻辑不可分割。
-
一致性
指事务执行前后数据的完整性。
-
隔离性
指事务在执行期间不应该受到其它事务的影响。
-
持久性
指事务执行成功后,数据应持久保存到磁盘上。
-
2.演示事务
2.1使用命令行方式演示事务
-
开启事务
start transaction;
-
提交事务
commit;提交事务后,数据将会写到磁盘上的数据库
-
回滚事务
rollback;数据回滚,回到最初的状态
- 关闭自动提交功能
-
开启事务
-
演示事务
原来zhangsan和lisi账户各有1000块,现修改lisi的账户为1100,在事务没有提交的情况下,查出lisi账户为1100,但此时数据并没有写入到磁盘,可使用rollback回滚。
当提交事务后,再查询发现lisi账户确为1100,且无法回滚到1000。
2.2使用代码方式演示事务
模拟转账:
- 不使用事务的情况下:
@Test
public void testTransaction() throws Exception {
Connection con=null;
PreparedStatement ps=null;
ResultSet rs=null;
con=JDBCUtils.getConnection();
String sql="update account set money=money-? where id=?";
ps=con.prepareStatement(sql);
//扣ID为1的100元
ps.setDouble(1, 100);
ps.setInt(2, 1);
ps.executeUpdate();
//模拟出错
int a=10/0;
//加ID为2的100元
ps.setDouble(1, -100);
ps.setInt(2, 2);
ps.executeUpdate();
}
转账出错后,1的钱少了100,2的钱却没有多:
- 使用事务的情况下:
@Test
public void testTransaction2() throws Exception {
Connection con=null;
PreparedStatement ps=null;
ResultSet rs=null;
con=JDBCUtils.getConnection();
try {
con=JDBCUtils.getConnection();
//关闭自动提交。事务默认是自动提交的
con.setAutoCommit(false);
String sql="update account set money=money-? where id=?";
ps=con.prepareStatement(sql);
//扣ID为1的100元
ps.setDouble(1, 100);
ps.setInt(2, 1);
ps.executeUpdate();
//模拟出错
int a=10/0;
//加ID为2的100元
ps.setDouble(1, -100);
ps.setInt(2, 2);
ps.executeUpdate();
//成功,提交事务
con.commit();
}catch(SQLException e) {
//失败,回滚事务
con.rollback();
}
}
转账出错后,1的钱没有少,2的钱也没有多:
3.事务的安全隐患
3.1读
-
脏读
一个事务读到另外一个事务还未提交的数据。
-
幻读
一个事务读到了另一个事务已提交的插入的或删除的数据,导致多次查询结果不一致。
幻读是当事务不是独立执行时发生的一种现象。
-
不可重复读
一个事务读到了另一个事务提交的数据, 导致多次查询结果不一致。
3.2写
-
丢失更新
指一个事务去修改数据库, 另一个事务也修改数据库,最后的那个事务,不管是提交还是回滚都会造成前面一个事务的数据更新丢失。
4.隔离级别
-
读未提交(Read Uncommitted)
一个事务可以读取到另一个事务还未提交的数据。 这就会引发 “脏读” 。读取到的是数据库内存中的数据,而并非真正磁盘上的数据。
演示:
- 开启A窗口,设置隔离级别为读未提交
- 开启B窗口,并分别开启两个连接的事务
- 在B窗口执行sql语句但不提交,在A窗口重新执行查询,看到B窗口没有提交的数据
-
读已提交(Read Committed)
与前面的读未提交刚好相反,这个隔离级别是 ,只能读取到其他事务已经提交的数据,那些没有提交的数据读不出来。但这会造成一个问题: 前后读取到的结果不一样。 发生了不可重复读, 所谓不可重复读,就是不能执行多次读取,否则出现结果不一 。
演示:
-
开启A窗口,设置A窗口的事务隔离级别为读未提交,命令如下:
set session transaction isolation level read committed;
-
开启B窗口,在两个窗口中选择数据库并开启事务,在A窗口执行查询;
-
在B窗口执行sql语句, 但是不提交,在A窗口重新执行查询, 是不会看到B窗口刚才执行sql 语句的结果,因为它还没有提交。在B窗口执行提交,在A窗口中执行查看 这时候才会看到B窗口已经修改的结果。
-
-
重复读(Repeatable Read)
MySQL默认隔离级别。
可以让事务在自己的会话中重复读取数据,并且不会出现结果不一样的情况,即使其他事务已经提交了,也依然还是显示以前的数据。
演示:
-
开启A窗口,设置当前窗口的事务隔离级别为读未提交,命令如下:
set session transaction isolation level repeatable read;
-
开启B窗口, 在两个窗口中选择数据库并开启事务,在A窗口执行查询;
-
在B窗口执行sql语句,但是不提交,在A窗口重新执行查询,结果不变;在B窗口提交事务后,再在A窗口重新查询,发现结果也不变,多次执行查询依然不变。
-
-
可串行化(Serializable)
最高级的事务级别,比前面几种都要强大一点,可以解决脏读、不可重复读、幻读。缺点是造成并发的性能问题。 其他的事务必须得等当前正在操作表的事务先提交,才能接着往下,否则只能一直在等着。
演示:
-
开启A窗口,设置当前窗口的事务隔离级别为可串行化,命令如下:
set session transaction isolation level serializable;
-
开启B窗口, 在两个窗口中选择数据库并开启事务;
开启事务的顺序不同,会导致效果也不同:
-
A先开事务,B再开事务
在A中执行查询,B中执行修改操作,B会卡住,因为A先开启事务,B要等A提交事务后才能执行成功。
-
-
B先开事务,A再开事务
在B中执行修改操作,A中执行查询,A会卡住,A要等B提交事务后才能执行成功。
-
5.四种隔离级别的比较
-
效率
读未提交>读已提交>可重复读>可串行化
-
拦截程度
可串行化>可重复读>读已提交>读未提交
-
比较
读未提交,引发脏读。
读已提交,引发不可重复读,解决脏读。
可重复读,解决脏读,不可重复读,未解决幻读。
可串行化,解决脏读,幻读,不可重复读。
5.写问题解决方法
-
悲观锁
指事务在一开始就认为丢失更新一定会发生, 这是一件很悲观的事情。
具体操作步骤如下:
-
所有事务在执行操作前,先查询一次数据, 查询语句如下:
select * from student for update ;
for update 其实是数据库锁机制 、 一种排他锁;
-
哪个事务先执行这个语句, 哪个事务就持有了这把锁, 可以查询出来数据, 后面的事务再执行这条语句,不会有任何数据显示,就只能等着。
-
一直等到前面的那个事务提交数据后, 后面的事务数据才会出来,那么才可以往下接着操作。
-
-
乐观锁
指从来不会觉得丢失更新会发生。
要求程序员在数据库中添加字段,然后在后续更新的时候,对该字段进行判定比对, 如果一致才允许更新。乐观锁的机制 ,其实是通过比对版本或者比对字段的方式来实现的。
具体操作步骤如下:
- 数据库表中,额外添加了一个version字段, 用于记录版本, 默认从0 开始, 只要有针对表中数据进行修改的,那么version就+1;
- 开启A事务, 然后开启B事务 ;
- A 先执行数据库表操作。 因为以前都没有人修改过。 所以是允许A事务修改数据库的,但是修改完毕,就把version的值变成 1 了;
- B事务, 这时候如果想执行修改,那么是不允许修改的。 因为B事务以前是没有查询过数据库内容的,所以它认为数据库版本还是0 ,但是数据库的版本经过A修改,已经是1了。
所以这时候不允许修改, 要求其重新查询 ; - B重新查询后, 将会得到version 为 1的数据,这份数据就是之前A 事务修改的数据了, B 再进行修改,也是在A的基础上修改的,所以就不会有丢失更新的情况出现了。