Spring 事务(声明式) 学习笔记

3 篇文章 1 订阅

 

  开篇

   前段时间回头再仔细看sping框架时候,跟着网上的视频又将sping的事务操作又温习了一遍,通过这篇文章进行一下总结。

     只要是搜事务,找到这篇文章肯定都对事务有所了解。事务管理是在开发过程中必不可少的,不要小看这点操作,可能就是我们的一个注解,如果是真实的企业级开发,是非常有必要的。事务主要是用来保持数据的完整性和一致性。事务的一系列动作他们被当做是一个独立的运行单元,要么全部成功或者全部失败。

事务的特性

   学习过数据库的应该都有了解到事务的四个属性(ACID):原子性、一致性、隔离性、持久性。这里不详细介绍事务的四个特性了,网上随便搜一篇写的都很详细。接下来主要看Spring操作数据库时候,怎么遵循这四个特性的。

Spring的事务管理

  Spring作为企业级的开发框架,Spring在不同的事务管理API上定义了一个抽象层,一般得开发人员不需要去了解底层的事务管理API,知道起API的作用就能使用事务。

   Spring有多种实现方式;有编程式事务和声明式事务之分,

    编程式事务,就是通过代码的形式将事务的操作嵌入到具体的业务实现场景中,来实现事务的控制。

    声明式事务,也是用到最多的事务实现形式;多数情况下比编程式事务好用,他将事务管理代码从具体的业务场景中抽离出来,以声明的方式来事项事务的管理。这样比声明式事务的好处是代码耦合度降低,不影响整体的业务实现。Spring通过SpringAOP框架支持声明式事务。

Spring中事务管理器的不同实现

   Spring核心事务管理抽象接口 PlatformTransactionManager ,Spring并不直接管理事务,而是提供了多种事务管理器,他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。 
Spring事务管理器的接口是org.springframework.transaction.PlatformTransactionManager,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器。例如,使用JDBC时,通过JdbcTemplate操作数据库,使用Hiberante框架时,操作数据库是通过SessionFactory,不同的平台实现的方式不一样。只要去实现Spring事务管理的接口,后面是如何实现的有不同的方法。下图Spring事务管理接口不同方式实现,图片摘自https://blog.csdn.net/trigl/article/details/50968079

 

下面我是通过DataSourceTransationMananger来实现事务的操作,通过代码的形式详细通过真实的案例来理解spring的操作,下面先创建一个spring项目,结合代码再说后面的概念;  

创建spring项目

通过注解的方式实现

这里我们是通过Sping注解的形式,后面我们再通过配置文件的形式来实现相同的事务操作;

创建Maven Spring项目 方便导入相应的jar包;

除了引入必要的spring核心jar包外,还需要Spring-tx(提供编程式和声明式事务)、spring-jdbc、c3p0、mysql-connector(根据自己的情况)

Spring配置文件

<?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-4.2.xsd"
>

    <!--自动扫描包 -->
    <context:component-scan base-package="com.jdbcTemplate"></context:component-scan>

    <!-- 导入资源文件-->
    <context:property-placeholder location="db.properties"/>

    <!--配置从c3po数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="user" value="${jdbc.user}"></property>
        <property name="password" value="${jdbc.password}"></property>
        <property name="jdbcUrl" value="${jdbc.url}"></property>
        <property name="driverClass" value="${jdbc.dirverClass}"></property>

        <property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
        <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
    </bean>

    <!--配置Spring 的Template -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置NamedParameterJdbcTemplate 该对象没有无参的构造方法,需要使用有参构造器,一般选用dataSourc -->
    <bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
        <constructor-arg ref="dataSource"></constructor-arg>
    </bean>

    <!--配置事务管理器 -->
    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--启用注解事务 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

</beans>

db.propertis 数据源配置信息

jdbc.user=root
jdbc.password=123456
jdbc.dirverClass=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test

jdbc.initPoolSize=5
jdbc.maxPoolSize=10

测试案例 学习视频中的案例,就直接拿过来学习了。

首先这里创建了三张表

 

1.account账户表 字段id、user_name(账户名)、balance(余额);

2.book 图书清单表 字段id、book_code(书编号)、book_name(名称)、book_price(单价);

3.count 库存表 id、book_code(书编号)、book_count(库存量)

业务实现;通过书本的编号查询书的价格,通过书的编号来更新书的库存(这里测试一次只卖一本),通过数的单价更新用户余额。

项目结构

分别创建三张表的实体类:

Acoount,Book,Bookcount

创建dao层,业务操作

public interface BuyBookDao {

    /**
     * 获取书本的price
     */
    int queryBookPrice(String bookCode);
    /**
     * 根据书本的code更新书本的数量
     */
    void updateBookCount(String bookCode);
    /**
     * 资金的更新
     */
    int updateAccount(String userName, int price);
}

Dao实现

package com.springTranscation.dao;

import com.springTranscation.exception.BookException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository("buBookDao")
public class BuyBookDaoImpl implements BuyBookDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public int queryBookPrice(String bookCode) {
        String sql = "select BOOK_PRICE price from BOOK where BOOK_CODE =?";
        return  jdbcTemplate.queryForObject(sql, Integer.class, bookCode);
    }

    @Override
    public void updateBookCount(String bookCode) {
        //检查书的库存是否大于0
        String sqlCount = "SELECT BOOK_COUNT FROM COUNT WHERE BOOK_CODE = ?";
        int count = jdbcTemplate.queryForObject(sqlCount,Integer.class,bookCode);
        if(count == 0){
            throw new BookException("库存不足");
        }
        String sql = "UPDATE COUNT SET BOOK_COUNT = BOOK_COUNT -1 WHERE BOOK_CODE = ?";
        jdbcTemplate.update(sql,bookCode);

    }

    @Override
    public int updateAccount(String useName, int price) {
        //余额是否大于0
        String sqlCount = "SELECT BALANCE balance FROM ACCOUNT WHERE USER_NAME = ?";
        int balance = jdbcTemplate.queryForObject(sqlCount,Integer.class,useName);
        if(balance < price){
            throw new BookException("余额不足");
        }
        String sql = "UPDATE ACCOUNT SET BALANCE = BALANCE - ? WHERE USER_NAME = ?";
        jdbcTemplate.update(sql,price,useName);
        return 0;
    }
}

 

异常处理(余额不足时抛出异常信息)

public class BookException extends RuntimeException{

    public BookException() {
    }

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

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

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

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

service层(一次买一本)

public interface BuyBookService {

    void buyBookByNamePrice(String userName, String bookCode);
}
@Service("BuyBookService")
public class BuyBookServiceImpl implements BuyBookService {

    @Autowired
    private BuyBookDao buyBookDao;

    @Override
    public void buyBookByNamePrice(String userName, String bookCode) {
        System.out.println("获取书单价");
        //获取书的单价
        int price = buyBookDao.queryBookPrice(bookCode);
        System.out.println("更新库存");
        //更新数据库存
        buyBookDao.updateBookCount(bookCode);
        System.out.println("更新余额");
        //更新用户余额
        buyBookDao.updateAccount(userName, price);

    }

完成基本业务之后,编写测试类测试三个方法能否正常执行; 

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:ApplicationContext.xml"})
public class TestMain {

   
    @Autowired
    private BuyBookDao buyBookDao;


    @Test
    public void queryBookPrice() {
        System.out.println(buyBookDao.queryBookPrice("101"));

    }

    @Test
    public void updateBookCount() {
        buyBookDao.updateBookCount("101");
    }

    @Test
    public void updateAccount() {
        buyBookDao.updateAccount("Jerry", 300);
    }

如果没有问题的话,我们这时候开始测试没有添加过的事务的情况时候;

account表Jerry账户余额100元

书目清单,这里我们模拟两本,单价分别是java 80 ,mysql 100,

书的库存表,初始值都是10本;

开始买书

第一次是成功的;

可以看到java书本的库存量减去了1,账户的余额也少了80,这个符合我们的预期,当我么再次后买时,钱已经不能够满足我们的需求,肯定会抛出余额不足的异常,这时候还已经根据书本的code查询进行了减一,余额不足账户表不会变,但是库存表是否会如我们所愿,钱没有减少,书肯定也不能少?测试再次购买

异常正常抛出;看一下数据的情况;

书本的库存减少了一本,而我的账户余额还是没有较少的,这肯定跟我们现实生活中的场景不太符合,我们遵从的是“一手交钱一手交货”这钱没减少,我就好比商家,没收到钱书本还被减少了一本。

该如何实现,余额不足的情况我们的书本的库存不会发生变化?

这里就是我们今天说的事物的管理,我们看一下我们加上事务后会是什么情况,会不会帮我们实现我们需要的真实场景;在serviceImpl层加上@Transactional注解

    @Transactional
    @Override
    public void buyBookByNamePrice(String userName, String bookCode) {
        System.out.println("获取书单价");
        //获取书的单价
        int price = buyBookDao.queryBookPrice(bookCode);
        System.out.println("更新库存");
        //更新数据库存
        buyBookDao.updateBookCount(bookCode);
        System.out.println("更新余额");
        //更新用户余额
        buyBookDao.updateAccount(userName, price);

    }

 

这里我们还是模拟第二次买书,现在的库存是8本,如果按照刚才的情况,余额还是20元的情况下,我们是不够买一本书的,刚才的情况是减少了一本,现在看是什么情况;

继续买书;

同样是上一步相同的操作,这里加上事务的注解之后就没有再更新书本的库存,没有购买成功,这就是我们希望看到的场景。

上面说的是买一本,假如我们还有一个事务方法,通过这个事务方法实现买多本书的情况,会放生什么样的情况?重新写一个买多本书的实现;

public interface BuyBooksService {
    void buyBooksByNamePrice(String userName, List<String> booksCode);
}
@Service("BuyBooksService")
public class BuyBooksServiceImpl implements BuyBooksService{

    @Autowired
    private BuyBookService buyBookService;


    @Override
    @Transactional
    public void buyBooksByNamePrice(String userName, List<String> booksCode) {
         for (String bookCode : booksCode){
             buyBookService.buyBookByNamePrice(userName,bookCode);
         }
    }
}

这里我们将书本的数量都重新设置为10本,账户的余额设置为300,模拟实现第一次可以一次买两本书,第二次的话最多就只能买一本java,在不添加事务的前提下能否成功呢?第二次书本的库存量该如何变化?

测试

第一次肯定是正常的,书本的数量各自减一,账户的约还有120元,符合我们的设想,再次购买会之后买一本java会发生什么情况?

余额是减少了一本java的价格但是两本书的库存确都减少了;怎样避免这种情况的发生呢?我们还将余额改成120元,书本的数量先不变,这次我们加上事务处理看会是什么情况?

余额不足,连第一本也没有买成功;

讲到这里开始今天的主要内容,事务的传播行为和事务的隔离级别;

事务的传播属性;

    当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring定义了七种传播行为:

传播行为含义
PROPAGATION_REQUIRED表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务
PROPAGATION_SUPPORTS表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行
PROPAGATION_MANDATORY表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常
PROPAGATION_REQUIRED_NEW表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager
PROPAGATION_NOT_SUPPORTED表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager
PROPAGATION_NEVER表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常
PROPAGATION_NESTED表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务

    1.使用propagation指定事务的传播行为(Propagation.REQUIRE)默认,即当前事务方法被另外一个事务方法调用时,调用方就使用被调用的同一事务; 我们测试使用的是默认的传播行为,所以在同一是是事务内如果事物存在则在同一个事务中执行;如果出现异常我们的数据都不会发生变化,我们刚才的情况是如果想实现在120元月的情况下,之后买一本java想要买成功,mysql买不成功另算只要保证第一本能够买成功,这时候我们设置为(Propagation.REQUIRE_NEW),在业务层的实现方法Transcational中设置propagation再来测试一下,

  @Transactional(propagation = Propagation.REQUIRES_NEW)

这时候书本的钱正好减少买一本java书的金额,而且java的库存量减一,mysql的库存是没有变的;实现了两个事物之间互不影响,这就是propagation属性的作用,可以根据情况来设置传播行为;最常用的也就是require和require_new,还有五种不是太常用可以作为了解;

事务的隔离级别

   脏读 : 一个事务读取到另一事务未提交的更新数据
  不可重复读 : 在同一事务中, 多次读取同一数据返回的结果有所不同, 换句话说, 
后续读取可以读到另一事务已提交的更新数据. 相反, "可重复读"在同一事务中多次
读取数据时, 能够保证所读数据一样, 也就是后续读取不能读到另一事务已提交的更新数据
   幻读 : 一个事务读到另一个事务已提交的insert数据

Spring事务隔离级别设置

   DEFAULT 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别. 
   未提交读(read uncommited) :脏读,不可重复读,虚读都有可能发生 
   已提交读 (read commited):避免脏读。但是不可重复读和虚读有可能发生 
   可重复读 (repeatable read) :避免脏读和不可重复读.但是虚读有可能发生. 
   串行化的 (serializable) :避免以上所有读问题. 
   Mysql 默认:可重复读 
   Oracle 默认:读已提交

2.事务的隔离级别,通过isolation来指定 通常默认的是 READ_COMMITTED;

总体来说隔离级别和传播行为都是事务的特性,此外还有异常回滚的设置,是否是只读设置,以及超时设置;

/**
     * 1.使用propagation指定事务的传播行为(Propagation.REQUIRE)默认,即当前事务方法被另外一个事务方法调用时,调用方就使用被调用的同一事务;
     * 如果propagation设置为(Propagation.REQUIRES_NEW),如果当前被调用方有事务,则挂起;自己重新创建一个新的事务;
     * 2.事务的隔离级别,通过isolation来指定 通常默认的是 READ_COMMITTED;
     * 3.noRollbackFor 指定哪些异常无需回滚,通常情况下不对其设置,默认;
     * 4.readOnly 只读设置,事务只是进行数据库的读操作,可以帮助数据库引擎优化;
     * 5.timeout 超时等待时间。
     */
   上面就是我们通过注解的方式来实现事务,不同的属性直接在注解中添加即可,使用起来很方便;接下来我们使用通过配置文件的形式来实现上面测试的效果,加上相应的事务属性;

配置文件

<?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"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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-4.2.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--自动扫描包 -->
    <context:component-scan base-package="com.springTranscation.*"></context:component-scan>

    <!-- 导入资源文件-->
    <context:property-placeholder location="db.properties"/>

    <!--配置从c3po数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="user" value="${jdbc.user}"></property>
        <property name="password" value="${jdbc.password}"></property>
        <property name="jdbcUrl" value="${jdbc.url}"></property>
        <property name="driverClass" value="${jdbc.dirverClass}"></property>

        <property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
        <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
    </bean>

    <!--配置Spring 的Template -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>


    <!--通过配置文件的形式实现-->
    <bean id="buyBookDao" class="com.springTranscation.dao.BuyBookDaoImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"></property>
        
    </bean>

    <bean id="buyBookService" class="com.springTranscation.service.BuyBookServiceImpl">
        <property name="buyBookDao" ref="buyBookDao"></property>
    </bean>

    <!--配置事务管理器-->
    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配事务属性-->
     <tx:advice id = "txAdvice" transaction-manager="transactionManager" >
         <tx:attributes>
             <!--根据方法名来设置事务属性 可以设置到具体的方法也可以作用在全部方法 用* 代替-->
             <tx:method name="*" propagation="REQUIRES_NEW"/>
         </tx:attributes>
     </tx:advice>

    <!--配置事务切点将事务的属性与切点关联起来-->
    <aop:config >
        <aop:pointcut id="txPoincut" expression="execution(* com.springTranscation.service.BuyBookService.*(..))"></aop:pointcut>
        <!--关联-->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPoincut"></aop:advisor>
    </aop:config>

</beans>

我们将数据库中的书本改成100元,数量还是10本,第一次是成功的,余额和书本的库存都减少了相应的量;

测试方法

余额不足以买一本书的时候;

书本和余额都不会减少;这就说明我们通过配置文件配置的事务起作用了。

以上就是事务的两种不同实现形式。总结算有不足之处请指正!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值