参考自:
https://baijiahao.baidu.com/s?id=1611918898724887602&wfr=spider&for=pc
https://blog.csdn.net/weixin_28760063/article/details/81369266
https://www.jb51.net/article/88229.htm
https://www.jb51.net/article/134469.htm
https://www.cnblogs.com/ysocean/p/7617620.html#_label5
https://blog.csdn.net/lee_sire/article/details/72904822
目录
TransactionDefinition 基本事务属性的定义
编程式事务处理实现转账(TransactionTemplate )
事务的基本概念
什么是事务
事务是用户定义的一个数据库操作序列,这些操作要么全做,要么全不做,是一个不可分割的工作单位。例如,在关系数据库中,一个事务可以是一个sql语句,一组sql语句或整个程序。
事务的开始和结束可以由用户显式控制,如果用户没有显式的定义事务,则由数据库管理系统按照默认规定自动划分事务。例如:mysql数据库默认一条sql语句一个事务,默认会开启并提交事务.
在SQL中,定义事务的语句一般有三条:
BEGIN TRANSACTION;
COMMIT;
ROLLBACK;
事务通常是以 BEGIN TRANSACTION开始,以COMMIT或ROLLBACK结束;COMMIT表示提交,即提交事务的所有操作,具体地说,就是将事务中所有对数据库的更新写回到磁盘上的物理数据库中,事务正常结束;ROLLBACK表示回滚,即在事务运行过程中发生了某种故障,事务不能继续执行,系统将事务中对数据库的所有已完成的更新操作全部撤销,回到事务开始时的状态。
事务的ACID特性
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败。比如在同一个事务中的SQL语句,要么全部执行成功,要么全部执行失败。
事务必须使数据库从一个一致性状态变换到另外一个一致性状态。以转账为例子,A向B转账,假设转账之前这两个用户的钱加起来总共是2000,那么A向B转账之后,不管这两个账户怎么转,A用户的钱和B用户的钱加起来的总额还是2000,这个就是事务的一致性。
一个事务的执行不能被其他事务干扰。事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
事务的四大特性中最麻烦的是隔离性,下面重点介绍一下事务的隔离级别
事务的隔离级别
多个线程开启各自的事务来操作数据库中数据时,数据库系统要负责隔离操作,以保证各个线程在获取数据时的准确性。
如果事务不考虑隔离性,可能会引发如下问题:
1、脏读
脏读指一个事务读取了另外一个事务未提交的数据。
事务A访问了数据库,它干了一件事情,往数据库里加上了新来的牛人的名字,但是没有提交事务。
insert into T values (4, '牛D');
这时,来了另一个事务B,他要查询所有牛人的名字。
select Name from T;
这时,如果没有事务之间没有有效隔离,那么事务B返回的结果中就会出现“牛D”的名字。这就是“脏读(dirty read)”。
2、不可重复读
不可重复读指在一个事务内读取表中的某一行数据,多次读取结果不同。
事务A访问了数据库,他要查看ID是1的牛人的名字,于是执行了
select Name from T where ID = 1;
这时,事务B来了,因为ID是1的牛人改名字了,所以要更新一下,然后提交了事务。
update T set Name = '不牛' where ID = 1;
接着,事务A还想再看看ID是1的牛人的名字,于是又执行了
select Name from T where ID = 1;
结果,两次读出来的ID是1的牛人名字竟然不相同,这就是不可重复读(unrepeatable read)。
3、虚读(幻读)
虚读(幻读)是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。
事务A访问了数据库,他想要看看数据库的牛人都有哪些,于是执行了
select * from T;
这时候,事务B来了,往数据库加入了一个新的牛人。
insert into T values(4, '牛D');
这时候,事务A忘了刚才的牛人都有哪些了,于是又执行了。
select * from T;
结果,第一次有三个牛人,第二次有四个牛人。
相信这个时候事务A就蒙了,刚才发生了什么?这种情况就叫“幻读(phantom problem)”。
为了防止出现脏读、不可重复读、幻读等情况,我们就需要根据我们的实际需求来设置数据库的隔离级别。
事务隔离级别
- Serializable(串行化):可避免脏读、不可重复读、虚读情况的发生。
- Repeatable read(可重复读):可避免脏读、不可重复读情况的发生。
- Read committed(读已提交):可避免脏读情况发生。
- Read uncommitted(读未提交):最低级别,以上情况均无法保证。
mysql数据库默认的事务隔离级别是:Repeatable read(可重复读)
oracle数据库默认的事务隔离级别是:read commited(读已提交)
查看数据库默认的隔离级别:select @@tx_isolation;
设置隔离级别:set session transaction isolation level
JDBC中使用事务
事务控制语句
当Jdbc程序向数据库获得一个Connection对象时,默认情况下这个Connection对象会自动向数据库提交在它上面发送的SQL语句。若想关闭这种默认提交方式,让多条SQL在一个事务中执行,可使用下列的JDBC控制事务语句.
- Connection.setAutoCommit(false); //开启事务(start transaction)
- Connection.rollback(); //回滚事务(rollback)
- Connection.commit(); //提交事务(commit)
设置隔离级别
Connection.setTransactionIsolation(int level) :参数可选值如下:
- Connection.TRANSACTION_READ_UNCOMMITTED; //读未提交
- Connection.TRANSACTION_READ_COMMITTED; //读已提交
- Connection.TRANSACTION_REPEATABLE_READ; //可重复读
- Connection.TRANSACTION_READ_SERIALIZABLE。 //序列化
设置事务回滚点
在开发中,有时候可能需要手动设置事务的回滚点,在JDBC中使用如下的语句设置事务回滚点:
Savepoint sp = connection.setSavepoint();
connection.rollback(sp);
connection.commit(); //回滚后必须通知数据库提交事务
JDBC中使用事务例子
这是单元测试类,所以有@Before @Test
package com.test.transaction.tset;
import org.junit.Before;
import org.junit.Test;
import java.sql.*;
public class TransactionTest {
private Connection con = null;
private Statement stmt = null;
private ResultSet rs = null;
private String url = "jdbc:mysql://localhost:3306/transaction?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC";
private String username = "root" ;
private String password = "123456789" ;
private String sql1 = "update account set money = money+100 where name='B'";
private String sql2 = "update account set money = money-100 where name='A'";
/**
* @Description:连接数据库
*/
@Before
public void connect(){
try{
//加载MySql的驱动类
Class.forName("com.mysql.cj.jdbc.Driver") ;
}catch(ClassNotFoundException e){
System.out.println("找不到驱动程序类 ,加载驱动失败!");
e.printStackTrace() ;
}
try{
con = DriverManager.getConnection(url, username, password ) ;
stmt = con.createStatement() ;
}catch(SQLException se){
System.out.println("数据库连接失败!");
se.printStackTrace() ;
}
}
/**
* @Description:模拟转账成功时的业务场景
*/
@Test
public void testTransaction1(){
try {
//通知数据库开启事务(start transaction)
con.setAutoCommit(false);
stmt.execute(sql1);
stmt.execute(sql2);
//提交事务
con.commit();
System.out.println("success");
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
con.close();
} catch (SQLException e) {
System.out.println("数据库连接关闭失败!");
e.printStackTrace();
}
}
}
/**
* @Description:模拟转账过程中出现异常导致有一部分SQL执行失败后让数据库自动回滚事务
*/
@Test
public void testTransaction2(){
try {
//通知数据库开启事务(start transaction)
con.setAutoCommit(false);
stmt.execute(sql1);
//用这句代码模拟执行完SQL1之后程序出现了异常而导致后面的SQL无法正常执行,事务也无法正常提交,此时数据库会自动执行回滚操作
int x = 1 / 0;
stmt.execute(sql2);
//提交事务
con.commit();
System.out.println("success");
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
con.close();
} catch (SQLException e) {
System.out.println("数据库连接关闭失败!");
e.printStackTrace();
}
}
}
/**
* @Description:模拟转账过程中出现异常导致有一部分SQL执行失败时手动通知数据库回滚事务
*/
@Test
public void testTransaction3(){
try {
//通知数据库开启事务(start transaction)
con.setAutoCommit(false);
stmt.execute(sql1);
//用这句代码模拟执行完SQL1之后程序出现了异常而导致后面的SQL无法正常执行,事务也无法正常提交,此时数据库会自动执行回滚操作
int x = 1 / 0;
stmt.execute(sql2);
//提交事务
con.commit();
System.out.println("success");
} catch (SQLException e) {
try {
//捕获到异常之后通知数据库回滚
con.rollback();
} catch (Exception ex) {
ex.printStackTrace();
}
e.printStackTrace();
}finally {
try {
con.close();
} catch (SQLException e) {
System.out.println("数据库连接关闭失败!");
e.printStackTrace();
}
}
}
/**
* @Description:设置事务回滚点
*/
@Test
public void testTransaction4(){
Savepoint sp = null;
String sql3 = "update account set money = money + 100 where name='C'";
try {
//通知数据库开启事务(start transaction)
con.setAutoCommit(false);
stmt.execute(sql1);
//stmt.execute(sql2);
sp = con.setSavepoint();
stmt.execute(sql2);
//用这句代码模拟执行完SQL1之后程序出现了异常而导致后面的SQL无法正常执行,事务也无法正常提交,此时数据库会自动执行回滚操作
int x = 1 / 0;
stmt.execute(sql3);
//提交事务
con.commit();
System.out.println("success");
} catch (Exception e) {
try {
//捕获到异常之后通知数据库回滚
con.rollback(sp);
con.commit();
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
}finally {
try {
con.close();
} catch (SQLException e) {
System.out.println("数据库连接关闭失败!");
e.printStackTrace();
}
}
}
}
spring中的事务管理
Spring事务原理
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:
1.获取连接 Connection con = DriverManager.getConnection()
2.开启事务con.setAutoCommit(false);
3.执行CRUD
4.提交事务/回滚事务 con.commit() / con.rollback();
5.关闭连接 con.close();
使用Spring的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由Spirng 自动完成。
那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。下面简单地介绍下,注解方式为例子
1.配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。
2.spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
3.真正的数据库层的事务提交和回滚是通过bin log或者redo log实现的。
TransactionDefinition 基本事务属性的定义
什么是事务属性呢?事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。事务属性包含了5个方面,如图所示:
传播行为
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播