声明式事务

1.事务分类

  1. 编程式事务
Connection connection = JdbcUtils.getConnection();
try{
//1.先设置事务不要提交
connection.setAutoCommit(false);
//2.进行业务 crud
//3.提交事务
connection.commit();
}catch(Exception e){
//4.出现异常,回滚
connection.rollback();
}
  1. 声明式事务(后面以一个购买商品的系统为例)

2.声明式事务-使用实例

2.1需求说明

  • 需求说明 - 用户购买商品

    去处理用户购买商品的业务逻辑:当一个用户去购买商品,应该包含三个步骤:

    1. 通过商品 id 获取价格
    2. 购买商品(某人购买商品,修改用户余额)
    3. 修改库存量

    这里一共涉及到三张表:用户表、商品表、商品存量表。显然,应该使用事务处理。

2.2解决方案分析

方案一:使用传统的编程式事务来处理,将代码写到一起

(缺点是:代码冗余,效率低,不利于拓展;优点是简单,好理解)

//例如:
Connection connection = JdbcUtils.getConnection();
try{
//1.先设置事务不要提交
connection.setAutoCommit(false); //2.进行业务 crud
//多个表的修改,添加,删除
//select form 商品表 => 获取价格
//修改用户余额 update...
//修改商品库存量 update... //3.提交事务
connection.commit();
}catch(Exception e){
//4.出现异常,回滚
connection.rollback();
}

方案二:使用 Spring 的声明式事务来处理,可以将上面三个子步骤分别写成一个方法,然后统一管理。

(这是Spring的优越性所在,开发中使用很多,优点是无代码冗余,效率高,拓展方便,缺点是理解较困难)底层使用AOP(动态代理+动态绑定+反射+注解)

2.3声明式事务使用-代码实现

  1. 创建表
-- 演示声明式事务创建的表
-- 用户表
CREATE TABLE `user_account`(
user_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(32) NOT NULL DEFAULT '',
money DOUBLE NOT NULL DEFAULT 0.0
)CHARSET=utf8; INSERT INTO `user_account` VALUES(NULL,'张三', 1000);
INSERT INTO `user_account` VALUES(NULL,'李四', 2000); -- 商品表
CREATE TABLE `goods`(
goods_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
goods_name VARCHAR(32) NOT NULL DEFAULT '',
price DOUBLE NOT NULL DEFAULT 0.0
)CHARSET=utf8 ; INSERT INTO `goods` VALUES(NULL,'小风扇', 10.00);
INSERT INTO `goods` VALUES(NULL,'小台灯', 12.00);
INSERT INTO `goods` VALUES(NULL,'可口可乐', 3.00); -- 商品存量表
CREATE TABLE `goods_amount`(
goods_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
goods_num INT UNSIGNED DEFAULT 0
)CHARSET=utf8 ; INSERT INTO `goods_amount` VALUES(1,200);
INSERT INTO `goods_amount` VALUES(2,20);
INSERT INTO `goods_amount` VALUES(3,15);

  1. 创建GoodsDao
package com.li.tx.dao;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository; import javax.annotation.Resource; /**
* @author 李
* @version 1.0
*/
@Repository //将GoodsDao对象 注入到 spring 容器
public class GoodsDao {
@Resource
private JdbcTemplate jdbcTemplate; /**
* 根据商品id,查询对应的商品价格
* @param id
* @return
*/
public Float queryPriceById(Integer id) {
String sql = "select price from goods where goods_id = ?";
Float price = jdbcTemplate.queryForObject(sql, Float.class, id);
return price;
} /**
* 修改用户余额 [减少用户余额]
* @param user_id
* @param money
*/
public void updateBalance(Integer user_id, Float money) {
String sql = "update user_account set money=money-? where user_id=? ";
jdbcTemplate.update(sql, money, user_id);
} /**
* 修改商品库存量
* @param goods_id
* @param amount
*/
public void updateAmount(Integer goods_id, int amount) {
String sql = "update goods_amount set goods_num=goods_num-? where goods_id=? ";
jdbcTemplate.update(sql, amount, goods_id);
}
}
  1. 配置容器文件

因为使用了注解 @Resource 的方式自动装配 JdbcTemplate 对象,这里需要配置该对象。

<!--配置要扫描的包-->
<context:component-scan base-package="com.li.tx"/> <!--引入外部的属性文件-->
<context:property-placeholder location="classpath:jdbc.properties"/> <!--配置数据源对象-DataSource-->
<bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSource">
<!--给数据源对象配置属性值-->
<property name="user" value="${jdbc.user}"/>
<property name="password" value="${jdbc.pwd}"/>
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
</bean> <!--配置JdbcTemplate对象-->
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
<!--给JdbcTemplate对象配置DataSource属性-->
<property name="dataSource" ref="dataSource"/>
</bean>
  1. 创建GoodsService,编写方法,验证不使用事务就会出现数据不一致现象
package com.li.tx.service;

import com.li.tx.dao.GoodsDao;
import org.springframework.stereotype.Service; import javax.annotation.Resource; /**
* @author 李
* @version 1.0
*/
@Service //将GoodsService对象注入到容器中
public class GoodsService {
@Resource
private GoodsDao goodsDao; /**
* 编写一个方法,完成用户购买商品的业务
*
* @param userId 用户 id
* @param goodsId 商品 id
* @param amount 购买的商品数量
*/
public void buyGoods(int userId, int goodsId, int amount) {
//输出购买的相关信息
System.out.println("用户购买信息 userId=" + userId
+ " goodsId=" + goodsId + " 购买数量=" + amount); //1.得到商品价格
Float price = goodsDao.queryPriceById(goodsId);
//2.减少用户余额
goodsDao.updateBalance(userId, price * amount);
//3.减少商品库存量
goodsDao.updateAmount(goodsId, amount); System.out.println("用户购买成功...");
}
}
  1. 新增添扫描的包
<context:component-scan base-package="com.li.tx.service"/>
  1. 为了测试,故意在Dao的sql语句中添加错误符号

测试:

@Test
public void buyGoodsTest() {
ApplicationContext ioc =
new ClassPathXmlApplicationContext("tx.xml");
GoodsService goodsService = ioc.getBean(GoodsService.class);
goodsService.buyGoods(1,1,10);
}

测试结果:出现异常

原始表信息:

当前表信息:

可以看到用户表的余额减少了,但是商品库存表的库存没有改变。这就产生了数据不一致问题,因此要使用事务。

  1. 改进GoodsService的业务方法,使用声明式事务:
/**
* 1.使用注解 @Transactional 可以进行声明式事务控制
* 2.该注解会将标识方法中,对数据库的操作 作为一个事务来管理
* 3.@Transactional 底层是使用的仍然是AOP机制
* 4.底层是使用动态代理对象来调用 buyGoodsByTx()方法
* 5.在执行 buyGoodsByTx()方法前,先调用事务管理器的 doBegin()方法,再调用目标方法
* 如果执行没有发生异常,就调用事务管理器 doCommit()方法,否则调用 doRollback()方法
* @param userId
* @param goodsId
* @param amount
*/
@Transactional
public void buyGoodsByTx(int userId, int goodsId, int amount) {
//输出购买的相关信息
System.out.println("用户购买信息 userId=" + userId
+ " goodsId=" + goodsId + " 购买数量=" + amount); //1.得到商品价格
Float price = goodsDao.queryPriceById(goodsId);
//2.减少用户余额
goodsDao.updateBalance(userId, price * amount);
//3.减少商品库存量
goodsDao.updateAmount(goodsId, amount); System.out.println("用户购买成功...");
}
  1. 之前的基础上,在容器文件中配置事务管理器,并启用基于注解的声明式事务管理功能
<!--配置事务管理器-对象
1.DataSourceTransactionManager 这个对象是进行事务管理的
2.一定要配置数据源属性,即指定该事务管理器 是对哪个数据源进行事务控制
-->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
id="transactionManager">
<property name="dataSource" ref="dataSource"/>
</bean> <!--配置:启用基于注解的声明式事务管理功能-->
<tx:annotation-driven transaction-manager="transactionManager"/>

注意:这里的 annotation-driven 标签要选择以tx结尾的

  1. 再次测试
@Test
public void buyGoodsTestByTx() {
ApplicationContext ioc =
new ClassPathXmlApplicationContext("tx.xml");
GoodsService goodsService = ioc.getBean(GoodsService.class);
goodsService.buyGoodsByTx(1,1,10);
}

测试结果:可以看到仍然出现异常(因为之前在sql语句中故意添加了错误字符)

测试前数据:

测试后数据:

表数据在测试前后数据一致。这说明事务控制起作用了,在出现异常时进行了回滚,因此数据没有被改变。

2.4声明式事务机制-Debug

在整个声明式事务中,DataSourceTransactionManager类尤为重要。

我们可以看到在 DataSourceTransactionManager 的源码中,有一个 DataSource 属性,即数据源对象。因为连接是在 DataSource 中获取,而事务管理器通过连接才能进行事务管理。

此外,DataSourceTransactionManager 还有很多重要的方法:doBegin(),doCommit(),doRollback()等。


debug-1-异常情况

  1. 以 2.3 的代码为例,在doBegin方法旁打上断点。

  2. debug测试方法:buyGoodsTestByTx()

    @Test
    public void buyGoodsTestByTx() {
    ApplicationContext ioc =
    new ClassPathXmlApplicationContext("tx.xml");
    GoodsService goodsService = ioc.getBean(GoodsService.class);
    goodsService.buyGoodsByTx(1,1,10);
    }
  3. 光标挑战到之前的断点位置,即doBegin方法中。点击Step Over,当运行到下面的代码时,可以看到con.getAutoCommit() 的值为true,即此时事务默认自动提交:

  4. 继续点击Step Over,当运行了 con.setAutoCommit(false); 后,可以看到 con.getAutoCommit() 的值变成了false,此时事务不再进行自动提交:

  5. 在GoodsService的方法旁添加第二个断点,点击 Resume Program

  6. 光标跳转到第二个断点处,说明程序是先执行了doBegin()方法,再执行的bugGoodsByTx()方法。

  7. 在事务管理器的doRollback方法中打上第三个断点

  8. 继续点击step Over,当bugGoodsByTx()方法执行到 goodsDao.updateAmount(goodsId, amount); 时,光标跳转到了第三个断点处!最终在该方法中,执行了 con.rollback(),进行回滚。

debug-2-正常的流程

修改之前的sql语句,将其变回正确的SQL。在事务管理器的doCommit方法中添加断点,然后点击debug。

光标仍然先进入到doBegin方法中,将自动事务提交修改为false后,又调转到目标方法。这次执行完目标方法后,光标跳转到了doCommit()方法中。在没有出现异常的情况下,执行了事务提交。

总结:

在执行目标方法 buyGoodsByTx() 前,先调用事务管理器的 doBegin() 方法,再调用目标方法。如果执行没有发生异常,就调用事务管理器 doCommit()方法,否则调用 doRollback()方法。

3.事务的传播机制

事务的传播机制说明:

  1. 当有多个事务处理并存时,如何控制?

  2. 比如用户去购买两次商品(使用不同的方法),每个方法都是一个事务,那么如何控制呢?

    也就是说,某个方法本身是一个事务,然后该方法中又调用了其他一些方法,这些方法也是被@Transactional 修饰的,也是一个一个的事务。

  3. 问题在于里层方法的事务是被外层方法事务管理?还是它本身作为一个独立的事务呢?

    这就涉及到事务的传播机制问题。

3.1事务传播机制种类

事务传播的属性 / 种类:

传播属性 说明
REQUIRED (默认)如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并且在自己的事务内运行
REQUIRES_NEW 当前的方法必须启动新事务,并在它自己的事务内运行,如果有事务正在运行,应该将它挂起
SUPPORTS 如果有事务在运行,当前的方法就在这个事务内运行,否则它可以不运行在事务中
NOT_SUPPORTED 当前的方法不应该运行在事务中,如果有运行的事务,将它挂起
MANDATORY 当前的方法必须运行在事务内部,如果没有正在运行的事务,就抛出异常
NEVER 当前的方法不应该运行在事务中,如果有运行的事务,就抛出异常
NESTED 如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则,就启动一个新的事务,并在它自己的事务内运行

常用的就是前面两种:(1)REQUIRED,(2)REQUIRES_NEWREQUIRES_NEW

其他的不常用

day15-声明式事务的更多相关文章

  1. spring声明式事务管理总结

    事务配置 首先在/WEB-INF/applicationContext.xml添加以下内容: <!-- 配置事务管理器 --> <bean id="transactionM ...

  2. Spring声明式事务管理

    一.Spring 的声明式事务管理概述 1.Spring 的声明式事务管理在底层是建立在 AOP 的基础之上的.其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法 ...

  3. spring笔记--事务管理之声明式事务

    事务简介: 事务管理是企业级应用开发中必不可少的技术,主要用来确保数据的完整性和一致性, 事务:就是一系列动作,它们被当作一个独立的工作单元,这些动作要么全部完成,要么全部不起作用. Spring中使 ...

  4. 全面分析 Spring 的编程式事务管理及声明式事务管理

    开始之前 关于本教程 本教程将深入讲解 Spring 简单而强大的事务管理功能,包括编程式事务和声明式事务.通过对本教程的学习,您将能够理解 Spring 事务管理的本质,并灵活运用之. 先决条件 本 ...

  5. 事务管理(下) 配置spring事务管理的几种方式(声明式事务)

    配置spring事务管理的几种方式(声明式事务) 概要: Spring对编程式事务的支持与EJB有很大的区别.不像EJB和Java事务API(Java Transaction API, JTA)耦合在 ...

  6. spring 声明式事务管理

    简单理解事务: 比如你去ATM机取5000块钱,大体有两个步骤:首先输入密码金额,银行卡扣掉5000元钱:然后ATM出5000元钱.这两个步骤必须是要么都执行要么都不执行.如果银行卡扣除了5000块但 ...

  7. spring aop 声明式事务管理

    一.声明式事务管理的概括 声明式事务(declarative transaction management)是Spring提供的对程序事务管理的方式之一. Spring的声明式事务顾名思义就是采用声明 ...

  8. Spring声明式事务管理基于@Transactional注解

    概述:我们已知道Spring声明式事务管理有两种常用的方式,一种是基于tx/aop命名空间的xml配置文件,另一种则是基于@Transactional 注解.         第一种方式我已在上文为大 ...

  9. Spring声明式事务管理基于tx/aop命名空间

    目的:通过Spring AOP 实现Spring声明式事务管理; Spring支持编程式事务管理和声明式事务管理两种方式. 而声明式事务管理也有两种常用的方式,一种是基于tx/aop命名空间的xml配 ...

  10. 声明式事务-整合Spring、Hibernate

    编程式事务:通过编码的方式,让事务处理的代码侵入到核心的业务代码中. 声明式事务:完成了事务处理的代码和业务核心代码的解耦合.提供事务处理代码的复用性和降低维护成本. 声明式事务:aop最典型的应用. ...

随机推荐

  1. 十六、资源控制器之DaemonSet

    资源控制器之DaemonSet DaemonSet 确保全部(或者一些) Node上运行一个 Pod 的副本,当有 Node 加入集群时,也会为他们新增一个 Pod,当有 Node 从集群移除时,这些 ...

  2. 【k8s】k8s pv、pvc无法删除问题。

    一般删除步骤为:先删除pod再删除pvc最后删除pv 遇到的问题 但是遇到pv使用处于"Terminating"状态,而且删不掉.如下图: 解决办法 直接删除k8s中的记录: ku ...

  3. webpack中 hash chunkhash

    hash一般是结合CDN缓存来使用,通过webpack构建之后,生成对应文件名自动带上对应的MD5值.如果文件内容发生改变的话,那么对应文件hash值也会改变,对应的HTML引用的URL地址也会改变, ...

  4. Graceful Java之try...catch()

    [优美的Java代码之try...catch] 目录 概述 优化 优化前写法(JDK1.7之前) 优化后写法(JDK1.7及以后) 延伸阅读:嵌套的文件流如何正确的关闭 概述 通常我们使用try... ...

  5. Windows版CheatSheet——一键显示当前程序快捷键列表

    Windows系统上的各种软件有太多的快捷键,想要记住是几乎不可能的,推荐一个一键显示当前软件快捷键的软件,在使用其他程序的时候,只要按下Ctrl+`就可以理解弹出该软件的所有快捷键列表,还支持收藏功 ...

  6. Python 学习思路 思维导图 Xmind

    如果需要,请在评论区留下邮箱,我看到后会一次发送.

  7. 《吐血整理》高级系列教程-吃透Fiddler抓包教程(34)-Fiddler如何抓取微信小程序的包-上篇

    1.简介 有些小伙伴或者是童鞋们说小程序抓不到包,该怎么办了???其实苹果手机如果按照宏哥前边的抓取APP包的设置方式设置好了,应该可以轻松就抓到包了.那么安卓手机小程序就比较困难,不是那么友好了.所 ...

  8. 手把手,完整的从0搭建vite-vue3-ts项目框架:配置less+svg+pinia+vant+axios

    项目同步git:https://gitee.com/lixin_ajax/vue3-vite-ts-pinia-vant-less.git 为避免赘述,过于基础的点会直接省略或贴图,比如创建文件夹/文 ...

  9. 使用vite + vue3 + ant-design-vue + vue-router + vuex 创建一个管理应用

    使用vite + vue3 + ant-design-vue + vue-router + vuex 创建一个管理应用的记录 使用vite 创建项目 我创建的node 版本是 v16.17.1 使用N ...

  10. kubernetes数据持久化PV-PVC详解(一)

    官方文档地址: https://kubernetes.io/zh-cn/docs/concepts/storage/persistent-volumes/ 1. 什么是PV,PVC? 1.1 什么是P ...