文章目录
- 前言
- 一、事务概述
- 1、什么是事务
- 2、事务的四个处理过程
- 3、事务的四个特性
- 二、引入事务场景
- 1、引入依赖
- 2、数据库创建
- 3、建包
- 4、spring.xml配置文件
- 5、测试程序
- 6、运行结果(成功)
- 7、模拟异常
- 三、Spring对事务的支持
- 1、Spring实现事务的两种方式
- 2、Spring事务管理API
- 3、解决上述代码没有事务的问题
- 3.1配置spring.xml
- 3.2使用事务
- 四、事务之传播行为
- 五、事务之隔离级别
前言
1、什么是事务
2、事务的四个处理过程
3、事务的四个特性
4、事务使用场景
5、spring实现事务的两种方式
6、事务的传播行为
7、事务的隔离级别
一、事务概述
1、什么是事务
- 在一个业务流程当中,通常需要多条DML(insert delete update)语句共同联合才能完成,这多条DML语句必须同时成果,或者同时失败,这样才能保证数据的安全。
- 多条DML要么同时成功,要么同时失败,这叫做事务。
- 事务:Transaction
2、事务的四个处理过程
- 第一步:开启事务 start transaction
- 第二步:执行核心业务代码
- 第三步:提交事务(如果核心业务处理过程中没有出现异常)commit transaction
- 第四步:回滚事务(如果核心业务处理过程中出现异常)rollback transaction
3、事务的四个特性
- 原子性:事务是最小的工作单元,不可再分
- 一致性:事务要求要么同时成功要么同时失败。
- 隔离性:事务和事务之间因为有隔离性,才能保证互不干扰
- 持久性:持久性是事务结束的标志
二、引入事务场景
以银行账户转账为例学习事务。两个账户act-001和act-002,act-001账户向act-002账户转账10000,必须同时成功,或者同时失败。(一个减成功,一个加成功,这两个update语句必须同时成功或失败)
1、引入依赖
1、spring-context依赖
2、spring-jdbc
3、mysql驱动 mysql-connector-java
4、德鲁伊连接池 druid
5、@Resource注解 lombok
6、单元测试
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.20</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
</dependencies>
2、数据库创建
3、建包
com.powernode.bank.dao
AccountDao 专门负责账户信息的CRUD(增删改查)操作
DAO中只执行SQL语句,没有任何业务逻辑。
也就是说DAO不和业务挂钩
package com.powernode.bank.dao;
import com.powernode.bank.pojo.Account;
public interface AccountDao {
/**
* 根据账号查询账户信息
* @param actno
* @return
*/
Account selectByActno(String actno);
/**
* 更新账户信息
* @param act
* @return
*/
int update(Account act);
}
com.powernode.bank.dao.impl
AccountDaoImpl
package com.powernode.bank.dao.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
@Resource(name="jdbcTemplate")
private JdbcTemplate jdbcTemplate;
@Override
public Account selectByActno(String actno) {
String sql = "select actno,balance from t_act where actno = ?";
Account account = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), actno);
return account;
}
@Override
public int update(Account act) {
String sql = "update t_act set balance = ? where actno = ?";
int count = jdbcTemplate.update(sql, act.getBalance(), act.getActno());
return count;
}
}
com.powernode.bank.pojo
Account
package com.powernode.bank.pojo;
public class Account {
private String actno;
private double balance;
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public double getBalance() {
return balance;
}
public Account() {
}
public void setActno(String actno) {
this.actno = actno;
}
public void setBalance(double balance) {
this.balance = balance;
}
@Override
public String toString() {
return "Account{" +
"actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
}
com.powernode.bank.service
AccountService 业务接口
事务就是在这个接口下控制的
package com.powernode.bank.service;
/**
* 业务接口
* 事务就是在这个接口下控制的
*/
public interface AccountService {
/**
* 转账业务方法
* @param fromActno 从这个账户转出
* @param toActno 转入这个账户
* @param money 转账金额
*/
void transfer(String fromActno,String toActno,double money);
}
com.powernode.bank.service.impl
AccountServiceImpl
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import javax.annotation.Resource;
@Repository("accountService")
public class AccountServicecImpl implements AccountService {
@Resource(name="accountDao")
private AccountDao accountDao;
//控制事务,因为在这个方法中要完成所有的转账业务
@Override
public void transfer(String fromActno, String toActno, double money) {
//查询转出账户的余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if(fromAct.getBalance() < money){
throw new RuntimeException("余额不足!!!");
}
//余额充足:
Account toAct = accountDao.selectByActno(toActno);
//将内存中两个对象的余额先修改
fromAct.setBalance(fromAct.getBalance()-money);
toAct.setBalance(toAct.getBalance()+money);
//数据库更新
int update = accountDao.update(fromAct);
update += accountDao.update(toAct);
if(update!=2){
throw new RuntimeException("转账失败,联系银行");
}
}
}
4、spring.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.powernode.bank"></context:component-scan>
<!--配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/dududu"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<!--配置JDBCTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
5、测试程序
package com.powernode;
import com.powernode.bank.service.AccountService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SpringTxTest {
@Test
public void testSpringTx(){
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
AccountService accountService = (AccountService) ac.getBean("accountService");
try {
accountService.transfer("act_001","act_002",10000);
System.out.println("转账成功");
}catch (Exception e){
e.printStackTrace();
System.out.println("转账失败");
}
}
}
6、运行结果(成功)
7、模拟异常
在两条update语句之间模拟异常
//数据库更新
int update = accountDao.update(fromAct);
//模拟异常
String s = null;
s.toString();
update += accountDao.update(toAct);
当前数据库中的数据:
执行测试程序【5】:
丢了一万块钱
事务管理:
@Repository("accountService")
public class AccountServicecImpl implements AccountService {
@Resource(name="accountDao")
private AccountDao accountDao;
//控制事务,因为在这个方法中要完成所有的转账业务
@Override
public void transfer(String fromActno, String toActno, double money) {
//1、开启事务
//2、执行核心代码
//查询转出账户的余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if(fromAct.getBalance() < money){
throw new RuntimeException("余额不足!!!");
}
//余额充足:
Account toAct = accountDao.selectByActno(toActno);
//将内存中两个对象的余额先修改
fromAct.setBalance(fromAct.getBalance()-money);
toAct.setBalance(toAct.getBalance()+money);
//数据库更新
int update = accountDao.update(fromAct);
update += accountDao.update(toAct);
if(update!=2){
throw new RuntimeException("转账失败,联系银行");
}
//3、如果执行业务流程过程中,没有异常提交事务
//4、如果执行业务流程过程中,遇到异常则回滚事务
}
}
三、Spring对事务的支持
1、Spring实现事务的两种方式
- 编程式事务:通过编写代码的方式来实现事务的管理
- 声明式事务:1、基于注解方式 2、基于XML配置方式
2、Spring事务管理API
spring 对事务的管理底层实现方式是基于AOP实现的。采用AOP的方式进行了封装。所以spring专门针对事务开发了一套API,核心接口如下:
PlatformTransactionManager接口:spring事务管理器核心接口:
DataSourceTransactionManager:支持JdbcTemplate、Mybatis、Hibernate等事务管理
JtaTransaction
如果要在spring中使用J都不错Template,就要使用DataSourceTransactionManager来管理事务
3、解决上述代码没有事务的问题
spring声明式事务之使用注解方式:
3.1配置spring.xml
1、配置事务管理器
管理事务需要将connection.setAutocommit(false)–>需要Connection对象—>需要连接对象—>需要数据源
<!--配置事务管理器-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
2、添加命名空间
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
3、开启事务注解驱动器
告诉spring框架,采用注解的方式去控制事务
<!--事务注解驱动器-->
<tx:annotation-driven transaction-manager="txManager"></tx:annotation-driven>
3.2使用事务
在业务实现类上/方法 添加注解@Transactional即可。
写在类上:代表这个类中所有的方法都应用事务
写在方法上:只是针对这个方法应用事务
重新执行测试程序(空指针异常):
数据库中的数据(一分钱没丢):
假设没有异常,删掉空指针异常的代码,运行结果:
数据库中的数据:
四、事务之传播行为
@Transactional的属性:
较为重要的:传播行为(propagation)、隔离级别(isolation)
什么是事务的传播行为?
在service类中有a()方法和b()方法,a()方法上有事务,b()方法上也有事务,当a()方法执行过程中调用了b()方法,事务是如何传递的?
事务传播行为在spring框架中被定义为枚举类型:
七种传播行为:
1、REQUIRED:支持当前事务,如果不存在就新建一个【没有就新建,有就加入】
2、SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行【有就加入,没有就不管了】
3、MANDATORY:必须运行在一个事务中,如果当前没有事务发生,将抛出一个异常【有就加入,没有就抛出异常】
4、REQUIRES_NEW:开启一个新的事物,如果一个事务已经存在,则将这个事务挂起【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】
5、NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务【不支持事务,存在就挂起】
6、NEVER:以非事务方式运行,如果事务存在,抛出异常【不支持事务,存在就抛异常】
7、NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样【有事务的话,就在这个事务里嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样】
不设置的话默认是REQUIRED
五、事务之隔离级别
事务隔离级别类似于教室A和教室B之间的一道墙,隔离级别越高表示墙体越厚,隔音效果越好。
数据库中读取数据存在的三大问题:(三大读问题)【重要】
- 脏读:读取到没有提交到数据库中的数据,叫做脏读
- 不可重复读:在同一个事务当中,第一次和第二次读取的数据不一样
- 幻读:读到的数据是假的
事务隔离级别包括四个级别:【重要】
- 读未提交:READ_UNCOMMITTED
这种隔离级别,存在脏读问题
- 读提交:READ_COMMITTED
解决了脏读问题,其他事务提交之后才能读到,但存在不可重复读问题
- 可重复读:REPEATABLE_READ
解决了不可重复读,可以达到可重复读效果,只要当前事务不结束,读取到的数据一直都是一样的。但存在幻读问题
- 序列化:SERIALIZABLE
解决了幻读问题,事务排队执行。不支持并发
如果是Oracle数据库 默认是READ_COMMITTED
如果是MySQl数据库 默认是REPEATABLE_READ