草庐IT

day15-声明式事务

liyuelian 2023-04-16 原文

声明式事务

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()方法。

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

  1. ruby-on-rails - active_admin 目录中的常量警告重新声明 - 2

    我正在使用active_admin,我在Rails3应用程序的应用程序中有一个目录管理,其中包含模型和页面的声明。时不时地我也有一个类,当那个类有一个常量时,就像这样:classFooBAR="bar"end然后,我在每个必须在我的Rails应用程序中重新加载一些代码的请求中收到此警告:/Users/pupeno/helloworld/app/admin/billing.rb:12:warning:alreadyinitializedconstantBAR知道发生了什么以及如何避免这些警告吗? 最佳答案 在纯Ruby中:classA

  2. ruby - 我可以使用 aws-sdk-ruby 在 AWS S3 上使用事务性文件删除/上传吗? - 2

    我发现ActiveRecord::Base.transaction在复杂方法中非常有效。我想知道是否可以在如下事务中从AWSS3上传/删除文件:S3Object.transactiondo#writeintofiles#raiseanexceptionend引发异常后,每个操作都应在S3上回滚。S3Object这可能吗?? 最佳答案 虽然S3API具有批量删除功能,但它不支持事务,因为每个删除操作都可以独立于其他操作成功/失败。该API不提供任何批量上传功能(通过PUT或POST),因此每个上传操作都是通过一个独立的API调用完成的

  3. ruby - 分布式事务和队列,ruby,erlang,scala - 2

    我有一个涉及多台机器、消息队列和事务的问题。因此,例如用户点击网页,点击将消息发送到另一台机器,该机器将付款添加到用户的帐户。每秒可能有数千次点击。事务的所有方面都应该是容错的。我以前从未遇到过这样的事情,但一些阅读表明这是一个众所周知的问题。所以我的问题。我假设安全的方法是使用两阶段提交,但协议(protocol)是阻塞的,所以我不会获得所需的性能,我是否正确?我通常写Ruby,但似乎Redis之类的数据库和Rescue、RabbitMQ等消息队列系统对我的帮助不大——即使我实现某种两阶段提交,如果Redis崩溃,数据也会丢失,因为它本质上只是内存。所有这些让我开始关注erlang和

  4. ruby - 如何使用 ruby​​ mysql2 执行事务 - 2

    我已经开始使用mysql2gem。我试图弄清楚一些基本的事情——其中之一是如何明确地执行事务(对于批处理操作,比如多个INSERT/UPDATE查询)。在旧的ruby-mysql中,这是我的方法:client=Mysql.real_connect(...)inserts=["INSERTINTO...","UPDATE..WHEREid=..",#etc]client.autocommit(false)inserts.eachdo|ins|beginclient.query(ins)rescue#handleerrorsorabortentirelyendendclient.commi

  5. ruby - 如何使用 method_missing 动态声明方法? - 2

    我有一个ruby​​程序,我想接受用户创建的方法,并使用该名称创建一个新方法。我试过这个:defmethod_missing(meth,*args,&block)name=meth.to_sclass我收到以下错误:`define_method':interningemptystring(ArgumentError)in'method_missing'有什么想法吗?谢谢。编辑:我以不同的方式让它工作,但我仍然很好奇如何以这种方式做到这一点。这是我的代码:defmethod_missing(meth,*args,&block)Adder.class_evaldodefine_method

  6. ruby-on-rails - 在 rails 中提交后回滚事务 - 2

    保存成功后可以回滚吗?让我有一个带有属性名称、电子邮件等的用户模型。例如u=User.newu.name="test_name"u.email="test@email.com"u.save现在记录将成功保存在数据库中,之后我想回滚我的事务(不是销毁或删除)。有什么想法吗? 最佳答案 您可以通过交易来做到这一点,请参阅http://markdaggett.com/blog/2011/12/01/transactions-in-rails/例子:User.transactiondoUser.create(:username=>'Nemu

  7. ruby-on-rails - rails : Find tasks that were created on a certain day? - 2

    我有一个任务列表(名称、starts_at),我试图在每日View中显示它们(就像iCal)。deftodays_tasks(day)Task.find(:all,:conditions=>["starts_atbetween?and?",day.beginning,day.ending]end我不知道如何将Time.now(例如“2009-04-1210:00:00”)动态转换为一天的开始(和结束),以便进行比较。 最佳答案 deftodays_tasks(now=Time.now)Task.find(:all,:conditio

  8. 什么是0day漏洞?如何预防0day攻击? - 2

    什么是0day漏洞?0day漏洞,是指已经被发现,但是还未被公开,同时官方还没有相关补丁的漏洞;通俗的讲,就是除了黑客,没人知道他的存在,其往往具有很大的突发性、破坏性、致命性。0day漏洞之所以称为0day,正是因为其补丁永远晚于攻击。所以攻击者利用0day漏洞攻击的成功率极高,往往可以达到目的并全身而退,而防守方却一无所知,只有在漏洞公布之后,才后知后觉,却为时已晚。“后知后觉、反应迟钝”就是当前安全防护面对0day攻击的真实写照!为了方便大家理解,中科三方为大家梳理当前安全防护模式下,一个漏洞从发现到解决的三个时间节点:T0:此时漏洞即0day漏洞,是已经被发现,还未被公开,官方还没有相

  9. ruby - Rails 比较 date.end_of_day.to_datetime 和 date.to_datetime.end_of_day 返回的日期对象值时返回 false - 2

    ruby1.9.3dev(2011-09-23修订版33323)[i686-linux]轨道3.0.20最近为什么在与DateTimeonRails相关的RSpecs项目上工作我发现在给定日期以下语句发出的值date.end_of_day.to_datetime和date.to_datetime.end_of_day虽然它们表示相同的日期时间,但比较时返回false。为了确认这一点,我打开了Rails控制台并尝试了以下操作1.9.3dev:053>monday=Time.now.monday=>2013-02-2500:00:00+05301.9.3dev:054>monday.cla

  10. ruby - 如何使用 PG Ruby Gem 有条件地回滚事务 - 2

    我目前正在上一门数据库类(class),其中一个实验室问题让我困惑于如何实现上述内容,事实上,如果可能的话。我试过搜索docs但是定义的交易方式比较模糊。这是我第一次尝试在没有Rails的情况下进行任何数据库操作,所以我有点迷茫。我已经成功地创建了一个到我的postgresql数据库的连接并且可以执行语句,我需要做的最后一件事是根据一个简单的条件回滚一个事务。请允许我向您展示代码:require'pg'@conn=PG::Connection.open(:dbname=>'db_15_11_labs')@conn.prepare('insert','INSERTINTOhouse(ho

随机推荐