文章目录

  • 数据库事务
  • 事务的四大特性
  • MySQL中的事务
  • 例子
  • 提交 commit
  • 事务回滚 rollback
  • 事务中断 quit
  • 并发问题
  • 事务隔离级别
  • 1、READ UNCOMMITTED(读未提交数据)【无锁,未提交】
  • 2、READ COMMITTED(读已提交数据)(Oracle默认)【无锁,提交】
  • 3、REPEATABLE READ(可重复读)(MySQL默认)【锁行】
  • 4、SERIALIZABLE(串行化)【锁表】
  • jdbc设置事务隔离级别
  • 其他
  • 脏读(dirty read)演示
  • 不可重复读演示
  • 幻读演示
  • 案例:JDBC实现转账例子
  • # JdbcUtil 工具
  • # Test类
  • # 事务执行成功
  • # 事务执行失败


数据库事务

可能每次操作数据库有一堆操作,不好管理。于是,就想出了把相关的操作打个包,从一个更大的面来管理这些操作。这打包之后的操作集合就可以称作事务(Transaction)

e.g.

一堆操作: 更新A、查看B、删除C、更新D、添加E、查看F
按事务划分: 事务一“查账”(更新A、查看B)、事务二“入账”(删除C、更新D、添加E)、事务三“生成报告”(查看F)

如此划分后,我们可以做更高级的安排:

  1. “查账” 没问题了,就可以“入账”,入账后再次“查账”,校对没问题了最后“生成报告”
  2. 💡 如果中间校对环节出了问题,全部“入账”数据回滚,重新任务

能体会到事务的作用了吗?

事务的四大特性

事务的四大特性(ACID)是:

  • 原子性(Atomicity)
    一个事务内部多个操作的结果,要么全部提交,要么全部回滚。即不允许提交一部分,回滚一部分。
  • 一致性(Consistency)
    一个事务内部多个操作的结果应该与它们摆脱事务,单独执行的结果一致。
  • 隔离性(Isolation)
    多个事务不允许同时操作(增删查改)同一块数据。
  • 持久性(Durability)
    一个事务的修改要么提交,要么回滚。如果事务提交了,那么修改的数据应该生效。

MySQL中的事务

在默认情况下,MySQL一条SQL语句,一个单独的事务。

如果需要一个事务中包含多条SQL语句,那么需要执行以下命令

# 开启事务
start transaction;

# your sql ...

# 结束事务(提交事务)
commit
# 或
# 结束事务(回滚事务)
rollback
例子
  • 创库代码 : 《MySQL 脚本 - jt_db》 -
-- A给B转账500元
use jt_db ; 
show tables ;  -- 看有没有 accbiao 表

java 数据库事务级别 数据库事务等级_java 数据库事务级别

java 数据库事务级别 数据库事务等级_sql_02

-- 开始事务
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 ; 
-- 事务里查看 是成功的!。但是再开个窗口数据没变

java 数据库事务级别 数据库事务等级_java_03

另外开一个mysql

mysql -uroot -proot --default-character-set=gbk use jt_db ; select * from acc ;

java 数据库事务级别 数据库事务等级_java 数据库事务级别_04

java 数据库事务级别 数据库事务等级_数据库_05

提交 commit

回到一开始的窗口

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

java 数据库事务级别 数据库事务等级_java_06

再到另外一个窗口

select * from acc ;

java 数据库事务级别 数据库事务等级_bc_07

java 数据库事务级别 数据库事务等级_数据库_08

事务回滚 rollback

java 数据库事务级别 数据库事务等级_java 数据库事务级别_09

事务中断 quit

java 数据库事务级别 数据库事务等级_java_10

java 数据库事务级别 数据库事务等级_java 数据库事务级别_11

java 数据库事务级别 数据库事务等级_java 数据库事务级别_12

数据没变

并发问题

讨论并发读问题,是为了铺垫“事务隔离级别”

从问题的严重到轻微,有下面几种并发读问题:

  • 【严重】 脏读(dirty read): 读到未提交的“脏”的数据
  • 【可接受】 不可重复读(unrepeatable read): 一个事务,两次读同一行数据,有不同结果
  • 【可接受】 幻读(phantom read): 对同一张表的两次查询不一致,因为另一事务插入了一条记录(是针对插入或删除操作);⚠️针对数据插入(INSERT)操作来说的

💡 提示

  • 脏读之所以不可接受,因为脏读可能读到错误的数据
  • 幻读、不可重复读之所以可接收,因为读到的数据是正确的,只是数据可能旧了(不可重复读)、数据可能漏了(幻读)。读到的数据时效性有待提高而已。

事务隔离级别

# 查看事务隔离级别
select @@tx_isolation;

事务的隔离性是原则,但是为了性能,我们可以牺牲原则! (⚠️这也导致不同程度的并发读问题)

💡提示:在开发中,一般情况下不需要修改事务隔离级别

事务的隔离级别从低到高分四个等级

1、READ UNCOMMITTED(读未提交数据)【无锁,未提交】

性能最好,但安全级别最低,可能出现任何事务并发问题: 脏读(dirty read)、不可以重复读(unrepeatable read)、幻读(phantom read)

set tx_isolation='read-uncommitted';

2、READ COMMITTED(读已提交数据)(Oracle默认)【无锁,提交】

性能其次,避免了脏读,可能出现: 不可重复读,幻读

set tx_isolation='read-committed';

3、REPEATABLE READ(可重复读)(MySQL默认)【锁行】

性能较差,防止脏读和不可重复读,不能处理幻读问题;

set tx_isolation='repeatable-read';

4、SERIALIZABLE(串行化)【锁表】

性能最差,不会出现任何并发问题,因为它是对同一数据的访问是串行的,不存在并发访问的情况;

set tx_isolation='serialiable';

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(不使用事务)

其他

脏读(dirty read)演示

准备

use jt_db ; 
update acc set money=1000 ; 
select * from acc ;

java 数据库事务级别 数据库事务等级_sql_13

未降低隔离级别

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

java 数据库事务级别 数据库事务等级_数据库_14

设置最低隔离级别

--窗口1
rollback ; 
set tx_isolation='read-uncommitted';


--窗口2 
set tx_isolation='read-uncommitted';

java 数据库事务级别 数据库事务等级_java 数据库事务级别_15

执行之前同样操作

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

结果一样了!!

java 数据库事务级别 数据库事务等级_bc_16


窗口1rollback

--窗口1 rollback
rollback ; 
select * from acc ; 

--窗口2 
select * from acc ;

java 数据库事务级别 数据库事务等级_数据库_17

不可重复读演示

-- 在窗口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的记录(感觉像是出现了幻觉)。这种情况称之为"幻读"


以上就是在事务并发时常见的三种并发读问题,那么如何防止这些问题的产生?
可以通过设置事务隔离级别进行预防。

案例: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);
		}
	}
}
# 事务执行成功

java 数据库事务级别 数据库事务等级_bc_18


java 数据库事务级别 数据库事务等级_数据库_19

# 事务执行失败

放开一行注释

java 数据库事务级别 数据库事务等级_sql_20

java 数据库事务级别 数据库事务等级_bc_21

java 数据库事务级别 数据库事务等级_bc_22