目录
事务(Transaction)
1.解释:
这个单词的指向含义的广泛的,并不一定就特指我们数据库中的事务处理
为什么出现事务:维护数据的一致性 + 一个业务动作有多条 SQL 语句组成
2.狭义:
在业务方(开发者)看来,就是一个不可再分的业务动作,这个动作最终表达为一条或者多条SQL语句。
3.事务的四个特性:ACID
Atomic(原子性):
业务动作对应的 SQL 应该是看作一个整体,不可再分的。针对数据的修改只能 All or None。
Consistency(一致性):
业务方来定义的,针对数据整体做的不可变的承诺。
lsolation(隔离性):
当有多个 DBMS 的用户,同时针对数据做增删查改时,是否互相直接保持隔离的特性。最理想的情况下,一个用户在操作数据时,是意识不到其他用户同时也在操作数据的。
Durability(持久性):
一旦 DBMS 通知我们数据修改成功了,则数据必然修改成功了(修改被持久化了)。
4.一些区分
Consistency(最终目标)需要 DBMS 和 我们(程序员)共同付出努力来进行维护的
Atomic(原子性)lsolation(隔离性)Durability(持久性):这三个属于 DBMS 对我们的承诺,属于 DBMS 的责任
DBMS承诺支持事务,指的就是 A、D、I (C 是做不到的)
Atomic(原子性)Durability(持久性):这两个,是单用户场景下的,关于数据存储和恢复问题的
lsolation(隔离性):多用户场景下的,,属于并发控制的内容
5.我们如何使用事务
(我们和 DBMS 之间在进行数据交换)
DBMS 不知道 SQL 是怎么划分成不同的事务的
第一个角度:我们如何在 SQL 中使用事务
strat transaction; 告诉 DBMS 一个事务明确开启了(多条 SQL 组成的事务)
执行属于这个事务的 SQL
commit; 事务执行完成
比如,以上图为例:
start transaction;
sql1;
sql2;
sql3;
commit; // 代表事务1完成了
sql4; // 事务中只有一条 sql,所以可以省略分界
start transaction;
sql5;
sql6;
commit; // 代表事务3完成了
第二个角度:通过 JDBC 的方式来使用事务
4个场景:
1.有事务,commit
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
// 一切正常下的 sql 演示
public class Demo1 {
public static void main(String[] args) throws SQLException {
String sql1 = "insert into records (rid, bid) values (1, 2)";
String sql2 = "update books set count = count - 1 where bid = 2";
// 要使用事务,在同一个事务中,操作 sql1 和 sql2,意味着必须在一条 Connection 完成
try(Connection c = DBUtil.connection()){
// connection 中有一个 自动提交(autocommit)的属性,默认情况下,是 true(开启)
// 开启状态下,意味着,每一条 sql 都会被独立的视为一个事务
// 我们要让 sql1 和 sql2 看作整体,只需要关闭 connection 的自动提交
c.setAutoCommit(false); // 不会提交,只能被我们进行手动提交
// 我们就可以手动的控制事务的结束位置
try(PreparedStatement ps = c.prepareStatement(sql1)){
ps.executeUpdate();
}
try(PreparedStatement ps = c.prepareStatement(sql2)){
ps.executeUpdate();
}
// 由于我们关闭了自动提交,所以,所修改的还没有真正的落盘
c.commit(); // 只有加上这句,事务才被真正的提交了
}
}
}
2.没有事务,被动失败(重启服务器)
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
// 没有事务,被动失败(重启服务器)
public class Demo2 {
public static void main(String[] args) throws SQLException {
String sql1 = "insert into records (rid, bid) values (1, 2)";
String sql2 = "update books set count = count - 1 where bid = 2";
try(Connection c = DBUtil.connection()){
try(PreparedStatement ps = c.prepareStatement(sql1)){
ps.executeUpdate();
}
// 执行完第一条 SQL 之后,要让第二条 SQL 执行失败
try(PreparedStatement ps = c.prepareStatement(sql2)){
ps.executeUpdate();
}
// 由于我们关闭了自动提交,所以,所修改的还没有真正的落盘
c.commit(); // 只有加上这句,事务才被真正的提交了
}
}
}
在两条sql语句中打上断点,执行第一条不执行第二条,关闭服务器,执行第二条,会出现错误提示
在Workbench中可以看到第一条语句执行成功了,第二条语句执行失败。
3.有事务,被动失败(重启服务器)
// 有事务,被动失败(重启服务器)
public class Demo3 {
public static void main(String[] args) throws SQLException {
String sql1 = "insert into records (rid, bid) values (1, 2)";
String sql2 = "update books set count = count - 1 where bid = 2";
// 要使用事务,在同一个事务中,操作 sql1 和 sql2,意味着必须在一条 Connection 完成
try(Connection c = DBUtil.connection()){
c.setAutoCommit(false);
try(PreparedStatement ps = c.prepareStatement(sql1)){
ps.executeUpdate();
}
// 执行完第一条 SQL 之后,要让第二条 SQL 执行失败
try(PreparedStatement ps = c.prepareStatement(sql2)){
ps.executeUpdate();
}
c.commit();
}
}
}
在两条sql语句中打上断点,执行第一条不执行第二条,关闭服务器,执行第二条
在Workbench中可以看到两条语句都没有执行成功,这是事务起到了作用
4.有事务,被动失败(程序导致)
// 有事务,主动失败
public class Demo4 {
public static void main(String[] args) throws SQLException {
String sql1 = "insert into records (rid, bid) values (1, 2)";
String sql2 = "update books set count = count - 1 where bid = 2";
// 要使用事务,在同一个事务中,操作 sql1 和 sql2,意味着必须在一条 Connection 完成
try(Connection c = DBUtil.connection()){
c.setAutoCommit(false);
try(PreparedStatement ps = c.prepareStatement(sql1)){
ps.executeUpdate();
}
// 执行完第一条 SQL 之后,要让第二条 SQL 执行失败
// 由于我们代码中的异常导致的被动失败
int a = 1 / 0; // 一定会出现 除0 异常,下面的代码就不会执行
// 我们没有 catch 除 0 异常,导致异常交给 JVM 去处理
// 第一条 SQL 成功、第二条 SQL 失败
try(PreparedStatement ps = c.prepareStatement(sql2)){
ps.executeUpdate();
}
c.commit();
}
}
}
执行这段代码,由于中间有异常,所以第二段代码不会被执行,Workbench中的数据不会有变化。
5.有事务,主动失败(rollback)
// 有事务,主动失败
public class Demo5 {
public static void main(String[] args) throws SQLException {
String sql1 = "insert into records (rid, bid) values (1, 2)";
String sql2 = "update books set count = count - 1 where bid = 2";
// 要使用事务,在同一个事务中,操作 sql1 和 sql2,意味着必须在一条 Connection 完成
try(Connection c = DBUtil.connection()){
c.setAutoCommit(false);
try(PreparedStatement ps = c.prepareStatement(sql1)){
ps.executeUpdate();
}
try(PreparedStatement ps = c.prepareStatement(sql2)){
ps.executeUpdate();
}
c.rollback(); //主动失败
}
}
}
执行程序,虽然两段 sql 都执行了,但最后没有使用commit 使用了 rollback主动失败,所以Workbench中的数据不会被改变。
6.通过事务保护了原子性Atomic:
三个场景:
1.开启事务,一切正常情况下,数据会被修改成功(所有 sql 都成功执行了)
2.模拟没有事务时,需要数据库服务器故障的情况
1.先执行 sql1
2.重启服务器
3.观察数据(一致性被破坏了)
3.模拟有事务时,需要数据库服务器故障的情况
1.先执行 sql1
2.重启 mysql 服务器
3.观察数据(一致性没有被破坏,部分修改的数据被回滚了)
事务只有两个结局:成功(All) 或者 失败(None)。无论成功还是失败,数据的一致性是不能被破坏的。
失败:
1.被动失败:硬件的原因 和 软件的原因(DBMS 本身的原因 和 我们的原因(比如异常))
由于没有执行 commit 所以,事务期间做的“部分修改” 都会被 回滚(rollback)
2.主动失败:我们可以主动要求事务失败(数据回滚)——不去执行 commit ,而去执行 rollback
7.事务的lsolation(隔离性):
多个用户,“同时”操作,同一个资源时,表现为互相之间是互不干扰的。
这句话是 隔离性 追求的理想状态。但实际上,如果精度够高,这个现象是永远达不到的。(时间精度、空间精度)
所以,如果要追求真正的隔离性,实际上是以牺牲并发性(真正的同时)为代价的。
所以 SQL 标准制定了隔离级别(isolation level)
隔离级别(从左往右:从追求隔离性到追求并发性)
serializeable(可串行性)、snapshot read(快照读)、repeatable read(可重复读)、read committed(读已提交)、read uncommitted(读未提交)
7.1 read uncommitted(读未提交)
多个在执行的事务,可以读取到(看到)其他事务,还处于事务中未提交时的数据修改(完全没隔离)
这种情况,相当于,t2 读到了“脏数据(dirty data)”,这种副作用被称为—脏读(dirty read),也有不可重复读
7.2 read committed(读已提交)
可以看到别的事务已经提交修改的数据
在同一个事务中(t2),可能出现多次读取同一份数据,得到的结果却是不同的,这个副作用被称为—不可重复读。这里没有脏读了
7.3 repeatable read(可重复读)
保证了再一次事务中(只要没有提交或者回滚),看到的值是不会变化的(即使有别的事务对这个数据做过修改,并且已经提交了)
只针对表中已有的数据做保护,对于新添加的数据没有保护
7.4 snapshot read(快照读)
快照读,并不是标准中存在的隔离级别
快照读,做到了连幻读这个副作用都没有了
MySQL 中的“可重复读”可以看作实际上是“快照读” ——MVCC
MySQL 的默认情况下,隔离级别就是可重复读
7.5 serializeable(可串行性)
每个事务都必须排队执行,一次只执行一条事务,在宏观视角上,仍然可以看作“同时”,并发性(性能)差。