最是人间留不住,朱颜辞镜花辞树。这篇文章主要讲述推荐学java——Spring事务相关的知识,希望能为你提供帮助。
前情回顾
已经学习了Spring基础入门知识 和Spring AOP知识,在[上一节内容]()中我们还将Spring
和 MyBatis
结合起来使用,熟悉开发模式。这节学习 Spring
中的事务,同样是重要内容。
事务概念
其实和我们前面学习 mysql 时,了解到的事务是同一概念,指的是一组或多条SQL语句的执行结果要么全部成功,要么全部失败,不会有其他结果,这就叫事务。事务的出现也是为了很好的解决现实生活中的问题。
Spring 事务管理器
使用 Spring 的事务管理器,管理不同数据库访问技术的事务处理。开发者只需要掌握 Spring事务处理这一个方案,就可以实现使用不用数据库访问技术的事务管理。
事务管理面向的是Spring,由Spring管理事务,做事务提交,事务回滚。
事务管理器接口:PlatformTransactionManager
,其有很多实现类,基本上不同数据库访问技术都有对应的实现类,我们要学习的是 DataSourceTransactionManager
,该实现类适合 jdbc 方式和 MyBatis 方式。
Spring事务定义接口
接口 TransactionDefinition
定义了三类常量,说明有关事务控制的属性。
事务属性:
- 隔离级别
- 传播行为
- 事务的超时
隔离级别:控制不同事务之间的影响程度。和学习MySQL中的事务隔离是一个意思。
5个值,只有四个隔离级别:
- DEFAULT:采用数据库默认的事务隔离级别,MySQL默认
REPEATABLE_READ
,Oracle默认READ_UNCOMMITTED
- READ_UNCOMMITTED:读未提交,未解决任何并发问题。
- READ_COMMITTED:读已提交,解决脏读,存在不可重复读与幻读
- REPEATABLE_READ:可重复读,解决了脏读、不可重复读,存在幻读
- SERIALIZABLE:串行化,不存在并发问题。
- PROPAGATION_REQUIRED:Spring默认传播行为,调用方法是,如果有事务则使用当前事务,如果没有事务,则会新建事务,方法在新建的事务中执行。
- PROPAGATION_SUPPORTS:支持,方法有无事务都可正常执行。
- PROPAGATION_SUPPORTS_NEW:新建,在调用方法时如果存在事务,则先暂停,直到新建事务执行结束;如果不存在事务,还是新建事务执行方法。
- PROPAGATION_MANDATORY:
- PROPAGATION_NESTED:
- PROPAGATION_NEVER:
- PROPAGATION_NOT_SUPPORTED:
Spring添加事务
Spring提供了两种方式添加事务:
- Spring框架自己的注解
@Transactional
- 使用
AspectJ
框架的 AOP 配置
场景需求出售商品简单流程,每当出售一件商品,那么销售记录表应该增加一条该商品的记录,同时,应该更新商品表中该商品的库存,而这两者应该是同时成功,或者同时失败,才是正确的业务逻辑,而要保证这一结果,就需要使用事务。
实现流程分析除了业务分析和添加事务外,其他的流程和上一节内容是相同的,再来温习一下具体步骤:
- 创建数据库、数据表
这里小编还用上节内容中的数据库,表需要新建两张:sale
和goods
.
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
- 创建实体类
这个操作估计各位已经轻车熟路了,废话不多说,上代码:
sale实体类代码:
public class Sale private Integer id; private Integer gid; private Integer num; public Integer getId() return id; public void setId(Integer id) this.id = id; public Integer getGid() return gid; public void setGid(Integer gid) this.gid = gid; public Integer getNum() return num; public void setNum(Integer num) this.num = num;
goods实体类代码:
public class Goods private Integer id; private String name; private Float price; private Integer account; public Integer getId() return id; public void setId(Integer id) this.id = id; public String getName() return name; public void setName(String name) this.name = name; public Float getPrice() return price; public void setPrice(Float price) this.price = price; public Integer getAccount() return account; public void setAccount(Integer account) this.account = account;
- 创建Dao和对应的Mapper
SaleDao代码如下:
public interface SaleDao int insertSale(Sale sale);
这里是新增销售记录,所以只需要一个添加记录的接口即可。
SaleDaoMapper.xml
如下:
< ?xml version="1.0" encoding="UTF-8" ?> < !DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> < mapper namespace="com.javafirst.dao.SaleDao"> < insert id="insertSale"> insert into sale(gid,num) values (#gid,#num) < /insert> < /mapper>
GoodsDao代码如下:
public interface GoodsDao int updateAccount(Goods goods); Goods selectById(Integer gid);
提供两个方法,一个是我们要更新库存使用,另一个我们需要根据给定的ID查询商品库存,避免更新后出现库存为负的情况。
GoodsDaoMapper.xml
代码如下:
< ?xml version="1.0" encoding="UTF-8" ?> < !DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> < mapper namespace="com.javafirst.dao.GoodsDao"> < update id="updateAccount"> update goods set account = account - #account where id = #id < /update> < select id="selectById" resultType="com.javafirst.daomain.Goods"> select * from goods where id = #id < /select> < /mapper>
- MyBatis核心配置
这都是老规矩了,上代码:
mybatis-config.xml
代码如下:
< ?xml version="1.0" encoding="UTF-8" ?> < !DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> < configuration> < !--日志--> < settings> < setting name="logImpl" value="https://www.songbingjia.com/android/STDOUT_LOGGING"/> < /settings> < typeAliases> < typeAlias type="com.javafirst.daomain.Sale"/> < typeAlias type="com.javafirst.daomain.Goods"/> < /typeAliases> < mappers> < mapper resource="mapper/SaleDaoMapper.xml"/> < mapper resource="mapper/GoodsDaoMapper.xml"/> < /mappers> < /configuration>
- 创建 Service 和其实现类
其实这里才是我们的真正业务逻辑处理,前面的都是支撑,这个整体流程需要多些案例,慢慢感受。
public interface BuyGoodsService void buyGoods(Integer gid, Integer num);
其实现类代码如下:
/** * desc: * author: 推荐学java * < p> * weChat: studyingJava */ public class BuyGoodsImpl implements BuyGoodsService SaleDao saleDao; GoodsDao goodsDao; public void setSaleDao(SaleDao saleDao) this.saleDao = saleDao; public void setGoodsDao(GoodsDao goodsDao) this.goodsDao = goodsDao; @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 || gid > 1002 || gid < 1001) throw new NullPointerException("商品不存在"); if (goods.getAccount() < num) throw new IndexOutOfBoundsException("库存不足"); // 更新库存 Goods buyGoods = new Goods(); buyGoods.setId(gid); buyGoods.setAccount(num); goodsDao.updateAccount(buyGoods);
这里的逻辑是能跑通整个流程就行,还没有加入事务前的逻辑。
- Spring核心配置文件
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 https://www.springframework.org/schema/context/spring-context.xsd"> < !--使用属性文件 配置数据源中数据库链接信息--> < context:property-placeholder location="jdbc.properties"/> < !-- 声明数据源--> < bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> < property name="url" value="https://www.songbingjia.com/android/$jdbc.url"/> < property name="username" value="https://www.songbingjia.com/android/$jdbc.username"/> < property name="password" value="https://www.songbingjia.com/android/$jdbc.password"/> < /bean> < !--声明 SQLSessionFactoryBean--> < bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> < property name="dataSource" ref="myDataSource"/> < property name="configLocation" value="https://www.songbingjia.com/android/classpath:mybatis-config.xml"/> < /bean> < !--声明 MapperScannerConfigurer--> < bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> < property name="sqlSessionFactoryBeanName" value="https://www.songbingjia.com/android/sqlSessionFactory"/> < property name="basePackage" value="https://www.songbingjia.com/android/com.javafirst.dao"/> < /bean> < !--注册自定义Service--> < bean id="buyGoods" class="com.javafirst.service.impl.BuyGoodsImpl"> < property name="saleDao" ref="saleDao"/> < property name="goodsDao" ref="goodsDao"/> < /bean> < /beans>
上节内容提到过,这里的基本都是流程性的,我们只需要注册自己业务相关的Service
就行。
- 测试
这都是固定代码了,贴一下:
@Test public void spring_transaction() String config = "applicationContext.xml"; ApplicationContext context = new ClassPathXmlApplicationContext(config); BuyGoodsService buyGoods = (BuyGoodsService) context.getBean("buyGoods"); buyGoods.buyGoods(1001, 10); System.out.println("测试方法执行完成了!");
结果大家可以通过改变参数验证结果,通过查看两张表中的数据变更来对比存在的问题,解决方案是通过Spring事务来控制整体流程的最终结果,下面就来学习事务的使用。
- 在Spring配置文件中声明事务的内容(声明事务管理器,指定使用的哪个事务管理器对象;声明使用哪个注解管理事务,开启事务注解驱动)
- 在类的源代码中加入
@Transactional
注解(可添加在类上面,有可添加在方法上面,推荐后者)
<
!--声明事务管理器-->
<
bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<
!--给事务管理器指定 要管理哪里的数据源-->
<
property name="dataSource" ref="myDataSource"/>
<
/bean>
<
!--声明事务管理器驱动-->
<
tx:annotation-driven transaction-manager="transactionManager"/>
然后在业务代码方法上面添加注解:
@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 声明事务控制叫做声明式事务。使用步骤如下:- 在
pom.xml
中加入spring-aspects
依赖; - 在 spring 的配置文件中声明事务的内容(声明事务管理器,业务方法需要的事务属性,声明切入点表达式)
pom.xml
文件添加如下代码:<
!--AspectJ aop实现注解-->
<
dependency>
<
groupId>
org.springframework<
/groupId>
<
artifactId>
spring-aspects<
/artifactId>
<
version>
5.3.14<
/version>
<
/dependency>
applicationContext.xml
文件添加如下代码:<
!--声明事务管理器-->
<
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层和业务层以及简单的配置
推荐阅读
- docker-compose安装graylog
- 八大排序算图解汇总
- 玩转自动化运维全流程
- 别再用YYYY-MM-dd了,不然就卷铺盖走人
- 我们公司是如何做到高效并行测试的()
- Nginx之location模块说明
- 浅析 Web3.0 DApp(去中心化应用程序)设计架构
- GitHub注册-创建数据库-本地项目推送GitHub远程数据库-(入门级教程)
- Kubernetes部署