1、Spring 的事务管理
事务原本是数据库中的概念,在 Dao 层。但一般情况下,需要将事务提升到业务层,即 Service 层。这样做是为了能够使用事务的特性来管理具体的业务。
在 Spring 中通常可以通过以下两种方式来实现对事务的管理:
(1)使用 Spring 的事务注解管理事务;
(2)使用 AspectJ 的 AOP 配置管理事务。
2、Spring 事务管理 API
Spring 的事务管理,主要用到两个事务相关的接口。 官方压缩文档下载地址:https://repo.spring.io/release/org/springframework/spring/
(1)事务管理器接口(重点)
事务管理器是 PlatformTransactionManager 接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息。
事务管理器接口的实现类
A、 常用的两个实现类
PlatformTransactionManager 接口有两个常用的实现类:
➢ DataSourceTransactionManager:使用 JDBC 或 MyBatis 进行数据库操作时使用。
➢ HibernateTransactionManager:使用 Hibernate 进行持久化数据时使用。
B、 Spring 的回滚方式(理解)
Spring 事务的默认回滚方式是:发生运行时异常和 error 时回滚,发生受查(编译)异常时提交。不过,对于受查异常,程序员也可以手工设置其回滚方式。
C、 回顾错误与异常(理解)
Throwable 类是 Java 语言中所有错误或异常的超类。只有当对象是此类(或其子类之一) 的实例时,才能通过Java 虚拟机或者 Java 的 throw 语句抛出。
Error 是程序在运行过程中出现的无法处理的错误,比如 OutOfMemoryError、 ThreadDeath、NoSuchMethodError 等。当这些错误发生时,程序是无法处理(捕获或抛出) 的,JVM 一般会终止线程。
程序在编译和运行时出现的另一类错误称之为异常,它是JVM通知程序员的一种方式。 通过这种方式,让程序员知道已经或可能出现错误,要求程序员对其进行处理。
异常分为运行时异常与受查异常:
运行时异常,是 RuntimeException 类或其子类,即只有在运行时才出现的异常。如,NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException 等均属于运行时异常。这些异常由 JVM 抛出,在编译时不要求必须处理(捕获或抛出)。但,只要代码编写足够仔细,程序足够健壮,运行时异常是可以避免的。
受查异常,也叫编译时异常,即在代码编写时要求必须捕获或抛出的异常,若不处理, 则无法通过编译。如SQLException,ClassNotFoundException,IOException 等都属于受查异常。
RuntimeException 及其子类以外的异常,均属于受查异常。当然,用户自定义的 Exception 的子类,即用户自定义的异常也属受查异常。程序员在定义异常时,只要未明确声明定义的为 RuntimeException 的子类,那么定义的就是受查异常。
(2) 事务定义接口
事务定义接口TransactionDefinition中定义了事务描述相关的三类常量:事务隔离级别、 事务传播行为、事务默认超时时限,及对它们的操作。 (控制事务是什么样的,怎么操作的?)
A、 定义了五个事务隔离级别常量(掌握)
这些常量均是以 ISOLATION_开头。即形如 ISOLATION_XXX:
➢ DEFAULT:采用 DB 默认的事务隔离级别。MySql 的默认为 REPEATABLE_READ; Oracle 默 认为 READ_COMMITTED。
➢ READ_UNCOMMITTED:读未提交。未解决任何并发问题。
➢ READ_COMMITTED:读已提交。解决脏读,存在不可重复读与幻读。
脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据,依据脏数据所做的操作可能是不正确的。
➢ REPEATABLE_READ:可重复读。解决脏读、不可重复读,存在幻读 。
➢ SERIALIZABLE:串行化。不存在并发问题。(安全、性能低)
B、 定义了七个事务传播行为常量(掌握)
所谓事务传播行为是指,处于不同事务中的方法在相互调用时,执行期间事务的维护情况。如,A 事务中的方法 doSome()调用 B 事务中的方法 doOther(),在调用执行期间事务的维护情况,就称为事务传播行为。事务传播行为是加在方法上的。
事务在方法之间可以传递,方法怎么来使用事务。通过传播行为指定方法怎么使用事务。
事务传播行为常量都是以 PROPAGATION_ 开头,形如 PROPAGATION_XXX:
PROPAGATION_REQUIRED
PROPAGATION_REQUIRES_NEW
PROPAGATION_SUPPORTS
PROPAGATION_MANDATORY
PROPAGATION_NESTED
PROPAGATION_NEVER
PROPAGATION_NOT_SUPPORTED
a、 PROPAGATION_REQUIRED:
指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事 务,则创建一个新事务。这种传播行为是最常见的选择,也是 Spring 默认的事务传播行为。
如该传播行为加在 doOther()方法上。若 doSome()方法在调用 doOther()方法时就是在事 务内运行的,则doOther()方法的执行也加入到该事务内执行。若 doSome()方法在调用 doOther()方法时没有在事务内执行,则doOther()方法会创建一个事务,并在其中执行。
PROPAGATION_REQUIRED的传播行为加在doOther()方法上,doSome调用doOther:
1、doSome中有事务,那么doOther就加到doSome方法之中执行。
2、doSome没有事务。那么doOther就自己创建创建一个事务,并在其中执行。
b、 PROPAGATION_SUPPORTS
指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。(查询操作)
c、 PROPAGATION_REQUIRES_NEW
总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。
事务是加在业务方法上的
C、 定义了默认事务超时时限
超时:事务的最长执行时间,也就是一个方法最长的执行时间。当时间到了,spring会回滚方法的执行。秒为单 位。默认是-1,也就是数据库自己的超时。
常量 TIMEOUT_DEFAULT 定义了事务底层默认的超时时限,及不支持事务超时时限设置 的 none 值。
注意,事务的超时时限起作用的条件比较多,且超时的时间计算点较复杂。所以,该值 一般就使用默认值即可。
3 、程序举例环境搭建
举例:购买商品 trans_sale 项目
本例要实现购买商品,模拟用户下订单,向订单表添加销售记录,从商品表减少库存。
实现步骤:
Step0:创建数据库表
创建两个数据库表 sale 、goods。
sale 销售表 id:销售表的id; gid:购买商品的id; nums:购买的数量
goods表 id:商品的id name:商品的名称 amount:商品的库存 price:商品的单价
Step1: maven 依赖 pom.xml
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!--spring的依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.16.RELEASE</version>
</dependency>
<!--事务的依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.3.16.RELEASE</version>
</dependency>
<!--JDBC的依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.16.RELEASE</version>
</dependency>
<!--Mybatis的依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<!--Mybatis整合spring的依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
</dependency>
<!--mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.9</version>
</dependency>
<!--连接池的依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
<!--插件-->
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
Step2:创建实体类
创建实体类 Sale 与 Goods
Step3:定义 dao 接口
定义两个 dao 的接口 SaleDao , GoodsDao
Step4:定义 dao 接口对应的 sql 映射文件
Step5 mybatis.xml配置文件
Step6 service
package org.example.service.impl;
import org.example.beans.Goods;
import org.example.beans.Sale;
import org.example.dao.GoodsDao;
import org.example.dao.SaleDao;
import org.example.excep.NotEnoughException;
import org.example.service.BuyGoodsService;
/**
* #-*- coding = utf-8 -*-
* #@Time: 2020/11/24 20:47
* #@Author: Winter
* #@File: BuyGoodsServiceImpl.py
* #@Software: IntelliJ IDEA
**/
public class BuyGoodsServiceImpl implements BuyGoodsService {
//定义Dao的依赖对象
private GoodsDao goodsDao; /*商品Dao*/
private SaleDao saleDao; /*销售Dao*/
/*set方法,【设置注入】*/
public void setGoodsDao(GoodsDao goodsDao) {
this.goodsDao = goodsDao;
}
public void setSaleDao(SaleDao saleDao) {
this.saleDao = saleDao;
}
/**
*
* @param goodsId:购买商品的id
* @param nums:购买商品的数量
*/
@Override
public void buyGoods(Integer goodsId, Integer nums) throws NullPointerException,NotEnoughException{
//生成订单
Sale sale = new Sale();
sale.setGid(goodsId);
sale.setNums(nums);
saleDao.insertSale(sale); /*将购买的商品加入到sale中*/
//修改库存
Goods goods = goodsDao.selectGoodsById(goodsId); /*先查找*/
if(goods==null){
//没有商品
throw new NullPointerException(goodsId+"没有该商品......");
}
if(goods.getAmount()<nums){
//库存不足
throw new NotEnoughException(goodsId+"库存不足......");
}
//操作库存
goods.setAmount(nums); /*这个是购买的商品*/
goodsDao.updateGoods(goods);
}
}
Step7:定义异常类
Step8:applicationContext.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">
<!--声明Mybatis对象-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--11、声明数据源DataSource init和close是DruidDataSource类或者其父类中的方法 。作用:访问数据库-->
<bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!--读取属性配置文件的key值,使用${key}-->
<!--2、指定数据库的url-->
<property name="url" value="${jdbc.url}"/> <!--setUrl()-->
<!--3、数据库的用户名-->
<property name="username" value="${jdbc.username}"/> <!--setUsername()-->
<!--4、数据库的密码-->
<property name="password" value="${jdbc.password}"/> <!--setPassword()-->
</bean>
<!--22、声明SqlSessionFactoryBean,创建SqlSessionFactory对象 目的:获取SqlSession -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--1、数据源-->
<property name="dataSource" ref="myDataSource"/>
<!--2、指定Mybatis的主配置文件-->
<property name="configLocation" value="classpath:mybatis.xml"/>
</bean>
<!--33、声明Mybatis的扫描器,创建Dao接口的实现类对象,不需要id。相当于之前的student,上面相当于School-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--1、指定SqlSessionFactory对象,能够获取SqlSession-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!--2、指定Dao接口的包名,框架会把这个包中的所有接口一次创建Dao对象【这里要修改】-->
<property name="basePackage" value="org.example.dao"/>
</bean>
<!--声明业务层对象-->
<!--44、声明Service-->
<bean id="buyService" class="org.example.service.impl.BuyGoodsServiceImpl">
<!--name是属性名,ref是对象名-->
<property name="goodsDao" ref="goodsDao"/> <!--setGoodsDao()-->
<property name="saleDao" ref="saleDao"/> <!--setSaleDao()-->
</bean>
</bean>
Step9:测试代码
package org.example;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config); //构造方法
String names[] = ctx.getBeanDefinitionNames();
for (String name:names) {
System.out.println(name);
}
}
}
购买商品
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config); //构造方法
//applicationContext.xml中的业务层对象,得到BuyGoodsService对象
BuyGoodsService service = (BuyGoodsService) ctx.getBean("buyService");
//买商品
service.buyGoods(1001,2);
System.out.println("购买商品成功......");
}
}
库存不足【原库存没有减少】
service.buyGoods(1001,200);
System.out.println("购买商品成功......");
没有该商品
service.buyGoods(1005,20);
System.out.println("购买商品成功......");
4、使用 Spring 的事务注解管理事务(掌握)
通过@Transactional 注解方式,可将事务织入到相应 public 方法中,实现事务管理。【业务层的公共方法上】
@Transactional 的所有可选属性如下所示:
➢ propagation:用于设置事务传播属性。该属性类型为 Propagation 枚举,默认值Propagation.REQUIRED。
➢ isolation:用于设置事务的隔离级别。该属性类型为 Isolation 枚举,默认值为 Isolation.DEFAULT。
➢ readOnly:用于设置该方法对数据库的操作是否是只读的。该属性为 boolean,默认值 为 false。【当查询的时候,可以把这个参数设置为true,告诉数据库这个操作是不需要事务的,提高效率】
➢ timeout:用于设置本操作与数据库连接的超时时限。单位为秒,类型为 int,默认值为 -1,即没有时限。
➢ rollbackFor:指定需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有 一个异常类时,可以不使用数组。 【当发生这个异常时进行回滚】
➢ rollbackForClassName:指定需要回滚的异常类类名。类型为 String[],默认值为空数组。 当然,若只有一个异常类时,可以不使用数组。
➢ noRollbackFor:指定不需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若 只有一个异常类时,可以不使用数组。
➢ noRollbackForClassName:指定不需要回滚的异常类类名。类型为 String[],默认值为空 数组。当然,若只有一个异常类时,可以不使用数组。
一般主要是propagation、 isolation、 rollbackFor
需要注意的是,@Transactional 若用在方法上,只能用于 public 方法上。对于其他非 public 方法,如果加上了注解@Transactional,虽然 Spring 不会报错,但不会将指定事务织入到该 方法中。因为 Spring 会忽略掉所有非 public 方法上的@Transaction 注解。
使用注解的步骤:
1、修改spring的配置文件
(1)声明事务管理器,管理器完成事务commit、rollback
(2)声明事务的注解驱动,告诉框架,使用注解完成事务的设置2、在Service的实现类的public方法之上加入@Tranactional
在applicationContext.xml
(1)声明事务管理器
<!--声明事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--数据源-->
<property name="dataSource" ref="myDataSource"/>
</bean>
(2)开启注解驱动
<tx:annotation-driven transaction-manager="transactionManager"/>
(3)在Service的实现类的public方法之上加入@Tranactional
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT,
timeout = 20,
rollbackFor = {NullPointerException.class,NotEnoughException.class})
@Override
public void buyGoods(Integer goodsId, Integer nums) throws NullPointerException,NotEnoughException{
...
...
...
}
/**
传播行为
propagation = Propagation.REQUIRED
指定的方法必须在事务内执行。
若当前存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事务。
这种传播行为是最常见的选择,也是 Spring 默认的事务传播行为。
隔壁级别
isolation = Isolation.DEFAULT
:采用 DB 默认的事务隔离级别。
MySql 的默认为 REPEATABLE_READ; Oracle 默认为 READ_COMMITTED。
超时时间
timeout = 20:
超时时间是20s
rollbackFor = {NullPointerException.class,NotEnoughException.class})
指定需要回滚的异常类。类型为 Class[],默认值为空数组。
当然,若只有一个异常类时,可以不使用数组。
也就是遇到这两个异常时发生回滚
*/
测试
数据库里库存不足和没有该商品都不影响数据库,再测试正常购买
只是之前库存不足和没有该商品的id跳过去了