事务:一个包含多个步骤的业务操作。如果这个包含多个步骤的业务操作被事务管理,则这多个步骤要么同时成功(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:

1、原子性:原子是不可分割的最小单位,要么同时成功,要么同时失败。
2、一致性:事务操作前后,数据总量不变。
3、隔离性:多个事务之间相互独立,我们希望多个事务相互独立,不影响相互的操作。真实的情况下,多个事务之间会产生相互影响。要解决这些影响带来的问题,就要了解事务的隔离级别。
4、持久性:事务一旦提交或者回滚,将会持久的更新数据库表。

事务的隔离级别:

概念:多个事务之间是隔离的,相互独立,但是如果并发情况下,多个事务操作同一批数据,则会引发一些问题设置不同的隔离级别就可以解决这些问题

引发的问题:

1、脏读:一个事务读到另一个事务没有提交的数据。这个问题非常严重。
2、不可重复读:同一个事务中(另一个事务结束之前)两次读取到的数据不一样。
3、幻读:一个事务操作(DML)数据表中所有的记录,比如给所有的记录加100块钱,另一个事务添加了一条数据,则第一个事务查询不到自己的修改。好像产生了幻觉一样。
mysql数据库中看不到幻读的情况,脏读和不可重复读是可以演示出来的。  

四种隔离级别:

1、read uncommitted:读未提交,
  会引发的问题:脏读、不可重复读、幻读。
2、read committed:读已提交,即只有提交的数据才能读到。(Oracle默认)
  会引发的问题:不可重复读、幻读。
3、repeatable read:可重复读(Mysql默认
  会引发的问题:幻读。
4、serializable:串行化(锁表
  可以解决所有问题
 
一般情况下,不会去修改数据库默认的隔离级别,只有特殊的情况下才会修改隔离级别来解决不同的问题。
是不是将隔离级别设置成serializable就行了呢?
注意:隔离级别从read uncommitted到serializable,安全性越来越高,但是效率越来越低,所以要设置合适的隔离级别,既保证安全,又保证效率
数据库查询隔离级别:
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提供了事务管理器,我们拿过来直接用就可以。

此接口是 spring 的事务管理器,它里面提供了我们常用的操作事务的方法,如下图:
我们在开发中都是使用它的实现类,如下图:

真正管理事务的对象

org.springframework.jdbc.datasource.DataSourceTransactionManager:使用 SpringJDBC 或 iBatis 进行持久化数据时使用
org.springframework.orm.hibernate5.HibernateTransactionManager:使用Hibernate 版本进行持久化数据时使用

TransactionDefinition 

它是事务的定义信息对象,里面有如下方法:

事务的传播行为指什么情况下必须有事务(增删改必须有事务Required),什么情况下可有可没有(查询可有可没有事务supports)

只读:增删改read-only="false"

读写:只有查询方法才能用只读,read-only="true"

事务的隔离级别

使用数据库默认的隔离级别,mysql数据库的默认隔离几级别为可重复读,Oracle数据库的默认隔离级别为读已提交。

事务的传播行为

REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。一般的选择(默认值)
SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行(没有事务)
MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常
REQUERS_NEW:新建事务,如果当前在事务中,把当前事务挂起
NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
NEVER:以非事务方式运行,如果当前存在事务,抛出异常
NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行REQUIRED类似的操作。

超时时间

默认值是-1,没有超时限制。如果有,以秒为单位进行设置。

是否是只读事务

建议查询时设置为只读,增删改为读写型。

TransactionStatus

此接口提供的是事务具体的运行状态,方法介绍如下图:

 

 
posted on 2021-04-10 16:55  周文豪  阅读(381)  评论(0编辑  收藏  举报