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 例子

-- 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 ;

<mstyle mathcolor="&#35;f01"> </mstyle> \color{#f01}{账户余额没有变化}

提交 commit

回到一开始的窗口

-- 提交事务
commit;
select * from acc ; 

再到另外一个窗口

	select * from acc ; 

<mstyle mathcolor="&#35;f01"> </mstyle> \color{#f01}{账户余额同时变化了!!}

事务回滚 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) - 演示 - <mstyle mathcolor="&#35;f01"> </mstyle> \color{#f01}{必须避免!}

<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 设置隔离级别( <mstyle mathcolor="&#35;f01"> </mstyle> \color{#f01}{重要!} )

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>