推荐学java——Spring事务

前情回顾

已经学习了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表中可以先维护两条数据,小编这里的数据如下:

    1100150,手机5G,5299 2100210,显示屏4K,2299
  2. 创建实体类

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

    sale实体类代码:

    1public class Sale { 2 3 private Integer id; 4 private Integer gid; 5 private Integer num; 6 7 public Integer getId() { 8 return id; 9 } 10 11 public void setId(Integer id) { 12 this.id = id; 13 } 14 15 public Integer getGid() { 16 return gid; 17 } 18 19 public void setGid(Integer gid) { 20 this.gid = gid; 21 } 22 23 public Integer getNum() { 24 return num; 25 } 26 27 public void setNum(Integer num) { 28 this.num = num; 29 } 30}

    goods实体类代码:

    1public class Goods { 2 3 private Integer id; 4 private String name; 5 private Float price; 6 private Integer account; 7 8 public Integer getId() { 9 return id; 10 } 11 12 public void setId(Integer id) { 13 this.id = id; 14 } 15 16 public String getName() { 17 return name; 18 } 19 20 public void setName(String name) { 21 this.name = name; 22 } 23 24 public Float getPrice() { 25 return price; 26 } 27 28 public void setPrice(Float price) { 29 this.price = price; 30 } 31 32 public Integer getAccount() { 33 return account; 34 } 35 36 public void setAccount(Integer account) { 37 this.account = account; 38 } 39}
  3. 创建Dao和对应的Mapper

    SaleDao代码如下:

    1public interface SaleDao { 2 3 int insertSale(Sale sale); 4}

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

    SaleDaoMapper.xml如下:

    1<?xml version="1.0" encoding="UTF-8" ?> 2<!DOCTYPE mapper 3 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 4 "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 5 6<mapper namespace="com.javafirst.dao.SaleDao"> 7 8 <insert id="insertSale"> 9 insert into sale(gid,num) values (#{gid},#{num}) 10 </insert> 11 12</mapper>

    GoodsDao代码如下:

    1public interface GoodsDao { 2 3 int updateAccount(Goods goods); 4 5 Goods selectById(Integer gid); 6}

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

    GoodsDaoMapper.xml代码如下:

    1<?xml version="1.0" encoding="UTF-8" ?> 2<!DOCTYPE mapper 3 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 4 "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 5 6<mapper namespace="com.javafirst.dao.GoodsDao"> 7 8 <update id="updateAccount"> 9 update goods set account = account - #{account} where id = #{id} 10 </update> 11 12 <select id="selectById" resultType="com.javafirst.daomain.Goods"> 13 select * from goods where id = #{id} 14 </select> 15</mapper>
  4. MyBatis核心配置

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

    mybatis-config.xml代码如下:

    1<?xml version="1.0" encoding="UTF-8" ?> 2<!DOCTYPE configuration 3 PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 4 "http://mybatis.org/dtd/mybatis-3-config.dtd"> 5<configuration> 6 7 <!-- 日志 --> 8 <settings> 9 <setting name="logImpl" value="STDOUT_LOGGING"/> 10 </settings> 11 12 <typeAliases> 13 <typeAlias type="com.javafirst.daomain.Sale"/> 14 <typeAlias type="com.javafirst.daomain.Goods"/> 15 </typeAliases> 16 17 <mappers> 18 <mapper resource="mapper/SaleDaoMapper.xml"/> 19 <mapper resource="mapper/GoodsDaoMapper.xml"/> 20 </mappers> 21</configuration>
  5. 创建 Service 和其实现类

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

    1public interface BuyGoodsService { 2 3 void buyGoods(Integer gid, Integer num); 4}

    其实现类代码如下:

    1/** 2 * desc: 3 * author: 推荐学java 4 * <p> 5 * weChat: studyingJava 6 */ 7public class BuyGoodsImpl implements BuyGoodsService { 8 9 SaleDao saleDao; 10 GoodsDao goodsDao; 11 12 public void setSaleDao(SaleDao saleDao) { 13 this.saleDao = saleDao; 14 } 15 16 public void setGoodsDao(GoodsDao goodsDao) { 17 this.goodsDao = goodsDao; 18 } 19 20 @Override 21 public void buyGoods(Integer gid, Integer num) { 22 System.out.println("buyGoods方法开始执行了..."); 23 24 // 先插入记录 再减掉库存 25 Sale sale = new Sale(); 26 sale.setGid(gid); 27 sale.setNum(num); 28 29 saleDao.insertSale(sale); 30 31 // 减掉库存之前 先判断库存数是否大于等于购买数量,避免更新后出现负库存 32 Goods goods = goodsDao.selectById(gid); 33 34 if (null == goods || gid > 1002 || gid < 1001) { 35 throw new NullPointerException("商品不存在"); 36 } 37 if (goods.getAccount() < num) { 38 throw new IndexOutOfBoundsException("库存不足"); 39 } 40 41 // 更新库存 42 Goods buyGoods = new Goods(); 43 buyGoods.setId(gid); 44 buyGoods.setAccount(num); 45 46 goodsDao.updateAccount(buyGoods); 47 } 48 49}

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

  6. Spring核心配置文件

    applicationContext.xml代码如下:

    1<?xml version="1.0" encoding="UTF-8"?> 2<beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:context="http://www.springframework.org/schema/context" 5 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"> 6 7 <!-- 使用属性文件 配置数据源中数据库链接信息 --> 8 <context:property-placeholder location="jdbc.properties"/> 9 10 <!-- 声明数据源--> 11 <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> 12 <property name="url" value="${jdbc.url}"/> 13 <property name="username" value="${jdbc.username}"/> 14 <property name="password" value="${jdbc.password}"/> 15 </bean> 16 17 <!-- 声明 SQLSessionFactoryBean --> 18 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> 19 <property name="dataSource" ref="myDataSource"/> 20 <property name="configLocation" value="classpath:mybatis-config.xml"/> 21 </bean> 22 23 <!-- 声明 MapperScannerConfigurer --> 24 <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> 25 <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> 26 27 <property name="basePackage" value="com.javafirst.dao"/> 28 </bean> 29 30 <!-- 注册自定义Service --> 31 <bean id="buyGoods" class="com.javafirst.service.impl.BuyGoodsImpl"> 32 <property name="saleDao" ref="saleDao"/> 33 <property name="goodsDao" ref="goodsDao"/> 34 </bean> 35</beans>

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

  7. 测试

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

    1@Test 2public void spring_transaction() { 3 String config = "applicationContext.xml"; 4 ApplicationContext context = new ClassPathXmlApplicationContext(config); 5 6 BuyGoodsService buyGoods = (BuyGoodsService) context.getBean("buyGoods"); 7 buyGoods.buyGoods(1001, 10); 8 9 System.out.println("测试方法执行完成了!"); 10}

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

使用 Transactional 注解添加事务

使用步骤:

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

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

1<!-- 声明事务管理器 --> 2<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> 3 <!-- 给事务管理器指定 要管理哪里的数据源 --> 4 <property name="dataSource" ref="myDataSource"/> 5</bean> 6 7<!-- 声明事务管理器驱动 --> 8<tx:annotation-driven transaction-manager="transactionManager"/>

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

1@Transactional( 2 propagation = Propagation.REQUIRED, // REQUIRED默认值 3 isolation = Isolation.DEFAULT, // 一般选择默认选项 4 timeout = 12, // 超时,单位秒,默认-1 5 readOnly = false, 6 // 返回异常类的class数组 在该数组中声明的异常,程序都会回滚,不在该数组中声明的运行时异常也都会回滚 7 rollbackFor = {NullPointerException.class, IndexOutOfBoundsException.class} 8) 9@Override 10public void buyGoods(Integer gid, Integer num) { 11 System.out.println("buyGoods方法开始执行了..."); 12 13 // 先插入记录 再减掉库存 14 Sale sale = new Sale(); 15 sale.setGid(gid); 16 sale.setNum(num); 17 18 saleDao.insertSale(sale); 19 20 // 减掉库存之前 先判断库存数是否大于等于购买数量,避免更新后出现负库存 21 Goods goods = goodsDao.selectById(gid); 22 23 if (null == goods) { 24 throw new NullPointerException("商品不存在"); 25 } 26 if (goods.getAccount() < num) { 27 throw new IndexOutOfBoundsException("库存不足"); 28 } 29 30 // 更新库存 31 Goods buyGoods = new Goods(); 32 buyGoods.setId(gid); 33 buyGoods.setAccount(num); 34 35 goodsDao.updateAccount(buyGoods); 36}

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

使用 AspectJ 框架声明事务控制

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

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

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

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

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

1<!-- AspectJ aop实现注解 --> 2<dependency> 3 <groupId>org.springframework</groupId> 4 <artifactId>spring-aspects</artifactId> 5 <version>5.3.14</version> 6</dependency>

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

1<!-- 声明事务管理器 --> 2<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> 3 <!-- 给事务管理器指定 要管理哪里的数据源 --> 4 <property name="dataSource" ref="myDataSource"/> 5</bean> 6 7<!-- 通过使用AspectJ框架AOP方式实现 Spring事务配置管理 --> 8<tx:advice id="buyServiceAdvice" transaction-manager="transactionManager"> 9 <tx:attributes> 10 <tx:method name="buyGoods" propagation="REQUIRED" isolation="DEFAULT" 11 rollback-for="java.lang.NullPointerException,java.lang.IndexOutOfBoundsException"/> 12 13 <!-- 有多个方法需要添加事务 可以在这里意义列举出来, 14 也可以把方法的命名起的有规则,然后通过通配符*来批量添加 15 16 通配符举例:add*、*(直接用一个*表示的意思是除了这里添加的方法外,应用于剩下的其他方法) 17 --> 18 </tx:attributes> 19</tx:advice> 20 21<!-- 切入点表达式,说明事务管理器是在哪个包下的哪个类需要使用--> 22<aop:config> 23 <!-- 24 id:切入点ID,唯一值 25 expression:切入点表达式,指定要执行的方法 26 --> 27 <aop:pointcut id="servicePointcut" expression="execution(* *..service..*.*(..))"/> 28 <!-- 将切入点表达式和声明的事务管理器关联起来 --> 29 <aop:advisor advice-ref="buyServiceAdvice" pointcut-ref="servicePointcut"/> 30</aop:config>

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

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

两种方式对比

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

总结

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

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

学编程,推荐首选Java语言,小编创建了一个专注Java的原创公众号推荐学java,各位可以在微信搜索javaFirst 关注,一起开启Java旅途!

qrcode_for_javafirst

点赞
收藏

评论区

加载中...

相关推荐

spring事务的这10种坑,你稍不注意可能就会踩中!!!

关注“苏三说技术”,回复:开发手册、时间管理有惊喜。对于从事java开发工作的同学来说,spring的事务肯定再熟悉不过了。在某些业务场景下,如果同时有多张表的写入操作,为了保证操作的原子性(要么同时成功,要么同时失败)避免数据不一致的情况,我们一般都会使用spring事务。没错,spring事务大多数情况下,可以满足我们的业务需求。

golang实现MySQL数据库事物的提交与回滚

MySQL事务主要用于处理操作量大,复杂度高的数据。在MySQL中只有使用了Innodb数据库引擎的数据库或表才支持事务。事务用来管理insert,update,delete语句,事务处理可以用来维护数据库的完整性,保证成批的SQL语句要么全部执行,要么全部不执行。一般来说,事务是必须满足4个条件(ACID)::原子性(Atomicit

spring事物管理

Spring提供了一流的事务管理。在Spring中可以支持声明式事务和编程式事务spring事物的概念和重要性事物的属性事物管理器声明事物1.1定义:事务管理在应用程序中起着至关重要的作用:它是一系列任务的组成工作单元,在这个工作单元中,所有的任务必须同时执行。它们只有二种可能执行结果,要么所有任务全

MySql学习17

一.数据库事务的四大特性(ACID)如果一个数据库声称支持事务的操作,那么该数据库必须要具备以下四个特性:原子性(Atomicity):原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,这和前面两篇博客介绍事务的功能是一样的概念,因此事务的操作如果成功就必须要完全应用到数据库,如果操

MySql学习18

本篇讲述数据库中非常重要的事务概念和如何使用MySQL命令行窗口来进行数据库的事务操作。下一篇会讲述如何使用JDBC进行数据库的事务操作。  事务是指数据库中的一组逻辑操作,这个操作的特点就是在该组逻辑中,所有的操作要么全部成功,要么全部失败。在各个数据具有特别紧密的联系时,最好是使用数据库的事务来完成逻辑处理。  例如路人甲A给路人甲B转账

MySQL常见问题

事务四大特性原子性:不可分割的操作单元,事务中所有操作,要么全部成功;要么撤回到执行事务之前的状态一致性:如果在执行事务之前数据库是一致的,那么在执行事务之后数据库也还是一致的;隔离性:事务操作之间彼此独立和透明互不影响。事务独立运行。这通常使用锁来实现。一个事务处理后的结果,影响了其他事务,那么其他事务会撤回。