jjzjj

Spring(四)-声明式事务

xiaoqigui 2023-04-16 原文

Spring-04 声明式事务

1、事务的定义

事务就是由一组逻辑上紧密关联多个工作单元(数据库操作)而合并成一个整体,这些操作要么都执行,要么都不执行

2、事务的特性:ACID

1)原子性A :原子即不可再分,表现:一个事务涉及的多个操作在业务逻辑上缺一不可,保证同一个事务中的操作要不都提交,要不都不提交

2)一致性C :数据的一致性,一个事务中,不管涉及到多少个操作,都必须保证数据提交的正确性(一致);即:如果在事务数据处理中,有一个或者几个操作失败,必须回退所有的数据操作,恢复到事务处理之前的统一状态;

3)隔离性I :程序运行过程中,事务是并发执行的,要求每个事务之间都是隔离的,互不干扰;

4)持久性D :事务处理结束,要将数据进行持久操作,即永久保存。

3、事务的分类:

1)编程式事务-使用jdbc原生的事务处理,可以将事务处理写在业务逻辑代码中,违背aop原则,不推荐;

2)声明式事务-使用事务注解 @Transactional,可以声明在方法上,也可以声明在类上

  •  **优先级**:
     *      <mark>声明在**类上**,会对**当前类内的所有方式生效**(所有方法都有事务处理);</mark>
     *      <mark>声明在**方法上**,只会**对当前方法生效**,当类上和方法上同时存在,**方法的优先级高于类**(有些方法,对声明式事务做特殊属性配置);</mark>
    

4、事务的属性:

4.1 事务的传播行为:propagation属性

事务的传播行为:propagation 属性指定;

一个带事务的方法另一个带事务的方法调用时(事务嵌套),当前事务如何处理:

  • propagation = Propagation.REQUIRED :

    • 默认值,使用调用者的事务(全程就一个事务,如果有事务嵌套,以外部事务为主);
  • propagation = Propagation.REQUIRES_NEW :

    • 调用者事务直接挂起,自己重开新的事务处理,结束提交事务,失败回滚;(当事务嵌套时,内层事务,会重新开启新事务的处理不受外部事务的管理);

4.2 事务的隔离级别:isolation属性

事务的隔离级别:isolation属性指定隔离级别,只有InnoDB支持事务,所有这里说的事务隔离级别指的是InnoDB下的事务隔离级别。

1、读未提交 : 读取其它事务未提交的数据,了解,基本不会使用;

2、读已提交 : oracle的默认事务隔离级别同一个事务处理中,只能读取其它事务提交后的数据(也就是说事务提交之前对其余事务不可见);

3、可重复读 : mysql默认事务隔离级别同一个事务处理中,多次读取同一数据是都是一样的,不受其它事务影响;

4、串行化 : 可以避免上面所有并发问题,但是执行效率最低数据一致性最高

4.3 事务的指定回滚和不会滚

事务的指定回滚和不会滚:Spring在默认的情况下,是对所有的运行时异常会执行事务回滚

1、 rollbackFor : 指定回滚异常,只有产生了指定的异常类型,才会回滚事务;

2、 noRollbackFor : 指定不会滚异常,产生了指定的异常类型,也不会回滚事务;

4.4 事务的超时时长-了解

1、timeout,指定事务出现异常,没有及时回滚,单位是秒,防止事务超时,占用资源;

4.5 事务的只读-了解

1、readOnly=false,默认,可读可写‘;

2、readOnly=true,代表该事务处理,理论上只允许读取,不能修改(只是通知spring,并不是一个强制选项)
目的就是:提示数据库驱动程序和数据库系统,这个事务并不包含更改数据的操作,那么JDBC驱动程序和数据库就有可能根据这种情况对该事务进行一些特定的优化,比方说不安排相应的数据库锁,以减轻事务对数据库的压力,毕竟事务也是要消耗数据库的资源的。
但是你非要在“只读事务”里面修改数据,也并非不可以,只不过对于数据一致性的保护不像“读写事务”那样保险而已。

5、 环境搭建

5.1主要 jar包

<spring.version>4.3.18.RELEASE</spring.version>
<mysql.version>5.1.47</mysql.version>
<c3p0.version>0.9.5.2</c3p0.version>

<!-- transaction begin -->

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>${spring.version}</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>${spring.version}</version>
</dependency>

 <!--c3p0数据源 -->
<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>${c3p0.version}</version>
</dependency>

 <!-- 最主要的是 spring-tx-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>${spring.version}</version>
</dependency>

 <!-- transaction end -->

<!-- mysql begin -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>${mysql.version}</version>
</dependency>
<!-- mysql end -->

5.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">


    <!-- 组件扫描-->
    <context:component-scan base-package="com.kgc.spring"></context:component-scan>

    <!-- spring框架读取外部配置文件-->
    <!-- 方式一-->
    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <!-- 指定配置文件的位置,classpath:是类路径,只有spring可识别 -->
        <property name="location" value="classpath:jdbc.properties"></property>
    </bean>

    <!-- c3p0 数据库配置,可以管理数据库连接,还可以自动重连 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${driver}"></property>
        <property name="jdbcUrl" value="${url}"></property>
        <property name="user" value="${username}"></property>
        <property name="password" value="${password}"></property>
    </bean>

    <!-- Spring框架对JDBC进行封装,我们使用JdbcTemplate可以方便实现对数据库的增删改查操作。 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 数据源事务管理器 -->
    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--
        配置声明式事务注解扫描,扫描所有添加的声明式事务注解,交给事务管理器进行统一管理;
        名称空间是tx结尾,才可以生效;
        transaction-manager属性是指定当前自定义的事务管理器;
        如果事务管理器的id值是transactionManager,可以省略此属性的指定
    -->
    <tx:annotation-driven transaction-manager="dataSourceTransactionManager"></tx:annotation-driven>

6、测试

5.1 购买一辆车(没有事务嵌套)

TES 购买一辆 AudiQ5;

模拟购买一辆车,主要流程:(1,2,3 整体是一个事务)

1、据买家购买汽车编号,获取汽车详情;

2、扣汽车的库存

3、扣买家的余额

5.1.2 主要业务代码

5.1.2.1 扣用户余额业务

BuyerServiceImpl

如果买家余额不足,直接返回;

@Service
public class BuyerServiceImpl implements BuyerService {

    @Autowired
    private BuyerDao buyerDao;

    @Override
    public void subBuyerMoneyByName(String buyerName, Car car) {

        // 根据买家姓名,查询买家详情
        Buyer buyer = buyerDao.selectBuyerByName(buyerName);

        // 判断买家余额是否充足,如果不足,不能继续扣减金额
        if(buyer.getMoney() < car.getPrice()){
            System.out.println(String.format("****** 买家:%s,余额不足! ------", buyerName));
            return;  //直接return
        }

        // 余额充足,执行扣减余额
        int row = buyerDao.updateBuyerMoneyById(buyer.getId(), car.getPrice());

        System.out.println(String.format("****** 买家:%s,余额扣减成功,影响行数:%s ------", buyerName, row));

    }


}
5.1.2.2 扣库存业务

CarsStockServiceImpl

如果库存不足,直接返回;

@Service
public class CarsStockServiceImpl implements CarsStockService {

    @Autowired
    private CarsStockDao carsStockDao;

    @Override
    public void subCarsStockBuyId(Car car) {

        //根据汽车编号,插叙汽车详情
        CarsStock carsStock = carsStockDao.selectCarsStockByCid(car.getId());

        //判断库存是否充足,如果不足,不能购买
        if(carsStock.getStock() <= 0){
            System.out.println("汽车"+car.getName()+"库存不足");
            return;  //直接return
        }

        //库存足够,执行库存减少
        int row = carsStockDao.updateCarStockById(car.getId());

        System.out.println("------ 汽车"+car.getName()+"库存扣减成功" + row +" ------");

    }

}
5.1.2.3 用户买车业务

根据 卖家名字,和汽车编号买车;

BuyCarServiceImpl

@Service("buyCarService") //方便从容器中获取对象
public class BuyCarServiceImpl implements BuyCarService {

    @Autowired
    private CarDao carDao;

    @Autowired
    private CarsStockService carStockService;

    @Autowired
    private BuyerService buyerService;

    //根据 卖家名字,和汽车编号买车
    @Override
    public void buyCar(String buyerName, Integer carId) {

        System.out.println(String.format("------ 买家:%s,购买汽车编号:%s 开始 ------",buyerName,carId));

        // 根据买家购买汽车编号,获取汽车详情
        Car car = carDao.selectCarById(carId);

        // 扣买家的余额
        buyerService.subBuyerMoneyByName(buyerName, car);

        // 扣汽车的库存
        carStockService.subCarsStockBuyId(car);

        System.out.println(String.format("------ 买家:%s,购买汽车编号:%s 结束 ------",buyerName,carId));

    }

}

5.1.3 测试(没有添加事务处理)

5.1.3.1 测试前的数据
  • 汽车价格

  • 用户余额

  • 库存

根据观察,发现用户TES的余额不够买AudiQ5;

5.1.3.2 测试
//没有添加事务处理
@Test
public void testSpringUnUsedTx(){
    //获取买车的业务实现对象
    BuyCarService buyCarService = context.getBean("buyCarService", BuyCarService.class);

    //调用买车的业务方法
    buyCarService.buyCar("TES",1);

}

运行结果:

5.1.3.3 测试后的数据
  • 用户余额

  • 库存

5.1.4 测试 (加上@Transactional 注解添加事务处理)

5.1.4.1 方法上加上@Transactional 注解
@Transactional
public void buyCar(String buyerName, Integer carId) {
   ...
}
5.1.4.1 测试

恢复初始数据后测试;

5.1.5 测试 (增加异常抛出)

余额不足,没有异常直接return,不能触发事务

需要抛出自定义异常才会触发事务处理

5.1.5.1 自定义异常类

BuyCarException

public class BuyCarException extends RuntimeException {

    //生成所有的构造方法
    public BuyCarException() {
    }

    public BuyCarException(String message) {
        super(message);
    }

    public BuyCarException(String message, Throwable cause) {
        super(message, cause);
    }

    public BuyCarException(Throwable cause) {
        super(cause);
    }

    public BuyCarException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }

}
5.1.5.2 抛出异常

当余额或库存不足的时候,抛出自定义异常;

BuyerServiceImpl

@Service
public class BuyerServiceImpl implements BuyerService {

    @Autowired
    private BuyerDao buyerDao;

    @Override
    public void subBuyerMoneyByName(String buyerName, Car car) {

        Buyer buyer = buyerDao.selectBuyerByName(buyerName);

        if(buyer.getMoney() < car.getPrice()){
            System.out.println(String.format("****** 买家:%s,余额不足! ------", buyerName));
            //return; //没有异常直接return,不能触发事务
            //余额不足抛出自定义异常
            
            //*****余额充足,执行扣减余额*****
            throw  new BuyCarException(String.format("****** 买家:%s,余额不足! ------", buyerName));
        }

        int row = buyerDao.updateBuyerMoneyById(buyer.getId(), car.getPrice());

        System.out.println(String.format("****** 买家:%s,余额扣减成功,影响行数:%s ------", buyerName, row));

    }

}

CarsStockServiceImpl

@Service
public class CarsStockServiceImpl implements CarsStockService {

    @Autowired
    private CarsStockDao carsStockDao;

    @Override
    public void subCarsStockBuyId(Car car) {

        CarsStock carsStock = carsStockDao.selectCarsStockByCid(car.getId());

        if(carsStock.getStock() <= 0){
            System.out.println("汽车"+car.getName()+"库存不足");
            //return; //没有异常直接return,不能触发事务
            
            //*****库存不足,执行扣减余额*****
            throw  new BuyCarException("汽车"+car.getName()+"库存不足");
        }

        int row = carsStockDao.updateCarStockById(car.getId());

        System.out.println("------ 汽车"+car.getName()+"库存扣减成功" + row +" ------");

    }

}
5.1.5.3 测试

恢复初始数据后测试;

5.1.5.4 测试 (余额充足)
5.1.5.4.1 测试前的数据
  • 用户余额

  • 库存

5.1.5.4.2测试

5.1.5.4.3 测试后的数据
  • 用户余额

  • 库存

5.2购买两辆车(有事务嵌套) **对过程理解还有问题

JDG 购买一辆 AudiQ5 和一辆 BmwX3

模拟购物车一次购买两辆车,主要流程:(1,2 整体是一个事务)

1、买第一辆车(1.1,1.2,1.3 整体是一个事务 默认情况内部事务不生效)

​ 1.1 据买家购买汽车编号,获取汽车详情;

​ 1.2扣汽车的库存

​ 1.3扣买家的余额

2、买第二辆车(1.1,1.2,1.3 整体是一个事务 默认情况内部事务不生效)

​ 1.1 据买家购买汽车编号,获取汽车详情;

​ 1.2扣汽车的库存

​ 1.3扣买家的余额

5.2.1 主要业务代码

模拟购物车一次购买两辆车;

多次调用购买一辆汽车业务;

BuyCarCartServiceImpl

@Service("BuyCarCartService" )
public class BuyCarCartServiceImpl implements BuyCarCartService {

    @Autowired
    private BuyCarService buyCarService;

    @Override
    @Transactional //购物车外层事务注解,buyCarService接口方法中也有事务注解
    public void buyCarCart(String buyerName, List<Integer> carIds) {
        //模拟购物车垢面多辆车,方便演示事务传播行为,一辆一辆购买(单独调用买车接口)
//        carIds.forEach(carId -> buyCarService.buyCar(buyerName,carId));

        for (int i = 0; i < carIds.size(); i++) {
            //异常处理,防止买第一辆车出现异常后,无法购买第二辆车
            try{
                buyCarService.buyCar(buyerName,carIds.get(i));
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        
    }
}

5.2.1 propagation = Propagation.REQUIRED

默认传播特性,以外部事务为主;propagation = Propagation.REQUIRED 可以不写;

5.2.1.1 测试前的数据
  • 汽车价格

  • 用户余额

  • 库存

5.2.1.2测试
//测试事务存在 事务嵌套 的传播行为
//购物车结算
@Test
public void testSpring(){

    BuyCarCartService buyCarCartService = context.getBean("BuyCarCartService", BuyCarCartService.class);

    //调用购物车买车的业务方法
    buyCarCartService.buyCarCart("JDG", Arrays.asList(1,2));

}

测试结果:

5.2.1.3测试后的数据
  • 用户余额

  • 库存

5.2.1.4 总结

通过查看数据库的数据发现,数据没有改变,说明事务并事务已经回滚,也就是说默认传播特性,以外部事务为主

5.2.3 propagation = Propagation.REQUIRES_NEW

propagation = Propagation.REQUIRES_NEW的传播特性,内部事务会自己重开新的事务处理;

5.2.3.1 内部事务注解添加属性参数

buyCar方法

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void buyCar(String buyerName, Integer carId) {
	...
}
5.2.3.2 测试

恢复数据再测试;

5.2.3.3测试后的数据
  • 用户余额

  • 库存

5.2.3.4 总结

通过查看数据库的数据发现,数据发生改变,说明内部事务重新开起新的事务处理不受外部事务的管理

有关Spring(四)-声明式事务的更多相关文章

  1. ruby-on-rails - active_admin 目录中的常量警告重新声明 - 2

    我正在使用active_admin,我在Rails3应用程序的应用程序中有一个目录管理,其中包含模型和页面的声明。时不时地我也有一个类,当那个类有一个常量时,就像这样:classFooBAR="bar"end然后,我在每个必须在我的Rails应用程序中重新加载一些代码的请求中收到此警告:/Users/pupeno/helloworld/app/admin/billing.rb:12:warning:alreadyinitializedconstantBAR知道发生了什么以及如何避免这些警告吗? 最佳答案 在纯Ruby中:classA

  2. ruby - 我可以使用 aws-sdk-ruby 在 AWS S3 上使用事务性文件删除/上传吗? - 2

    我发现ActiveRecord::Base.transaction在复杂方法中非常有效。我想知道是否可以在如下事务中从AWSS3上传/删除文件:S3Object.transactiondo#writeintofiles#raiseanexceptionend引发异常后,每个操作都应在S3上回滚。S3Object这可能吗?? 最佳答案 虽然S3API具有批量删除功能,但它不支持事务,因为每个删除操作都可以独立于其他操作成功/失败。该API不提供任何批量上传功能(通过PUT或POST),因此每个上传操作都是通过一个独立的API调用完成的

  3. ruby-on-rails - 带 Spring 锁的 Rails 4 控制台 - 2

    我正在使用Ruby2.1.1和Rails4.1.0.rc1。当执行railsc时,它被锁定了。使用Ctrl-C停止,我得到以下错误日志:~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.2/lib/spring/client/run.rb:47:in`gets':Interruptfrom~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.2/lib/spring/client/run.rb:47:in`verify_server_version'from~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.

  4. ruby - 分布式事务和队列,ruby,erlang,scala - 2

    我有一个涉及多台机器、消息队列和事务的问题。因此,例如用户点击网页,点击将消息发送到另一台机器,该机器将付款添加到用户的帐户。每秒可能有数千次点击。事务的所有方面都应该是容错的。我以前从未遇到过这样的事情,但一些阅读表明这是一个众所周知的问题。所以我的问题。我假设安全的方法是使用两阶段提交,但协议(protocol)是阻塞的,所以我不会获得所需的性能,我是否正确?我通常写Ruby,但似乎Redis之类的数据库和Rescue、RabbitMQ等消息队列系统对我的帮助不大——即使我实现某种两阶段提交,如果Redis崩溃,数据也会丢失,因为它本质上只是内存。所有这些让我开始关注erlang和

  5. spring.profiles.active和spring.profiles.include的使用及区别说明 - 2

    转自:spring.profiles.active和spring.profiles.include的使用及区别说明下文笔者讲述spring.profiles.active和spring.profiles.include的区别简介说明,如下所示我们都知道,在日常开发中,开发|测试|生产环境都拥有不同的配置信息如:jdbc地址、ip、端口等此时为了避免每次都修改全部信息,我们则可以采用以上的属性处理此类异常spring.profiles.active属性例:配置文件,可使用以下方式定义application-${profile}.properties开发环境配置文件:application-dev

  6. ruby - 如何使用 ruby​​ mysql2 执行事务 - 2

    我已经开始使用mysql2gem。我试图弄清楚一些基本的事情——其中之一是如何明确地执行事务(对于批处理操作,比如多个INSERT/UPDATE查询)。在旧的ruby-mysql中,这是我的方法:client=Mysql.real_connect(...)inserts=["INSERTINTO...","UPDATE..WHEREid=..",#etc]client.autocommit(false)inserts.eachdo|ins|beginclient.query(ins)rescue#handleerrorsorabortentirelyendendclient.commi

  7. ruby - 如何使用 method_missing 动态声明方法? - 2

    我有一个ruby​​程序,我想接受用户创建的方法,并使用该名称创建一个新方法。我试过这个:defmethod_missing(meth,*args,&block)name=meth.to_sclass我收到以下错误:`define_method':interningemptystring(ArgumentError)in'method_missing'有什么想法吗?谢谢。编辑:我以不同的方式让它工作,但我仍然很好奇如何以这种方式做到这一点。这是我的代码:defmethod_missing(meth,*args,&block)Adder.class_evaldodefine_method

  8. ruby-on-rails - 在 rails 中提交后回滚事务 - 2

    保存成功后可以回滚吗?让我有一个带有属性名称、电子邮件等的用户模型。例如u=User.newu.name="test_name"u.email="test@email.com"u.save现在记录将成功保存在数据库中,之后我想回滚我的事务(不是销毁或删除)。有什么想法吗? 最佳答案 您可以通过交易来做到这一点,请参阅http://markdaggett.com/blog/2011/12/01/transactions-in-rails/例子:User.transactiondoUser.create(:username=>'Nemu

  9. ruby-on-rails - Spring 不起作用。 [未初始化常量 Spring::SID::DL] - 2

    我无法运行Spring。这是错误日志。myid-no-MacBook-Pro:myid$spring/Users/myid/.rbenv/versions/1.9.3-p484/lib/ruby/gems/1.9.1/gems/spring-0.0.10/lib/spring/sid.rb:17:in`fiddle_func':uninitializedconstantSpring::SID::DL(NameError)from/Users/myid/.rbenv/versions/1.9.3-p484/lib/ruby/gems/1.9.1/gems/spring-0.0.10/li

  10. 【云原生】SpringCloud-Spring Boot Starter使用测试 - 2

    目录SpringBootStarter是什么?以前传统的做法使用SpringBootStarter之后starter的理念:starter的实现: 创建SpringBootStarter步骤在idea新建一个starter项目、直接执行下一步即可生成项目。 在xml中加入如下配置文件:创建proterties类来保存配置信息创建业务类:创建AutoConfiguration测试如下:SpringBootStarter是什么? SpringBootStarter是在SpringBoot组件中被提出来的一种概念、简化了很多烦琐的配置、通过引入各种SpringBootStarter包可以快速搭建出一

随机推荐