10.事务

本章目标

  • 为什么要添加事务
  • 事务操作
  • JDBC事务管理
  • 事务特性(扩展,自行了解)
  • MYSQL架构(扩展,自行了解)
  • 锁(扩展,自行了解)

本章内容

一、为什么要添加事务

  • Dao每个方法针对是一次数据库操作

    public void update(){
     
     }
    
  • service层每一个方法针对是一个业务功能

    public  void transAcccount(){
          update();-
          update();+
     }
    

3、银行转账的例子

为了演示方便,我们先来看一个转账的功能,首先先建立两张表,账户表,交易记录表

  • 数据库脚本

    #账户表
     create table account(
     id int primary key auto_increment,
     account_no varchar(50),
     account_name varchar(50),
     balance DECIMAL(10,2)
     );
     
     #测试数据
     insert into account(account_no,account_name,balance)VALUES(‘6212264100011335371’,‘张三’,1000);
     insert into account(account_no,account_name,balance)VALUES(‘6212264100011335372’,‘李四’,1000);
     COMMIT;
    
  • 编写实体对象

    public class Account {
         private Integer id;
         private String accountNo;
         private String accountName;
         private BigDecimal balance;
     }
    
  • 编写数据交互层代码

    public interface AccountDao{
         //通过账号查找账户对象
         Account findAccoutByAccountNo(String accountNo);
         //修改账户余额
         void updateBalanceByAccountNo(Account account);
     }
    
  • SQL

    – 更新账户信息
     UPDATE account SET balance=? WHERE account_no=?
     – 查询账户信息
     SELECT * FROM account WHERE account_no=?
    
  • 业务层接口

     public interface AccountService {
         void transferAccounts(String fromAccount, String toAccount,BigDecimal money);
     }
    
  • 业务层代码

    public class AccountServiceImpl implements AccountService {
       
         @Override
         public void transferAccounts(String fromAccount, String toAccount,  BigDecimal money) {
                //———————-修改转账人信息—————————
                 Account accountFrom = accountDao.queryByNo(fromAccount);//查询A的账户
                 accountFrom.setBalance(accountFrom.getBalance().subtract(money));//减掉转账金额
                 accountDao.updateAccount(accountFrom,conn);//更新账户
                 //———————-修改收款人信息————————
                 Account accountTo = accountDao.queryByNo(toAccount);
                 accountTo.setBalance(accountTo.getBalance().add(money));
                 accountDao.updateAccount(accountTo,conn);
        }
     }
    
  • 测试代码

    AccountService accountService=new AccountServiceImpl();
     String fromAccount=“6212264100011335371”;
     String toAccount=“6212264100011335372”;
     accountService.transferAccounts(fromAccount,toAccount,new BigDecimal(100));
    
  • 执行后,正常情况下,已经完成了张三向李四的转账功能

  • 将toAccount改为一个不存在的账户,并测试,

查看控制台,已经报了异常错误信息

发现账户减了,但由于另一个账户不存在,程序出错了

这是因为程序在执行转账功能的时候,由于收款者的账户没有,所以出现NullPointerException,在空指向异常之前的代码成功执行,但是在空指向异常之后的代码不再执行,导致转账出现了问题,显然这样肯定不可以。

那么我们应该怎么办?实际上解决这个问题需要配置事务可以解决。

二、 事务操作

官网https://dev.mysql.com/doc/refman/8.0/en/commit.html

1、事务简介

事务(Transaction)是用户定义的一个操作序列,这些操作要么全部成功要么全部失败,是一个不可分割的工作单位(构成单一逻辑工作单元的操作集合)。 如果某一事务成功,则在该事务中进行的所有数据更改均会提交,成为数据库中的永久组成部分。 如果事务遇到错误且必须取消或回滚,则所有更改均被清除

对于银行系统而言,转账业务是银行最基本的、且最常用的业务,要保证转账业务的正常进行就必须要用到事务处理。

2、基本命令

 START TRANSACTION
     [transaction_characteristic [, transaction_characteristic] ...]
 
 transaction_characteristic: {
     WITH CONSISTENT SNAPSHOT
   | READ WRITE
   | READ ONLY
 }
 
 BEGIN [WORK]
 COMMIT [WORK] [AND [NO] CHAIN] [[NO] RELEASE]
 ROLLBACK [WORK] [AND [NO] CHAIN] [[NO] RELEASE]
 SET autocommit = {0 | 1}

命令使用说明:

  • START TRANSACTIONBEGIN开始新的事务。
  • COMMIT 提交当前事务,使其更改永久化。
  • ROLLBACK 回滚当前事务,取消其更改。
  • SET autocommit 禁用或启用当前会话的默认自动提交模式。

3、 执行流程

4、 关闭自动提交

MySQL中默认采用的是自动提交(autocommit)模式,在自动提交模式下,每个sql语句都会被当做一个事务执行提交操作。

查看状态:

 SHOW VARIABLES LIKE 'autocommit';

关闭自动提交的方法有两种:

  1. 显示地关闭自动提交:

     SET autocommit=0;
    

    通过将autocommit变量设置为零禁用自动提交模式后 ,对事务安全表(例如InnoDB或 的 那些NDB)的更改 不会立即永久生效。您必须使用COMMIT将更改存储到磁盘或ROLLBACK忽略更改。

    该设置针对连接的,在一个连接中修改了参数,不会对其他连接产生影响

  2. 隐式地关闭自动提交:

     START TRANSACTION;
     ......
     COMMIT;
    

    使用START TRANSACTION,自动提交将保持禁用状态,直到您使用COMMIT或结束事务 ROLLBACK。自动提交模式然后恢复到其先前的状态。

    start transaction;只针对当前操作,不会修改系统会话变量@@autocommit的值。

4、 开启事务

START TRANSACTION 启动事务后把所有后继的SQL语句看作事务的组成部分,直到提交或回滚事务为止。 该语句标记一个显式事务的开始点,即事务开始

语法

 START { TRAN | TRANSACTION }
 [ { transaction_name | @tran_name_variable } [ WITH MARK [ 'description' ] ]    ]

示例:

 START TRANSACTION;
 SELECT @A:=SUM(salary) FROM table1 WHERE type=1;
 UPDATE table2 SET summary=@A WHERE type=1;
 ......

5、 提交事务

COMMIT TRANSACTION: 提交事务后,事务结束,事务期间对数据库所做的任何修改将应用到数据库,事务结束。 语法:

 COMMIT { TRAN | TRANSACTION }
 [ transaction_name | @tran_name_variable ] ]

示例:

 START TRANSACTION;
 SELECT @A:=SUM(salary) FROM table1 WHERE type=1;
 UPDATE table2 SET summary=@A WHERE type=1;
 COMMIT;

在MySQL中,存在一些特殊的命令,如果在事务中执行了这些命令,会马上强制执行commit提交事务; DDL语句(create table/drop table/alter/table) lock tables语句等等。

6、 回滚事务

ROLLBACK TRANSACTION : 回滚事务后,事务结束,放弃事务期间所做的任何修改,事务结束。 该语句将显式事务或隐性事务回滚到事务的起点或事务内的某个保存点。

语法:

 ROLLBACK { TRAN | TRANSACTION }
      [ transaction_name | @tran_name_variable
      | savepoint_name | @savepoint_variable ]

7、 保存点(了解)

savepoint:保存点是事务中的一点。用于取消部分事务,当结束事务时,会自动的删除该事务所定义的所有保存点。当执行rollback 时,通过指定保存点可以回退到指定的点

语法:

 SAVEPOINT identifier
 ROLLBACK [WORK] TO [SAVEPOINT] identifier
 RELEASE SAVEPOINT identifier

保存点操作:

  1. 设置保存点 savepoint a
  2. 取消部分事务 rollback to SAVEPOINT a
  3. 取消全部事务 rollback

示例:

 START TRANSACTION;
 SAVEPOINT a1;
 UPDATE dept SET dept_name='行政部6' WHERE dept_id=60;
 SAVEPOINT a2;
 UPDATE dept SET dept_name='行政部7' WHERE dept_id=50;
 
 ROLLBACK TO a1;

如果执行 aCOMMIT或未ROLLBACK命名保存点的a , 将删除当前事务的所有保存点。

三、 JDBC的事务管理

1、JDBC事务管理

在JDBC的数据库操作中,一项事务是由一条或多条表达式所组成的一个不可分割的工作单元。我们通过提交commit()或是回退rollback()来结束事务的操作。关于事务操作的方法都位于接口java.sql.Connection中。 在JDBC中,事务操作默认是自动提交。也就是说,一条对数据库的更新表达式代表一项事务操作。操作成功后,系统将自动调用commit()来提交,否则将调用rollback()来回退。

3、手动提交事务

在JDBC中,可以通过调用setAutoCommit(false)来禁止自动提交。之后就可以把多个数据库操作的表达式作为一个事务,在操作完成后调用commit()来进行整体提交。倘若其中一个表达式操作失败,都不会执行到commit(),并且将产生响应的异常。此时就可以在异常捕获时调用rollback()进行回退。这样做可以保持多次更新操作后,相关数据的一致性。

在同一个dao层操作:

     try
     {
         //获取Connection对象的当前自动提交模式
         boolean   autoCommit=connect.getAutoCommit();
         //禁用自动提交模式
         connect.setAutoCommit(false);
         connect.commit();//将Connection对象的提交模式置回原来的模式
         connect.setAutoCommit(autoCommit);
     }catch(){
         connect.rollback();
     }
 

服务层操作:

 public boolean transferAccounts(String fromAccount, String toAccount, BigDecimal money) throws SQLException {
         Connection conn = DBUtil.getConn();//得到连接对象,两个修改操作要放在一个连接对象才能控制在一个事务中
         System.out.println(conn.getAutoCommit());
         conn.setAutoCommit(false);//关闭自动提交事务
         int i = 0;
         try {
             //----------------------修改转账人信息---------------------------
             Account accountFrom = accountDao.queryByNo(fromAccount);//查询A的账户
             accountFrom.setBalance(accountFrom.getBalance().subtract(money));//减掉转账金额
             accountDao.updateAccount(accountFrom,conn);//更新账户
             //----------------------修改收款人信息------------------------
             Account accountTo = accountDao.queryByNo(toAccount);
             accountTo.setBalance(accountTo.getBalance().add(money));
             i = accountDao.updateAccount(accountTo,conn);
             conn.commit();//提交事务
         } catch (SQLException e) {
             conn.rollback();//如果运行过程中有异常,则回滚
             throw new RuntimeException(e);
         } finally {
             DBUtil.release(conn,null,null);//关闭连接
         }
         return i>0?true:false;
     }
 

注意:在同一个事务内,需要使用同一个连接,所以需要把 conn 传递进来。


以下内容是面试时会涉及到

四、 事务特性(扩展-四阶段面试用)

1、 MySQL日志

在说明原子性原理之前,首先介绍一下MySQL的事务日志。MySQL的日志有很多种,日志主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。作为开发,我们重点需要关注的是二进制日志(bin log)和事务日志(包括redo log和undo log)。

InnoDB存储引擎提供了两种事务日志:

  • redo logg(重做日志):可以理解是当服务宿机时,重启后强制保持一致
  • undo log(回滚日志):可以理解是如果回滚时回滚到之前某一个状态

其中redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础

2、 原子性(Atomic)

原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。

实现原理:undo log(回滚日志)

在Mysql里数据每次修改前,都首先会把修改之前的数据作为历史保存一份到undo log里面的,数据里面会记录操作该数据的事务ID。当事务执行失败或调用rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的状态。

  1. insert undo log 代表事务在 insert 新记录时产生的 undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
  2. update undo log 事务在进行 update 或 delete 时产生的 undo log ; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除

3、 持久性 (Durability)

持久性也称永久性(Permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其执行结果有任何影响。

刷脏

InnoDB提供了缓存(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)

刷脏引发的问题

Buffer Pool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。解决这个问题通过redo log来解决。

实现原理:redo log

重做日志是一种基于磁盘的数据结构,用于在崩溃恢复期间纠正不完整事务写入的数据 redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志。

4、 一致性(Consistency)

事务操作成功后,数据库所处的状态和它的业务规则是一致的,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。 数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。

一致性是事务追求的最终目标:原子性、持久性和隔离性,都是为了保证数据库状态的一致性。

5、 隔离性(Isolation)

隔离性是指事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。

隔离性主要考虑最简单的读操作写操作

5.1、 事务的并发

取钱场景:老公去在 ATM 上取钱,老婆在柜台存钱,假设这个账户中有 1000 元:

  1. 老公首先执行查询操作,查询到账户余额为 1000 此时程序 将 1000 拿到内存中,老公取了 200 元,程序就执行了更新操作将账户余额改为 800
  2. 但是当老公的程序没有 commit 的时候,老婆查询账户,此时账户余额还是 1000 元,老婆存入 200 元,程序执行了更新操作将账户余额改为 1200
  3. 然后老公将更新语句提交,接着老婆也将更新语句提交。
  4. 最后导致的结果就是该账户的余额为 1200,这就是更新丢失的问题。
  5. 引发更新丢失的根源就是查询上,因为双方都是根据从数据库查询到的数据再 对数据库中的数据进行更新的。

并发情况下,读操作可能存在的三类问题:

  • 脏读
  • 不可重复读
  • 幻读

5.2、 脏读:

当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据),这种现象是脏读。

5.3、 不可重复读:

在事务A中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读

5.4、幻读:

在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。

6、 事务隔离级别

SQL标准中定义了四种隔离级别,并规定了每种隔离级别下上述几个问题是否存在。一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差

读未提交在并发时会导致很多问题,而性能相对于其他隔离级别提高却很有限,因此使用较少。

可串行化强制事务串行,并发效率很低,只有当对数据一致性要求极高且可以接受没有并发时使用,因此使用也较少。

在大多数数据库系统中,默认的隔离级别是读已提交(如Oracle)可重复读(后文简称RR)。

InnoDB默认的隔离级别是RR

查看当前事物级别:

 SELECT @@tx_isolation;# 8之前
 select @@transaction_isolation; # 8中

设置事务的隔离级别

MySQL支持4种事务隔离级别,在InnoDB存储引擎中,可以使用以下命令设置事务的隔离级别。

 #设置read uncommitted级别:
 set session transaction isolation level read uncommitted;
 
 #设置read committed级别:
 set session transaction isolation level read committed;
 
 #设置repeatable read级别:
 set session transaction isolation level repeatable read;
 
 #设置serializable级别:
 set session transaction isolation level serializable;

在SQL标准中,可重复读是无法避免幻读问题的 InnoDB实现可重复读取(RR)解决了脏读、不可重复读、幻读等问题。

五、MYSQL架构(扩展-四阶段面试用)

1、 逻辑架构

MySQL服务器逻辑架构从上往下可以分为三层:

  1. 第一层:处理客户端连接、授权认证等。
  2. 第二层:服务器层,负责查询语句的解析、优化、缓存以及内置函数的实现、存储过程等。
  3. 第三层:存储引擎,负责MySQL中数据的存储和提取。

2、MySQL引擎

数据库存储引擎是数据库底层软件组织,数据库管理系统(DBMS)使用数据引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以获得特定的功能。现在许多不同的数据库管理系统都支持多种不同的数据引擎。

2.1、 查看引擎:

 SHOW ENGINES

202308131626434.png

2.2、InnoDB存储引擎的特点

  • 支持行锁和外键约束,因此可以支持写并发
  • 对于AUTO_INCREMENT类型的字段,必须包含只有该字段的索引
  • DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的删除
  • 主键索引采用聚集索引
  • 支持事务(Transaction):
  • 需要执行大量的增、删、改操作(insert、delete、update语句),出于事务安全方面的考虑,InnoDB是更好的选择

2.3、MyISAM存储引擎的特点

参考

  • 不支持事务,但是整个操作是原子性的(事务具备四种特性:原子性、一致性、隔离性、持久性)
  • 不支持外键,支持表锁,每次锁住的是整张表,MyISAM的表锁有读锁和写锁(两个锁都是表级别)
  • 采用非聚集索引,索引文件的数据域存储指向数据文件的指针。
  • 支持全文索引和空间索引
  • 对于AUTO_INCREMENT类型的字段,可以和其他字段一起建立联合索引
  • 如果需要执行大量的select语句,出于性能方面的考虑,MyISAM存储引擎是更好的选择。

MySQL中服务器层不管理事务,事务是由存储引擎实现的。MySQL支持事务的存储引擎有InnoDB、NDB Cluster等,其中InnoDB的使用最为广泛;其他存储引擎不支持事务,如MyIsam、Memory等。

如无特殊说明,后文中描述的内容都是基于InnoDB。

六. 锁(Lock)(扩展,有兴趣同学了解)

锁是计算机在执行多线程或线程时用于并发访问同一共享资源时的同步机制,MySQL中的锁是在服务器层或者存储引擎层实现的,保证了数据访问的一致性与有效性。

锁机制的基本原理可以概括为:

  • 事务在修改数据之前,需要先获得相应的锁;
  • 获得锁之后,事务便可以修改数据;
  • 该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁

1、 查看锁

 select * from performance_schema.data_locks; # 8中
 select * from information_schema.innodb_locks; #8之前版本锁的概况
 show engine innodb status; #InnoDB整体状态,其中包括锁的情况
 #在事务A中执行:
 start transaction;
 update account SET balance = 1000 where id = 1;
 #在事务B中执行:
 start transaction;
 update account SET balance = 2000 where id = 1;

2、 锁机制的必要性

并发用户访问同一数据,锁机制可以避免数据不一致问题的发生。

202211101659400.png

3、 MySQL锁分类

4、 共享锁

又称之为读锁,简称S锁,当事务A对数据加上读锁后,其他事务只能对该数据加读锁,不能做任何修改操作,也就是不能添加写锁。只有当事务A上的读锁被释放后,其他事务才能对其添加写锁。

应用场景

共享锁主要是为了支持并发的读取数据而出现的,读取数据时,不允许其他事务对当前数据进行修改操作,从而避免”不可重读”的问题的出现。

实现方式

select * from dept WHERE dept_id=50 LOCK IN SHARE MODE;

必须关闭MySQL的自动提交,set autocommit=0

测试时可以在客户端开户两个查询端口,第一个端口执行上面命令,第二个端口此时可以查询,但要执行修改操作会被阻塞,除非第一个端口执行相关的事务操作,如果rollback,commit等相关操作

5、 排它锁

又称之为写锁、独占锁,排它锁,简称X锁,当事务对数据加上写锁后,其他事务既不能对该数据添加读锁,也不能对该数据添加写锁,写锁与其他锁都是互斥的。只有当前数据写锁被释放后,其他事务才能对其添加写锁或者是读锁。

MySQL InnoDB引擎默认update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型。

应用场景

写锁主要是为了解决在修改数据时,不允许其他事务对当前数据进行修改操作,从而可以有效避免”脏读”问题的产生。

实现方式

select * from dept WHERE dept_id=50 FOR UPDATE;

必须关闭MySQL的自动提交,set autocommit=0

6、 锁的粒度分类

  • 表级锁:开销小,加锁快,不会出现死锁,锁定力度大,发生冲突所的概率高,并发度低。
  • 行级锁:开销大,加锁慢,会出现死锁,锁定力度最小,发生锁冲突的概率最低,并发度高。
  • 页面锁:开销和加锁时间介于表锁和行锁之间,会出现死锁,锁定力度介于表和行行级锁之间,并发度一般。

MyISAM和MEMORY存储引擎采用表级锁 InnoDB支持行级锁、表级锁,默认情况采用行级锁

7、 乐观锁

乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。

应用场景

适用于读多写少,因为如果出现大量的写操作,写冲突的可能性就会增大,业务层需要不断重试,会大大降低系统性能。 实现方式:一般使用数据版本(Version)记录机制实现,在数据库表中增加一个数字类型的 “version” 字段来实现

8、 悲观锁

悲观锁,正如其名,具有强烈的独占和排他特性,每次去拿数据的时候都认为别人会修改,对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。

应用场景:适用于并发量不大、写入操作比较频繁、数据一致性比较高的场景。

实现方式:select…for update是MySQL提供的实现悲观锁的方式,属于排它锁。

在MySQL中使用悲观锁,必须关闭MySQL的自动提交,set autocommit=0。共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴。

9、 死锁

当某组资源的两个或多个线程之间有循环相关性时,将发生死锁。 死锁是一种可能发生在任何多线程系统中的状态,而不仅仅发生在关系数据库管理系统中。

给MyISAM表施加表级锁不会导致死锁问题的发生,这是由于MyISAM总是一次性地获得SQL语句的全部锁。给InnoDB表施加行级锁可能导致死锁问题的发生,这是由于执行SQL语句期间,可以继续施加行级锁。

默认情况下:InnoDB存储引擎一旦出现锁等待超时异常,InnoDB存储引擎即不会提交事务,也不会回滚事务,而这是十分危险的。一旦发生锁等待超时异常,应用程序应该自定义错误处理程序,由程序开发人员选择进一步提交事务,还是回滚事务。

为尽可能避免死锁的发生,用户应该遵循以下原则:

  • 在所有的事务中都按同一顺序来访问各个表。尽可能利用存储过程来完成一个事务,以便能保证对各表的访问次序都是一致的。
  • 事务应该尽量小且应尽快提交。
  • 尽量避免人工输入操作出现在事务中。
  • 尽量避免同时执行诸如【INSERT】、【UPDATE】和【DELETE】等数据修改语句。
posted @ 2025-04-10 12:00  icui4cu  阅读(27)  评论(0)    收藏  举报