前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >推荐学java——Spring事务

推荐学java——Spring事务

原创
作者头像
逆锋起笔
发布2022-01-25 09:30:30
8781
发布2022-01-25 09:30:30
举报
文章被收录于专栏:逆锋起笔逆锋起笔

前情回顾

已经学习了Spring基础入门知识Spring AOP知识,在上一节内容中我们还将SpringMyBatis结合起来使用,熟悉开发模式。这节学习 Spring 中的事务,同样是重要内容。

事务概念

其实和我们前面学习 MySql 时,了解到的事务是同一概念,指的是一组或多条SQL语句的执行结果要么全部成功,要么全部失败,不会有其他结果,这就叫事务。事务的出现也是为了很好的解决现实生活中的问题。

Spring 事务管理器

使用 Spring 的事务管理器,管理不同数据库访问技术的事务处理。开发者只需要掌握 Spring事务处理这一个方案,就可以实现使用不用数据库访问技术的事务管理。

事务管理面向的是Spring,由Spring管理事务,做事务提交,事务回滚。

事务管理器接口:PlatformTransactionManager,其有很多实现类,基本上不同数据库访问技术都有对应的实现类,我们要学习的是 DataSourceTransactionManager ,该实现类适合 jdbc 方式和 MyBatis 方式。

Spring事务定义接口

接口 TransactionDefinition定义了三类常量,说明有关事务控制的属性。

事务属性:

  • 隔离级别
  • 传播行为
  • 事务的超时

Spring事务隔离级别与传播

隔离级别:控制不同事务之间的影响程度。和学习MySQL中的事务隔离是一个意思。

5个值,只有四个隔离级别:

  1. DEFAULT:采用数据库默认的事务隔离级别,MySQL默认REPEATABLE_READ,Oracle默认READ_UNCOMMITTED
  2. READ_UNCOMMITTED:读未提交,未解决任何并发问题。
  3. READ_COMMITTED:读已提交,解决脏读,存在不可重复读与幻读
  4. REPEATABLE_READ:可重复读,解决了脏读、不可重复读,存在幻读
  5. SERIALIZABLE:串行化,不存在并发问题。

Spring事务传播行为,标识方法有无事务:

  • PROPAGATION_REQUIRED:Spring默认传播行为,调用方法是,如果有事务则使用当前事务,如果没有事务,则会新建事务,方法在新建的事务中执行。
  • PROPAGATION_SUPPORTS:支持,方法有无事务都可正常执行。
  • PROPAGATION_SUPPORTS_NEW:新建,在调用方法时如果存在事务,则先暂停,直到新建事务执行结束;如果不存在事务,还是新建事务执行方法。
  • PROPAGATION_MANDATORY:
  • PROPAGATION_NESTED:
  • PROPAGATION_NEVER:
  • PROPAGATION_NOT_SUPPORTED:

其中前三个必须掌握。

Spring添加事务

Spring提供了两种方式添加事务:

  • Spring框架自己的注解 @Transactional
  • 使用 AspectJ 框架的 AOP 配置

Spring事务应用案例

场景需求

出售商品简单流程,每当出售一件商品,那么销售记录表应该增加一条该商品的记录,同时,应该更新商品表中该商品的库存,而这两者应该是同时成功,或者同时失败,才是正确的业务逻辑,而要保证这一结果,就需要使用事务

实现流程分析

除了业务分析和添加事务外,其他的流程和上一节内容是相同的,再来温习一下具体步骤:

  1. 创建数据库、数据表

这里小编还用上节内容中的数据库,表需要新建两张:salegoods .

sale表字段:id(自动增长、int、主键、销售记录ID)gid(int、商品ID)num(int、购买数量);

goods表字段:id(自动增长、int、主键、商品ID)name(varchar(255)、商品名称)account(int、商品库存)price(float、商品单价)

其中,goods表中可以先维护两条数据,小编这里的数据如下:

1001,50,手机5G,5299

1002,10,显示屏4K,2299

  1. 创建实体类

这个操作估计各位已经轻车熟路了,废话不多说,上代码:

sale实体类代码:

public class Sale {

代码语言:txt
复制
   private Integer id;
代码语言:txt
复制
   private Integer gid;
代码语言:txt
复制
   private Integer num;
代码语言:txt
复制
   public Integer getId() {
代码语言:txt
复制
       return id;
代码语言:txt
复制
   }
代码语言:txt
复制
   public void setId(Integer id) {
代码语言:txt
复制
       this.id = id;
代码语言:txt
复制
   }
代码语言:txt
复制
   public Integer getGid() {
代码语言:txt
复制
       return gid;
代码语言:txt
复制
   }
代码语言:txt
复制
   public void setGid(Integer gid) {
代码语言:txt
复制
       this.gid = gid;
代码语言:txt
复制
   }
代码语言:txt
复制
   public Integer getNum() {
代码语言:txt
复制
       return num;
代码语言:txt
复制
   }
代码语言:txt
复制
   public void setNum(Integer num) {
代码语言:txt
复制
       this.num = num;
代码语言:txt
复制
   }

}

goods实体类代码:

public class Goods {

代码语言:txt
复制
   private Integer id;
代码语言:txt
复制
   private String name;
代码语言:txt
复制
   private Float price;
代码语言:txt
复制
   private Integer account;
代码语言:txt
复制
   public Integer getId() {
代码语言:txt
复制
       return id;
代码语言:txt
复制
   }
代码语言:txt
复制
   public void setId(Integer id) {
代码语言:txt
复制
       this.id = id;
代码语言:txt
复制
   }
代码语言:txt
复制
   public String getName() {
代码语言:txt
复制
       return name;
代码语言:txt
复制
   }
代码语言:txt
复制
   public void setName(String name) {
代码语言:txt
复制
       this.name = name;
代码语言:txt
复制
   }
代码语言:txt
复制
   public Float getPrice() {
代码语言:txt
复制
       return price;
代码语言:txt
复制
   }
代码语言:txt
复制
   public void setPrice(Float price) {
代码语言:txt
复制
       this.price = price;
代码语言:txt
复制
   }
代码语言:txt
复制
   public Integer getAccount() {
代码语言:txt
复制
       return account;
代码语言:txt
复制
   }
代码语言:txt
复制
   public void setAccount(Integer account) {
代码语言:txt
复制
       this.account = account;
代码语言:txt
复制
   }

}

  1. 创建Dao和对应的Mapper

SaleDao代码如下:

public interface SaleDao {

代码语言:txt
复制
   int insertSale(Sale sale);

}

这里是新增销售记录,所以只需要一个添加记录的接口即可。

SaleDaoMapper.xml如下:

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE mapper

代码语言:txt
复制
       PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
代码语言:txt
复制
       "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.javafirst.dao.SaleDao">

代码语言:txt
复制
   <insert id="insertSale">
代码语言:txt
复制
       insert into sale(gid,num) values (#{gid},#{num})
代码语言:txt
复制
   </insert>

</mapper>

GoodsDao代码如下:

public interface GoodsDao {

代码语言:txt
复制
   int updateAccount(Goods goods);
代码语言:txt
复制
   Goods selectById(Integer gid);

}

提供两个方法,一个是我们要更新库存使用,另一个我们需要根据给定的ID查询商品库存,避免更新后出现库存为负的情况。

GoodsDaoMapper.xml代码如下:

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE mapper

代码语言:txt
复制
       PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
代码语言:txt
复制
       "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.javafirst.dao.GoodsDao">

代码语言:txt
复制
   <update id="updateAccount">
代码语言:txt
复制
       update goods set account = account - #{account} where id = #{id}
代码语言:txt
复制
   </update>
代码语言:txt
复制
   <select id="selectById" resultType="com.javafirst.daomain.Goods">
代码语言:txt
复制
       select * from goods where id = #{id}
代码语言:txt
复制
   </select>

</mapper>

  1. MyBatis核心配置

这都是老规矩了,上代码:

mybatis-config.xml代码如下:

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE configuration

代码语言:txt
复制
       PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
代码语言:txt
复制
       "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

代码语言:txt
复制
   <!--  日志  -->
代码语言:txt
复制
   <settings>
代码语言:txt
复制
       <setting name="logImpl" value="STDOUT_LOGGING"/>
代码语言:txt
复制
   </settings>
代码语言:txt
复制
   <typeAliases>
代码语言:txt
复制
       <typeAlias type="com.javafirst.daomain.Sale"/>
代码语言:txt
复制
       <typeAlias type="com.javafirst.daomain.Goods"/>
代码语言:txt
复制
   </typeAliases>
代码语言:txt
复制
   <mappers>
代码语言:txt
复制
       <mapper resource="mapper/SaleDaoMapper.xml"/>
代码语言:txt
复制
       <mapper resource="mapper/GoodsDaoMapper.xml"/>
代码语言:txt
复制
   </mappers>

</configuration>

  1. 创建 Service 和其实现类

其实这里才是我们的真正业务逻辑处理,前面的都是支撑,这个整体流程需要多些案例,慢慢感受。

public interface BuyGoodsService {

代码语言:txt
复制
   void buyGoods(Integer gid, Integer num);

}

其实现类代码如下:

/**

代码语言:txt
复制
* desc:
* author: 推荐学java
* <p>
* weChat: studyingJava
*/   public class BuyGoodsImpl implements BuyGoodsService {
   
代码语言:txt
复制
   SaleDao saleDao;
代码语言:txt
复制
   GoodsDao goodsDao;
代码语言:txt
复制
   public void setSaleDao(SaleDao saleDao) {
代码语言:txt
复制
       this.saleDao = saleDao;
代码语言:txt
复制
   }
代码语言:txt
复制
   public void setGoodsDao(GoodsDao goodsDao) {
代码语言:txt
复制
       this.goodsDao = goodsDao;
代码语言:txt
复制
   }
代码语言:txt
复制
   @Override
代码语言:txt
复制
   public void buyGoods(Integer gid, Integer num) {
代码语言:txt
复制
       System.out.println("buyGoods方法开始执行了...");
代码语言:txt
复制
       // 先插入记录 再减掉库存
代码语言:txt
复制
       Sale sale = new Sale();
代码语言:txt
复制
       sale.setGid(gid);
代码语言:txt
复制
       sale.setNum(num);
代码语言:txt
复制
       saleDao.insertSale(sale);
代码语言:txt
复制
       // 减掉库存之前 先判断库存数是否大于等于购买数量,避免更新后出现负库存
代码语言:txt
复制
       Goods goods = goodsDao.selectById(gid);
代码语言:txt
复制
       if (null == goods || gid > 1002 || gid < 1001) {
代码语言:txt
复制
           throw new NullPointerException("商品不存在");
代码语言:txt
复制
       }
代码语言:txt
复制
       if (goods.getAccount() < num) {
代码语言:txt
复制
           throw new IndexOutOfBoundsException("库存不足");
代码语言:txt
复制
       }
代码语言:txt
复制
       // 更新库存
代码语言:txt
复制
       Goods buyGoods = new Goods();
代码语言:txt
复制
       buyGoods.setId(gid);
代码语言:txt
复制
       buyGoods.setAccount(num);
代码语言:txt
复制
       goodsDao.updateAccount(buyGoods);
代码语言:txt
复制
   }

}

这里的逻辑是能跑通整个流程就行,还没有加入事务前的逻辑。

  1. Spring核心配置文件

applicationContext.xml代码如下:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

代码语言:txt
复制
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
代码语言:txt
复制
      xmlns:context="http://www.springframework.org/schema/context"
代码语言:txt
复制
      xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
代码语言:txt
复制
   <!--  使用属性文件 配置数据源中数据库链接信息  -->
代码语言:txt
复制
   <context:property-placeholder location="jdbc.properties"/>
代码语言:txt
复制
   <!-- 声明数据源-->
代码语言:txt
复制
   <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
代码语言:txt
复制
       <property name="url" value="${jdbc.url}"/>
代码语言:txt
复制
       <property name="username" value="${jdbc.username}"/>
代码语言:txt
复制
       <property name="password" value="${jdbc.password}"/>
代码语言:txt
复制
   </bean>
代码语言:txt
复制
   <!--  声明 SQLSessionFactoryBean  -->
代码语言:txt
复制
   <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
代码语言:txt
复制
       <property name="dataSource" ref="myDataSource"/>
代码语言:txt
复制
       <property name="configLocation" value="classpath:mybatis-config.xml"/>
代码语言:txt
复制
   </bean>
代码语言:txt
复制
   <!--  声明 MapperScannerConfigurer  -->
代码语言:txt
复制
   <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
代码语言:txt
复制
       <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
代码语言:txt
复制
       <property name="basePackage" value="com.javafirst.dao"/>
代码语言:txt
复制
   </bean>
代码语言:txt
复制
   <!--  注册自定义Service  -->
代码语言:txt
复制
   <bean id="buyGoods" class="com.javafirst.service.impl.BuyGoodsImpl">
代码语言:txt
复制
       <property name="saleDao" ref="saleDao"/>
代码语言:txt
复制
       <property name="goodsDao" ref="goodsDao"/>
代码语言:txt
复制
   </bean>

</beans>

上节内容提到过,这里的基本都是流程性的,我们只需要注册自己业务相关的Service 就行。

  1. 测试

这都是固定代码了,贴一下:

@Test

public void spring_transaction() {

代码语言:txt
复制
   String config = "applicationContext.xml";
代码语言:txt
复制
   ApplicationContext context = new ClassPathXmlApplicationContext(config);
代码语言:txt
复制
   BuyGoodsService buyGoods = (BuyGoodsService) context.getBean("buyGoods");
代码语言:txt
复制
   buyGoods.buyGoods(1001, 10);
代码语言:txt
复制
   System.out.println("测试方法执行完成了!");

}

结果大家可以通过改变参数验证结果,通过查看两张表中的数据变更来对比存在的问题,解决方案是通过Spring事务来控制整体流程的最终结果,下面就来学习事务的使用。

使用 Transactional 注解添加事务

使用步骤:

  1. 在Spring配置文件中声明事务的内容(声明事务管理器,指定使用的哪个事务管理器对象;声明使用哪个注解管理事务,开启事务注解驱动)
  2. 在类的源代码中加入 @Transactional 注解(可添加在类上面,有可添加在方法上面,推荐后者)

Spring配置文件中新增如下代码:

代码语言:html
复制
<!--  声明事务管理器  -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--    给事务管理器指定 要管理哪里的数据源    -->
    <property name="dataSource" ref="myDataSource"/>
</bean>

<!--  声明事务管理器驱动  -->
<tx:annotation-driven transaction-manager="transactionManager"/>

然后在业务代码方法上面添加注解:

代码语言:java
复制
@Transactional(
    propagation = Propagation.REQUIRED, // REQUIRED默认值
    isolation = Isolation.DEFAULT, // 一般选择默认选项
    timeout = 12, // 超时,单位秒,默认-1
    readOnly = false,
    // 返回异常类的class数组 在该数组中声明的异常,程序都会回滚,不在该数组中声明的运行时异常也都会回滚
    rollbackFor = {NullPointerException.class, IndexOutOfBoundsException.class}
)
@Override
public void buyGoods(Integer gid, Integer num) {
    System.out.println("buyGoods方法开始执行了...");

    // 先插入记录 再减掉库存
    Sale sale = new Sale();
    sale.setGid(gid);
    sale.setNum(num);

    saleDao.insertSale(sale);

    // 减掉库存之前 先判断库存数是否大于等于购买数量,避免更新后出现负库存
    Goods goods = goodsDao.selectById(gid);

    if (null == goods) {
        throw new NullPointerException("商品不存在");
    }
    if (goods.getAccount() < num) {
        throw new IndexOutOfBoundsException("库存不足");
    }

    // 更新库存
    Goods buyGoods = new Goods();
    buyGoods.setId(gid);
    buyGoods.setAccount(num);

    goodsDao.updateAccount(buyGoods);
}

接着同样是通过修改参数来做测试,证明加了事务之后,能保证我们在错误操作的情况下,不会多插入销售记录,也就保证了业务方法内的SQL逻辑要么全部成功,要么全部失败。

使用 AspectJ 框架声明事务控制

这是另一种方式使用Spring的事务。

使用 AspectJ 的 AOP 声明事务控制叫做声明式事务。使用步骤如下:

  1. pom.xml中加入 spring-aspects依赖;
  2. 在 spring 的配置文件中声明事务的内容(声明事务管理器,业务方法需要的事务属性,声明切入点表达式)

下面就通过这种方式来做测试事务使用,在这之前,先把我们前面添加事务的方式注释掉,在进行下面的配置。

pom.xml文件添加如下代码:

代码语言:html
复制
<!--    AspectJ aop实现注解    -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.14</version>
</dependency>

applicationContext.xml文件添加如下代码:

代码语言:html
复制
<!--  声明事务管理器  -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--    给事务管理器指定 要管理哪里的数据源    -->
    <property name="dataSource" ref="myDataSource"/>
</bean>

<!--  通过使用AspectJ框架AOP方式实现 Spring事务配置管理  -->
<tx:advice id="buyServiceAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="buyGoods" propagation="REQUIRED" isolation="DEFAULT"
                   rollback-for="java.lang.NullPointerException,java.lang.IndexOutOfBoundsException"/>

        <!--    有多个方法需要添加事务 可以在这里意义列举出来,
                    也可以把方法的命名起的有规则,然后通过通配符*来批量添加

                    通配符举例:add*、*(直接用一个*表示的意思是除了这里添加的方法外,应用于剩下的其他方法)
        -->
    </tx:attributes>
</tx:advice>

<!-- 切入点表达式,说明事务管理器是在哪个包下的哪个类需要使用-->
<aop:config>
    <!--
                id:切入点ID,唯一值
                expression:切入点表达式,指定要执行的方法
    -->
    <aop:pointcut id="servicePointcut" expression="execution(* *..service..*.*(..))"/>
    <!--    将切入点表达式和声明的事务管理器关联起来    -->
    <aop:advisor advice-ref="buyServiceAdvice" pointcut-ref="servicePointcut"/>
</aop:config>

声明事务管理器是固定不变的,其他的其实也都是固定代码,以后当做模板使用即可。

最后是测试,同样通过改变参数,验证结果的正确性。

两种方式对比

前者适合中小型项目,流程比较清晰,容易理解;后者这种配置的方式理解起来难度比较大,不容易读懂,适合大型项目,一般配置好就不会动了。

总结

  • 本文介绍的 Spring事务管理器体系结构和用法,还是新东西,重点掌握事务的应用场景,因为实际开发中是需要用的
  • 代码这块必须掌握事务的使用流程,自己会配置,能让事务起到作用
  • 结合上一节内容,现在需要掌握从头开始搭建一个Spring项目,包括Dao层和业务层以及简单的配置

《推荐学java》系列干到这里,小编似乎也有一种豁然开朗的感觉,并没有开始学习前的恐惧了,而是开轻松,大家加油~

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前情回顾
  • 事务概念
  • Spring 事务管理器
  • Spring事务定义接口
  • Spring事务隔离级别与传播
  • Spring添加事务
  • Spring事务应用案例
    • 场景需求
      • 实现流程分析
        • 使用 Transactional 注解添加事务
          • 使用 AspectJ 框架声明事务控制
            • 两种方式对比
            • 总结
            相关产品与服务
            云数据库 MySQL
            腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档