2

day15-声明式事务 - 一刀一个小西瓜

 1 year ago
source link: https://www.cnblogs.com/liyuelian/p/17081141.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

声明式事务

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声明式事务使用-代码实现

-- 演示声明式事务创建的表-- 用户表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);
image-20230131192300386
image-20230131192318552

image-20230131192401446

  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语句中添加错误符号
image-20230131210013516
@Testpublic void buyGoodsTest() { ApplicationContext ioc = new ClassPathXmlApplicationContext("tx.xml"); GoodsService goodsService = ioc.getBean(GoodsService.class); goodsService.buyGoods(1,1,10);}

测试结果:出现异常

image-20230131210119244

原始表信息:

image-20230131192300386

image-20230131192401446

当前表信息:

image-20230131210212596
image-20230131210234150

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

  1. 改进GoodsService的业务方法,使用声明式事务:
/** * 1.使用注解 @Transactional 可以进行声明式事务控制 * 2.该注解会将标识方法中,对数据库的操作 作为一个事务来管理 * 3.@Transactional 底层是使用的仍然是AOP机制 * 4.底层是使用动态代理对象来调用 buyGoodsByTx()方法 * 5.在执行 buyGoodsByTx()方法前,先调用事务管理器的 doBegin()方法,再调用目标方法 * 如果执行没有发生异常,就调用事务管理器 doCommit()方法,否则调用 doRollback()方法 * @param userId * @param goodsId * @param amount */@Transactionalpublic 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结尾的

image-20230131211851101
@Testpublic void buyGoodsTestByTx() { ApplicationContext ioc = new ClassPathXmlApplicationContext("tx.xml"); GoodsService goodsService = ioc.getBean(GoodsService.class); goodsService.buyGoodsByTx(1,1,10);}

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

image-20230131212146965

测试前数据:

image-20230131210602751

image-20230131210619874

测试后数据:

image-20230131212215971

image-20230131212236499

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

2.4声明式事务机制-Debug

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

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

image-20230131213707516

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


debug-1-异常情况

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

    image-20230131215444956
  2. debug测试方法:buyGoodsTestByTx()

    @Testpublic 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,即此时事务默认自动提交:

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

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

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

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

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

    image-20230131222736805

debug-2-正常的流程

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

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

image-20230131223839605

总结:

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

3.事务的传播机制

事务的传播机制说明:

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

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

    image-20230131224401793

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

  3. 问题在于:里层方法的事务是被外层方法事务管理?还是它本身作为一个独立的事务呢?这就涉及到事务的传播机制问题。

3.1事务传播机制种类

事务传播的属性 / 种类:

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

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

其他的不常用


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK