声明式事务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 对象,这里需要配置该对象。

                            
  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. 新增添扫描的包
  1. 为了测试,故意在Dao的sql语句中添加错误符号

测试:

@Testpublic 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 */@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. 之前的基础上,在容器文件中配置事务管理器,并启用基于注解的声明式事务管理功能
    

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

  1. 再次测试
@Testpublic 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()

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

  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

其他的不常用