事务:一个包含多个步骤的业务操作。如果这个包含多个步骤的业务操作被事务管理,则这多个步骤要么同时成功(commit),要么同时失败(rollback)。
什么是本地事务?
本地事务就是用关系数据库来控制事务,关系数据库通常都具有ACID特性,传统的单体应用通常会将数据全部存储在一个数据库中,会借助关系型数据库来完成事务控制。
PS:MySQL中,一条DML语句(增删改)会自动提交一次事务,不需要进行事务管理,而对于多步操作,要想实现同时成功或同时失败,则需要进行事务管理。
操作:
1. 开启事务:start transaction
2. 提交事务:commit
3. 回滚事务:rollback
银行转账业务存在问题的演示:
1、新建db1数据库,创建表格并插入数据
CREATE TABLE account ( id INT PRIMARY KEY AUTO_INCREMENT, NAME VARCHAR(10), balance DOUBLE ); -- 添加数据 INSERT INTO account (NAME, balance) VALUES ('Jack', 1000), ('Rose', 1000);
2、正常情况下,执行以下语句,发现转账正常
-- jack减500 UPDATE account SET balance = balance -500 WHERE NAME = 'jack'; -- rose加500 UPDATE account SET balance = balance +500 WHERE NAME = 'rose';
结果为:
3、如果在执行第一条SQL语句之后出现了异常,以下SQL模拟异常
-- jack减500 UPDATE account SET balance = balance -500 WHERE NAME = 'jack'; rose加500 UPDATE account SET balance = balance +500 WHERE NAME = 'rose';
即第一条SQL执行成功,第二条SQL由于异常没有执行
结果为:
使用事务管理来解决这个问题
转账之前开启事务,当使用了start transaction表示手动提交。(mysql中DML语句(增删改)不开启事务就会自动提交,开启了事务就不会自动提交)
-- 开启事务 START TRANSACTION; -- jack减500 UPDATE account SET balance = balance -500 WHERE NAME = 'jack'; rose加500 UPDATE account SET balance = balance +500 WHERE NAME = 'rose';
执行以上SQL,并没有提交,也没有回滚,现在这个事务还没有结束。此时在新的sqlyog窗口中查询,数据没有任何变化,如下图所示
但是在本sqlyog窗口中查询发现数据变化了,如下图所示,但是这些数据的变化是临时的变化,并不是持久的变化。当窗口关闭再打开,临时变化会被取消掉。
如果你开启了事务,没有提交事务(即使没有出错),则事务默认会自动回滚。
5、出了问题则回滚事务。这样数据库表中的数据没有发生变化,保证了账户的安全性
-- 开启事务 START TRANSACTION; -- jack减500 UPDATE account SET balance = balance -500 WHERE NAME = 'jack'; rose加500 UPDATE account SET balance = balance +500 WHERE NAME = 'rose'; -- 回滚事务 ROLLBACK;
6、发现执行没有问题,则提交事务
-- 开启事务 START TRANSACTION; -- jack减500 UPDATE account SET balance = balance -500 WHERE NAME = 'jack'; rose加500 UPDATE account SET balance = balance +500 WHERE NAME = 'rose'; -- 提交事务 COMMIT;
由于两个update语句之间出现了异常,事务最终没有提交,所以事务中所做的修改也不会保存到数据库中。
如果发现执行没有问题,commit之后,数据发生了持久性的变化,事务随着commit的执行结束了。
PS:对于多步操作,必须手动开启事务和手动提交事务,出了问题则回滚,没有问题则提交。
Mysql数据库中事务默认自动提交(Oracle数据库默认是手动提交的),一条DML语句(增删改)会自动提交一次事务,例如执行以下SQL
UPDATE account SET balance = 1000;
执行之后会默认提交一次事务,数据会被持久化更新,当执行了start transaction,就表示手动提交。
手动开启需要start transaction,手动提交使用commit。仅仅一条DML语句不开启事务就会自动提交,如果一条DML语句开启了事务就要手动提交。
事务提交的两种方式:
1、自动提交:mysql就是自动提交的
2、手动提交:需要先开启事务,再提交
修改事务的默认提交方式:
1、先查看事务默认提交方式:1代表自动提交,0代表手动提交
SELECT @@autocommit;
结果如下:
2、修改默认提交方式
SET @@autocommit = 0;
如果此时执行DML语句而没有commit,sq语句是不会生效的,即还没有持久化保存。只有执行commit命令才会持久化保存。
Oracle数据库默认是手动提交的,将来用Oracle数据库,执行了增删改操作之后必须commit才会生效,如果没有commit,关闭窗口就会还原到之前的状态。
注意:使用navicat时,Oracle数据库时自动提交的;使用PLSQL时,Oracle数据库是手动提交的。
事务四个特性 ACID:
事务的隔离级别:
引发的问题:
四种隔离级别:
SELECT @@tx_isolation;
mysql8查询事务的隔离级别:
select @@transaction_isolation;
结果:
数据库设置隔离级别:
SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串;
如:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
mysql8设置隔离级别
set global transaction isolation level read committed;
接下来演示脏读和不可重复读,通过设置不同的隔离级别来解决这些问题。
演示读未提交(没提交也能读到)
1、打开一个cmd窗口,输入msyql -uroot -p123456,use db3;,select * from account;结果如下:
2、 设置隔离级别为读未提交,这样才能演示脏读的问题。
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
PS:设置事务的隔离级别后,再次查询隔离级别会发现没有生效,此时关闭cmd窗口后再次进入发现生效了。
设置全局的事务隔离级别,该设置不会影响当前已经连接的会话,设置完毕后,新打开的会话,将使用新设置的事务隔离级别
3、开启事务
start transaction;
4、再打开一个窗口,登录mysql,进入db3数据库,查询所有记录,给新窗口开启事务,这样,两个窗口都开启了事务。
模拟并发情况下,多个事务操作同样的数据。
5、第一个窗口执行转账操作(未手动提交)
-- jack减500
UPDATE account SET balance = balance -500 WHERE NAME = 'jack';
-- rose加500
UPDATE account SET balance = balance +500 WHERE NAME = 'rose';
注意:并没有提交
6、新窗口查询所有记录
发现能够查询到旧窗口没有提交的数据,脏读的情况发生,隔离级别设置生效。
7、旧窗口执行rollback,回滚,数据会还原到转账之前的操作。此时新窗口再次查询所有记录,发现转账并没有成功,
此时也发生了不可重复读,在同一个事务中,两次读取的数据不一样。
脏读值一个事务还没有提交,另一个事务就能读取到没有提交的数据。
演示读已提交(没提交读不到,只有提交了才能读到)
设置隔离级别为读已提交,来解决脏读的问题
1、打开一个cmd窗口,输入msyql -uroot -p123456,use db3;,select * from account;结果如下:
1、将旧窗口事务的隔离级别设置为读已提交
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
PS:设置事务的隔离级别后,再次查询隔离级别会发现没有生效,此时关闭cmd窗口后再次进入发现生效了。
3、关闭窗口,重新打开,开启事务
start transaction;
4、再打开一个窗口,登录mysql,进入db3数据库,查询所有记录,给新窗口开启事务,这样,两个窗口都开启了事务。
模拟多个事务操作同样的数据;
5、在旧窗口中执行转账的操作,
-- jack减500 UPDATE account SET balance = balance -500 WHERE NAME = 'jack'; -- rose加500 UPDATE account SET balance = balance +500 WHERE NAME = 'rose';
由于现在的隔离级别是读已提交,且还没有提交
6、此时在新窗口中查询所有记录,发现钱还没有到,只有当旧窗口的事务提交之后,真正的数据发生修改
7、旧窗口提交
commit;
8、新窗口查询所有记录,发现转账成功
但是还有一个问题,就是在同一个事务中,两次的查询结果不一致,即不可重复读。
有些需求要求在同一个事务中(事务没结束之前),每次读取的数据一模一样。我们希望在同一个事务中,每次查询的数据都是一样的。只有当这个事务结束之后,才会看到其他事务对这个表数据的修改情况。要完成这个需求,我们需要将事务的隔离级别设置成repeatable read
不可重复读指一个事务在提交前和提交后,另一个事务在没有结束之前读取到的数据不一样。
演示可重复度(在事务结束之前读取到的数据一样)
设置隔离级别为可重复读,来解决不可重复读的问题
可重复读指事务结束之前可重复读。
1、打开一个cmd窗口,输入msyql -uroot -p123456,use db3;,select * from account;结果如下:
2、将旧窗口事务的隔离级别设置为读已提交
SET GLOBAL TRANSACTION ISOLATION LEVEL repeatable read;
PS:设置事务的隔离级别后,再次查询隔离级别会发现没有生效,此时关闭cmd窗口后再次进入发现生效了。
3、关闭窗口,重新打开,因为设置了隔离级别之后,重新打开才会生效。此时事务的隔离级别为repeatable read,开启事务
start transaction;
4、再打开一个窗口,登录mysql,进入db3数据库,查询所有记录,给新窗口开启事务,这样,两个窗口都开启了事务。
模拟多个事务操作同样的数据。
5、在旧窗口中执行转账的操作,
-- jack减500
UPDATE account SET balance = balance -500 WHERE NAME = 'jack';
-- rose加500
UPDATE account SET balance = balance +500 WHERE NAME = 'rose';
由于现在的隔离级别是可重复读,还没有提交
6、在新窗口中查询所有记录,发现数据没有产生任何变化。
此时,右边的事务还没有提交,
7、提交旧窗口的事务,此时新窗口还是没有提交事务,新窗口再次查询所有记录,发现数据没有发生变化。说明,可重复读就生效了,因为在事务结束之前读取到的数据一样。
8、当把新窗口中的事务提交或回滚了之后,再次查询,才可以看到数据表的变化,
可重复读是指一个事务提交前和提交后,另一个事务在没有结束之前可重复读,即另一个事务在没有结束之前读取的数据没有发生变化。
可重复读会存在幻读的问题,那如何解决幻读呢?
演示串行化(锁表)
serializable:串行化就是一个锁表的动作,一个事务在操作数据表,另一个事务是不可以操作数据表的,
1、打开一个cmd窗口,输入msyql -uroot -p123456,use db3;,select * from account;结果如下:
2、将旧窗口事务的隔离级别设置为读已提交
SET GLOBAL TRANSACTION ISOLATION LEVEL serializable;
3、关闭窗口,重新打开,因为设置了隔离级别之后,重新打开才会生效。此时事务的隔离级别为serializable,开启事务
start transaction;
4、再打开一个窗口,登录mysql,进入db3数据库,查询所有记录,给新窗口开启事务,这样,两个窗口都开启了事务。
5、在旧窗口中执行转账的操作,
-- jack减500 UPDATE account SET balance = balance -500 WHERE NAME = 'jack'; -- rose加500 UPDATE account SET balance = balance +500 WHERE NAME = 'rose';
由于现在的隔离级别是串行化,还没有提交
6、在新窗口中查询所有记录,发现光标一直在闪,查询的动作并没有执行,只有当旧窗口事务提交或回滚之后,新窗口才能完成查询的动作,相当于这张表被锁住了。
7、提交旧窗口的事务,新窗口立即回查询出来数据
串行化是将表锁住了,即一个事务在操作数据,其他事务无法操作相同数据。
串行化解决了幻读的问题。
使用Connection对象来管理事务
Connection数据库连接对象管理事务
* 开启事务:setAutoCommit(boolean autoCommit) :调用该方法设置参数为false,即开启事务
在执行sql之前开启事务
* 提交事务:commit()
当所有sql都执行完提交事务
* 回滚事务:rollback()
在catch中回滚事务
代码:
public class JDBCDemo10 { public static void main(String[] args) { Connection conn = null; PreparedStatement pstmt1 = null; PreparedStatement pstmt2 = null; try { //1.获取连接 conn = JDBCUtils.getConnection(); //开启事务 conn.setAutoCommit(false); //2.定义sql //2.1 张三 - 500 String sql1 = "update account set balance = balance - ? where id = ?"; //2.2 李四 + 500 String sql2 = "update account set balance = balance + ? where id = ?"; //3.获取执行sql对象 pstmt1 = conn.prepareStatement(sql1); pstmt2 = conn.prepareStatement(sql2); //4. 设置参数 pstmt1.setDouble(1,500); pstmt1.setInt(2,1); pstmt2.setDouble(1,500); pstmt2.setInt(2,2); //5.执行sql pstmt1.executeUpdate(); // 手动制造异常 int i = 3/0; pstmt2.executeUpdate(); //提交事务 conn.commit(); } catch (Exception e) { //事务回滚 try { if(conn != null) { conn.rollback(); } } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); }finally { JDBCUtils.close(pstmt1,conn); JDBCUtils.close(pstmt2,null); } } }
PlatformTransactionManager接口
我们自己写了一个事务管理器,spring提供了事务管理器,我们拿过来直接用就可以。
真正管理事务的对象
TransactionDefinition
事务的传播行为指什么情况下必须有事务(增删改必须有事务Required),什么情况下可有可没有(查询可有可没有事务supports)
只读:增删改read-only="false"
读写:只有查询方法才能用只读,read-only="true"
事务的隔离级别使用数据库默认的隔离级别,mysql数据库的默认隔离几级别为可重复读,Oracle数据库的默认隔离级别为读已提交。
事务的传播行为
超时时间
是否是只读事务
TransactionStatus