文章目录
- 数据库事务
- 事务的四大特性
- 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)
如此划分后,我们可以做更高级的安排:
- “查账” 没问题了,就可以“入账”,入账后再次“查账”,校对没问题了最后“生成报告”
- 💡 如果中间校对环节出了问题,全部“入账”数据回滚,重新任务
能体会到事务的作用了吗?
事务的四大特性
事务的四大特性(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 表
-- 开始事务
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
数据没变
并发问题
讨论并发读问题,是为了铺垫“事务隔离级别”
从问题的严重到轻微,有下面几种并发读问题:
- 【严重】 脏读(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 ;
未降低隔离级别
--窗口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 ;
设置最低隔离级别
--窗口1
rollback ;
set tx_isolation='read-uncommitted';
--窗口2
set tx_isolation='read-uncommitted';
执行之前同样操作
--窗口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 ;
结果一样了!!
窗口1rollback
--窗口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的记录(感觉像是出现了幻觉)。这种情况称之为"幻读"
以上就是在事务并发时常见的三种并发读问题,那么如何防止这些问题的产生?
可以通过设置事务隔离级别进行预防。
案例: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);
}
}
}
# 事务执行成功
# 事务执行失败
放开一行注释