11 数据库事务
今日目标:
(1)了解事务的作用
(2)掌握事务的四大特性(面试)
(3)了解事务的三个读问题
(4)掌握mysql开启和结束事务
(5)了解事物的四个隔离级别
11.1 事务及四大特性
11.1.1 什么是事务
数据库事务(Database Transaction),是指作为单个逻辑工作单元执行的一系列操作,<mark>要么完全地执行,要么完全地不执行</mark>。
简单的说:
事务就是将一堆的SQL语句(通常是增删改操作)绑在一起执行,
要么都执行成功,要么都执行失败,
即都执行成功才算成功,
否则就会恢复到这堆SQL执行之前的状态。
如:
A --> B 转账
A - 500:update 账号表 set money=money-500 where name='A'
B + 500 :update 账号表 set money=money+500 where name='B'
下面以银行转账为例,张三转100块到李四的账户,这至少需要两条SQL语句:
- 给张三的账户减去100元;
update 账户表 set money=money-100 where name='张三';
- 给李四的账户加上100元。
update 账户表 set money=money+100 where name='李四';
说明:
.
如果在第一条SQL语句执行成功后,
在执行第二条SQL语句之前,
程序被中断了(可能是抛出了某个异常,也可能是其他什么原因),
.
那么李四的账户没有加上100元,
而张三却减去了100元,
在现实生活中这肯定是<mark>不允许</mark>的。
.
如果在转账过程中加入事务,
则整个转账过程中执行的所有SQL语句会在一个事务中,
而事务中的所有操作,
要么全都成功,
要么全都失败,
不可能存在成功一半的情况。
.
也就是说给张三的账户减去100元如果成功了
,那么给李四的账户加上100元的操作也必须是成功的
;否则,给张三减去100元以及给李四加上100元都是失败的。
11.1.2 事务的四大特性
事务的四大特性(ACID)是:
- <mark>原子性(Atomicity)</mark>:
事务中所有操作是不可再分割的原子单位。
事务中所有操作要么全部执行成功,要么全部执行失败。 - <mark>一致性(Consistency)</mark>:
事务执行后,数据库状态与其它业务规则保持一致。
如转账业务,无论事务执行成功与否,参与转账的两个账号金额之和应该是不变的。 - <mark>隔离性(Isolation)</mark>:
隔离性是指在<mark>并发操作中</mark>,不同事务之间应该隔离开来,使每个并发中的事务不会相互干扰。
也就是说,在事中务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
例如:
在A事务中,查看另一B事务(正在修改张三的账户金额)中张三的账户金额,
要么查看到B事务之前的张三的账户金额,
要么查看到B事务之后张三的账户金额。
事务1 开始 ----
A - 500 500
B + 500 1500
事务1 结束 —
.
事务2开始-----
查询A和B的账号金额之和
事务2结束----<mark>事务2 应该看到 事务1开始前的状态】
或者【事务2 应该看到事务1结束时候的状态】</mark>
- <mark>持久性(Durability)</mark>:
一旦事务提交成功,
事务中所有的数据操作都必须被持久化到数据库中,
即使提交事务后,数据库马上崩溃,在数据库重启时,也必须能保证通过某种机制恢复数据。
11.2 MySQL中的事务
在默认情况下,
MySQL每执行一条SQL语句,都是一个单独的事务。
如果需要在一个事务中包含多条SQL语句,那么需要开启事务和结束事务。
- 开启事务:
- start transaction;
- begin;
- 结束事务:
- commit(提交事务)或
- rollback(回滚事务)。
在执行SQL语句之前,
<mark>先执行strat transaction</mark>,
这就开启了一个事务(事务的起点),
然后可以去<mark>执行多条SQL语句</mark>,
最后要<mark>结束事务</mark>,
<mark>commit表示提交</mark>,即事务中的多条SQL语句所做出的影响会持久化到数据库中。
或者<mark>rollback,表示回滚</mark>,即回滚到事务的起点,之前做的所有操作都被撤消了!
jt_db 例子
- 创库代码 : 《MySQL 脚本 - jt_db》 - https://blog.csdn.net/LawssssCat/article/details/103592280
-- A给B转账500元
use jt_db ;
show tables ; -- 看有没有 accbiao 表
-- 开始事务
start transaction ; -- A给B转账500元
update acc set money=money-500 where name='A' ;
update acc set money=money+500 where name='B';
-- 回车,还没结束事务。。。
select * from acc ;
-- 事务里查看 是成功的!。但是再开个窗口数据没变
另外开一个mysql
mysql -uroot -proot --default-character-set=gbk use jt_db ; select * from acc ;
账户余额没有变化
提交 commit
回到一开始的窗口
-- 提交事务
commit;
select * from acc ;
再到另外一个窗口
select * from acc ;
账户余额同时变化了!!
事务回滚 rollback
事务中断 quit
<mark>数据没变</mark>
11.3 事务并发读问题
11.3.1 事务并发读问题
多个事务对相同的数据同时进行操作,这叫做事务并发。
在事务并发时,
如果没有采取必要的隔离措施,
可能会导致各种并发问题,
破坏数据的完整性等。
这些问题中,
其中有三类是读问题,分别是:<mark>脏读、不可重复读、幻读</mark>。
- <mark>脏读(dirty read)</mark>:
读到另一个事务的未提交更新数据,即读取到了脏数据;
例如:
A给B转账100元但未提交事务,
在B查询后,
A做了回滚操作,
那么B查询到了A未提交的数据,
就称之为脏读。 - <mark>不可重复读(unrepeatable read)</mark>:
对同一记录的两次读取不一致,因为另一事务对该记录做了修改(是针对修改操作)
例如:在事务1中,前后两次查询A账户的金额,在两次查询之间,另一事物2对A账户的金额做了修改,此种情况可能会导致事务1中,前后两次查询的结果不一致。这就是不可重复度 - 幻读(虚读)(phantom read):
对同一张表的两次查询不一致,因为另一事务插入了一条记录(是针对插入或删除操作);
注意:
mysql<mark>默认</mark>的是<mark>不允许</mark>出现 <mark>脏读</mark> 和 <mark>不可重复读</mark>,
所以
在下面演示之前需要<mark>设置</mark>mysql<mark>允许</mark>出现脏读、不可重复读等。
脏读(dirty read) - 演示 - 必须避免!
<mark>准备</mark>
use jt_db ;
update acc set money=1000 ;
select * from acc ;
<mark>未降低隔离级别</mark>
--窗口1
mysql -uroot -proot --default-character-set=gbk
use jt_db ;
start transaction ;
update acc set money=money-500 where name='A' ;
update acc set money=money+500 where name='B' ;
select * from acc ;
--窗口2
mysql -uroot -proot --default-character-set=gbk
use jt_db ;
select * from acc ;
<mark>设置最低隔离级别</mark>
--窗口1
rollback ;
set tx_isolation='read-uncommitted';
--窗口2
set tx_isolation='read-uncommitted';
<mark>执行之前同样操作</mark>
--窗口1
mysql -uroot -proot --default-character-set=gbk
use jt_db ;
start transaction ;
update acc set money=money-500 where name='A' ;
update acc set money=money+500 where name='B' ;
select * from acc ;
--窗口2
mysql -uroot -proot --default-character-set=gbk
use jt_db ;
select * from acc ;
结果一样了!!
<mark>窗口1rollback</mark>
--窗口1 rollback
rollback ;
select * from acc ;
--窗口2
select * from acc ;
不可重复读示例
-- 在窗口1中,开启事务,查询A账户的金额
set tx_isolation='read-uncommitted'; -- 允许脏读、不可重复读、幻读
use jt_db; -- 选择jt_db库
start transaction; -- 开启事务
select * from acc where name='A';
-- 在窗口2中,开启事务,查询A的账户金额减100
set tx_isolation='read-uncommitted'; -- 允许脏读、不可重复读、幻读
use jt_db; -- 选择jt_db库
start transaction; -- 开启事务
update acc set money=money-100 where name='A'; -- A账户减去100
select * from acc where name='A';
commit; -- 提交事务
-- 切换到窗口1,再次查询A账户的金额。
select * from acc where name='A'; -- 前后查询结果不一致
在窗口1中,前后两次对同一数据(账户A的金额)查询结果不一致,是因为在两次查询之间,另一事务对A账户的金额做了修改。此种情况就是"不可以重复读"
幻读示例
-- 在窗口1中,开启事务,查询账户表中是否存在id=3的账户
set tx_isolation='read-uncommitted'; -- 允许脏读、不可重复读、幻读
use jt_db; -- 选择jt_db库
start transaction; -- 开启事务
select * from acc where id=3;
-- 在窗口2中,开启事务,往账户表中插入了一条id为3记录,并提交事务。
-- 设置mysql允许出现脏读、不可重复度、幻读
set tx_isolation='read-uncommitted';
use jt_db; -- 选择jt_db库
start transaction; -- 开启事务
insert into acc values(3, 'C', 1000);
commit; -- 提交事务
-- 切换到窗口1,由于上面窗口1中查询到没有id为3的记录,所以可以插入id为3的记录。
insert into acc values(3, 'C', 1000); -- 插入会失败!
在窗口1中,查询了不存在id为3的记录,所以接下来要执行插入id为3的记录,但是还未执行插入时,另一事务中插入了id为3的记录并提交了事务,所以接下来窗口1中执行插入操作会失败。
探究原因,发现账户表中又有了id为3的记录(感觉像是出现了幻觉)。这种情况称之为"幻读"
以上就是在事务并发时常见的三种并发读问题,那么如何防止这些问题的产生?
可以通过设置事务隔离级别进行预防。
11.3.2 事务隔离级别
事务隔离级别分<mark>四个等级</mark>,
在相同数据环境下,
对数据执行相同的操作,
设置不同的隔离级别,
可能导致不同的结果。
不同事务隔离级别能够解决的数据并发问题的能力也是不同的。
set tx_isolation='read-uncommitted';
1、READ UNCOMMITTED(读未提交数据)【无锁,未提交】
安全级别最低, 可能出现任何事务并发问题(比如脏读、不可以重复读、幻读等)
性能最好(<mark>不使用!!</mark>)
2、READ COMMITTED(读已提交数据)(Oracle默认)【无锁,提交】
防止脏读,没有处理不可重复读,也没有处理幻读;
性能比REPEATABLE READ好
3、REPEATABLE READ(可重复读)(MySQL默认)【锁行】
防止脏读和不可重复读,不能处理幻读问题;
性能比SERIALIZABLE好
4、SERIALIZABLE(串行化)【锁表】
不会出现任何并发问题,因为它是对同一数据的访问是串行的,非并发访问的;
<mark>性能最差</mark>;
MySQL的默认隔离级别为REPEATABLE READ,即可以防止脏读和不可重复读
11.3.3 设置隔离级别( 重要!)
0、MySQL查询当前的事务隔离级别
select @@tx_isolation;
1、MySQL设置事务隔离级别(了解)
(1) set tx_isolation='read-uncommitted';
安全性最差,容易出现脏读、不可重复读、幻觉读,但性能最高
(2) set tx_isolation='read-committed';
安全性一般,可防止脏读,但容易出现不可重复读、幻觉读
(3) set tx_isolation='repeatable-read';
安全性较好,可防止脏读、不可重复读,但是容易出现幻读
(4) set tx_isolation='serialiable';
安全性最好,可以防止一切事务并发问题,但是性能最差。
2、JDBC设置事务隔离界别
JDBC中通过Connection提供的方法设置事务隔离级别:
Connection. setTransactionIsolation(int level)
参数可选值如下:
Connection.TRANSACTION_READ_UNCOMMITTED 1(读未提交数据)
Connection.TRANSACTION_READ_COMMITTED 2(读已提交数据)
Connection.TRANSACTION_REPEATABLE_READ 4(可重复读)
Connection.TRANSACTION_SERIALIZABLE 8(串行化)
Connection.TRANSACTION_NONE 0(不使用事务)
提示:在开发中,一般情况下不需要修改事务隔离级别
3、JDBC中实现转账例子
提示:
JDBC中默认是自动提交事务,
所以需要关闭自动提交,
改为手动提交事务
也就是说, 关闭了自动提交后, 事务就自动开启, 但是执行完后需要手动提交或者回滚!!
-
执行下面的程序,程序执行没有异常,转账成功!
A账户减去100元,B账户增加100元。 -
将第4步、5步中间的代码放开,再次执行程序,在转账过程中抛异常,转账失败!
由于事务回滚,所以A和B账户金额不变。
JdbcUtil 工具
package cn.edut.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class JdbcUtil {
/** * 获得连接器 */
public static Connection getConn() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(
"jdbc:mysql://127.0.0.1:3306/jt_db?characterEncoding=gbk",
"root",
"root"
);
return conn;
}
/** * 关闭传入的连接 */
public static <T extends AutoCloseable> void close(T ... ts) {
for (T t : ts) {
if(t!=null) {
try {
t.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
t = null;
}
}
}
}
}
Test类
package cn.edut.jdbc;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.junit.Test;
public class TestJdbcTransaction {
@Test
public void testJdbcJdbcUtilTransaction() throws SQLException {
Connection conn = null;
Statement stat = null;
ResultSet rs = null;
try {
//1.获取连接
conn = JdbcUtil.getConn();
//2.关闭JDBC自动提交事务(默认开启事务)
conn.setAutoCommit(false);
//3.获取传输器
stat = conn.createStatement();
/* ***** A给B转账100元 ***** */
//4.A账户减去100元
String sql = "update acc set money=money-100 where name='A'";
stat.executeUpdate(sql);
//int i = 1/0; // 让程序抛出异常,中断转账操作
//5.B账户加上100元
sql = "update acc set money=money+100 where name='B'";
stat.executeUpdate(sql);
//6.手动提交事务
conn.commit();
System.out.println("转账成功!提交事务...");
} catch (Exception e) {
e.printStackTrace();
//一旦其中一个操作出错都将回滚,使两个操作都不成功
conn.rollback();
System.out.println("执行失败!回滚事务...");
} finally{
JdbcUtil.close(conn, stat, rs);
}
}
}
事务执行成功
事务执行失败
<mark>放开一行注释</mark>