Spring系列七:声明式事务

🐘声明式事务

和AOP有密切的联系, 是AOP的一个实际的应用.

🐲事务分类简述

●分类
1.编程式事务:
示意代码, 传统方式
Connection connection = JdbcUtils.getConnection();
try {
    //1.先设置事务不要自动提交
    connection.setAutoCommit(false);
    //2.进行各种crud
    //多个表的修改, 添加, 删除
    //3.提交
    connection.commit();
} catch (Exception e) {
    //4.回滚
    connection.rollback();
}

🐲声明式事务案例


需求分析

我们需要去处理用户购买商品的业务逻辑. 分析: 当一个用户要去购买商品应该包含三个步骤

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

其实我们也可以看到, 这时, 需要涉及到三张表: 用户表, 商品表, 商品存量表. 应该使用事务管理.


解决方案分析

1.使用传统的编程事务来处理, 将代码写到一起 [缺点: 代码冗余, 效率低, 不利于扩展, 优点是简单, 好理解]

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

2.使用Spring的声明式事务处理, 可以将上面三个子步骤分别写成一个方法, 然后统一管理. [这是Spring很牛的地方, 在开发使用很多, 优点是无代码冗余, 效率高, 扩展方便, 缺点是理解较困难 ==> 底层使用AOP(动态代理+动态绑定+反射+注解) => 看Debug源码]


代码实现

1.先创建商品系统的数据库和表

USE spring;

-- 用户表
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(1, '张三', 300);
INSERT INTO user_account VALUES(2, '李四', 400);

UPDATE user_account SET money = money - 1 WHERE user_id = '1';
SELECT * FROM user_account;

-- 商品表
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(1, '电风扇', '13.5');
INSERT INTO goods VALUES(2, '小台灯', '15.5');

SELECT price FROM goods WHERE goods_id = 1;

-- 商品库存表
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, 100);
INSERT INTO goods_amount VALUES(2, 200);
INSERT INTO goods_amount VALUES(3, 300);


UPDATE goods_amount SET goods_num = goods_num - 1 WHERE goods_id = '1';
SELECT * FROM goods_amount;

2.在spring项目com.zzw.spring.tx.dao包下新建GoodsDao

@Repository //将GoodsDao-对象 注入到spring容器
public class GoodsDao {

    @Resource
    private JdbcTemplate jdbcTemplate;

    /**
     * 根据商品id返回价格
     * @param id
     * @return
     */
    public Float queryPriceById(Integer goods_id) {
        String sql = "SELECT price FROM goods WHERE goods_id = ?";
        Float price = jdbcTemplate.queryForObject(sql, Float.class, goods_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, Integer amount) {
        String sql = "UPDATE goods_amount SET goods_num = goods_num - ? WHERE goods_id = ?";
        jdbcTemplate.update(sql, amount, goods_id);
    }
}

3.src目录下, 新建容器配置文件 tx_ioc.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:component-scan base-package="com.zzw.spring.tx.dao"/>
</beans>

4.在com.zzw.spring.tx包下新建测试类TxTest

public class TxTest {
    @Test
    public void queryPriceById() {
        //获取到容器
        ApplicationContext ioc =
                new ClassPathXmlApplicationContext("tx_ioc.xml");

        GoodsDao goodsDao = ioc.getBean(GoodsDao.class);

        Float price = goodsDao.queryPriceById(1);
        System.out.println("id等于1的价格=" + price);
    }
}

结果报错 No qualifying bean of type 'org.springframework.jdbc.core.JdbcTemplate' available

因为没有注入JdbcTemplate对象, 正确配置tx_ioc.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:component-scan base-package="com.zzw.spring.tx.dao"/>

    <!--引入外部的jdbc.properties文件-->
    <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对象-->
    <!--JdbcTemplate会使用到DataSource, 而DataSource可以拿到连接去操作数据库-->
    <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
        <!--给JdbcTemplate对象配置dateSource-->
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>

再次测试

public class TxTest {
    @Test
    public void queryPriceById() {
        //获取到容器
        ApplicationContext ioc =
                new ClassPathXmlApplicationContext("tx_ioc.xml");

        GoodsDao goodsDao = ioc.getBean(GoodsDao.class);

        Float price = goodsDao.queryPriceById(1);
        System.out.println("id等于1的价格=" + price);
    }

    @Test
    public void updateBalance() {
        //获取到容器
        ApplicationContext ioc =
                new ClassPathXmlApplicationContext("tx_ioc.xml");

        GoodsDao goodsDao = ioc.getBean(GoodsDao.class);

        goodsDao.updateBalance(1, 1f);
        System.out.println("用户余额减少成功");
    }

    @Test
    public void updateAmount() {
        //获取到容器
        ApplicationContext ioc =
                new ClassPathXmlApplicationContext("tx_ioc.xml");

        GoodsDao goodsDao = ioc.getBean(GoodsDao.class);

        goodsDao.updateAmount(1, 1);
        System.out.println("商品减少库存量成功");
    }
}

5.在com.zzw.spring.tx.service包下新建GoodsService, 验证不使用事务就会出现数据不一致现象.

@Service //将GoodsService-对象 注入到spring容器
public class GoodsService {

    //定义属性GoodsDao
    @Resource
    private GoodsDao goodsDao;

    /**
     * 编写一个方法, 完成用户购买商品的业务
     * 这里主要是讲解事务管理
     * @param userId  用户id
     * @param goodsId 商品id
     * @param amount   购买数量
     */
    public void buyGoods(int userId, int goodsId, int amount) {

        //输出购买相关信息
        System.out.println("用户购买信息 用户id=" + userId + ", 商品id=" +
                goodsId + ", 购买数量=" + amount);

        //1.得到商品的价格
        Float price = goodsDao.queryPriceById(goodsId);
        //2.减少用户的余额
        goodsDao.updateBalance(userId, price * amount);
        //3.减少库存量
        goodsDao.updateAmount(goodsId, amount);

        System.out.println("用户购买成功");
    }
}

修改xml要扫描的包

<!--配置要扫描的包-->
<context:component-scan base-package="com.zzw.spring.tx"/>

测试

public class TxTest {
    //测试用户购买商品业务
    @Test
    public void buyGoodsTest() {
        //获取到容器
        ApplicationContext ioc =
                new ClassPathXmlApplicationContext("tx_ioc.xml");

        GoodsService goodsService = ioc.getBean(GoodsService.class);

        goodsService.buyGoods(1,1,1);
    }
}

验证不使用事务就会出现数据不一致现象. 故意修改MonsterDaoupdateAmount语句

参考: 家居购, 数据不一致问题

/**
 * 修改商品的库存量
 * @param goods_id
 * @param amount
 */
public void updateAmount(Integer goods_id, Integer amount) {
    String sql = "UPDATEX goods_amount SET goods_num = goods_num - ? WHERE goods_id = ?";
    jdbcTemplate.update(sql, amount, goods_id);
}

结论:不能出现部分正确的情况, 要成为一个整体, 要有原子性.


6.加入@Transactional注解

@Service //将GoodsService-对象 注入到spring容器
public class GoodsService {

    //定义属性GoodsDao
    @Resource
    private GoodsDao goodsDao;

    /**
     * @Transactional 注解解读
     * 1.使用@Transactional 可以进行声明式事务控制
     * 2.即将标识的方法中的对数据库的操作作为一个事务管理
     * @param userId
     * @param goodsId
     * @param amount
     */
    @Transactional
    public void buyGoodsByTx(int userId, int goodsId, int amount) {

        //输出购买相关信息
        System.out.println("用户购买信息 用户id=" + userId + ", 商品id=" +
                goodsId + ", 购买数量=" + amount);

        //1.得到商品的价格
        Float price = goodsDao.queryPriceById(goodsId);
        //2.减少用户的余额
        goodsDao.updateBalance(userId, price * amount);
        //3.减少库存量
        goodsDao.updateAmount(goodsId, amount);

        System.out.println("用户购买成功");
    }
}

测试

public class TxTest {
    //测试用户购买商品业务
    @Test
    public void buyGoodsTestByTx() {
        //获取到容器
        ApplicationContext ioc =
                new ClassPathXmlApplicationContext("tx_ioc.xml");

        GoodsService goodsService = ioc.getBean(GoodsService.class);

        //这里我们调用的是进行了事务声明的方法
        goodsService.buyGoodsByTx(1,1,1);
    }
}

发现事务没有发挥作用. 也就是只加一个注解是没有什么作用的.


解决方案:tx_ioc.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" xmlns:tx="http://www.springframework.org/schema/tx"
       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 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--配置要扫描的包-->
    <context:component-scan base-package="com.zzw.spring.tx"/>

    <!--引入外部的jdbc.properties文件-->
    <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对象-->
    <!--JdbcTemplate会使用到DataSource, 而DataSource可以拿到连接去操作数据库-->
    <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
        <!--给JdbcTemplate对象配置dateSource-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置事务管理器-对象
    1.DataSourceTransactionManager 这个对象是进行事务管理的
    2.一定要配置数据源属性, 这样指定该事务管理器 是对哪个数据源进行事务控制
    -->
    <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置启用基于注解的声明式事务管理功能-->
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

在这里插入图片描述

7.debug事务管理器

/**
 * @Transactional 注解解读
 * 1.使用@Transactional 可以进行声明式事务控制
 * 2.即将标识的方法中的对数据库的操作作为一个事务管理
 * 3.@Transaction 底层使用的仍然是AOP
 * 4.底层使用动态代理对象来调用buyGoodsByTx
 * 5.在执行buyGoodsByTx()方法时, 先调用 事务管理器的 doBegin(), 再调用buyGoodsByTx()
 *   如果执行没有发生异常, 则调用 事务管理器的 doCommit(), 如果发生异常, 调用 事务管理器的
 *   doRollback()
 * @param userId
 * @param goodsId
 * @param amount
 */
@Transactional
public void buyGoodsByTx(int userId, int goodsId, int amount) {

    //输出购买相关信息
    System.out.println("用户购买信息 用户id=" + userId + ", 商品id=" +
            goodsId + ", 购买数量=" + amount);

    //1.得到商品的价格
    Float price = goodsDao.queryPriceById(goodsId);
    //2.减少用户的余额
    goodsDao.updateBalance(userId, price * amount);
    //3.减少库存量
    goodsDao.updateAmount(goodsId, amount);

    System.out.println("用户购买成功");
}

1.在这里打个断点在这里插入图片描述

2.开始测试. 运行到doBegin方法
在这里插入图片描述

在doBegin方法中将connection设置为不自动提交
在这里插入图片描述

进入buyGoodsByTx方法. 从第一句开始执行
在这里插入图片描述

🔴
当运行到updateAmount会报错, 出错后进入rollback方法
在这里插入图片描述

进行回滚
在这里插入图片描述

🔴
如果程序不报错, 会进入doCommit方法
在这里插入图片描述

提交
在这里插入图片描述

doBegin相当于前置通知
doCommit相当于返回通知
doRollback相当于异常通知

🐘事务传播机制问题

1.当有多个事务处理并存时, 如何控制?
2.比如用户去购买两次商品(使用不同的方法), 每个方法都是一个事务, 如何控制呢?

🐲事务传播机制种类

事务传播的属性/种类机制分析, 重点分析 requiredrequires_new 两种事务传播属性, 其它忽略.

传播属性描述
REQUIRED如果有事务在运行, 当前的方法就在这个事务内运行. 否则, 就启动一个新的事务, 并在自己的事务内运行.
REQUIRES_NEW当前的方法必须启动新事务, 并在它自己的事务内运行, 如果有事务正在运行, 应该将它挂起

🐲事务传播机制图解

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

1.如果设置为REQUIRES_NEW
buyGoods2如果错误, 不会影响到 buyGoods(), 反之亦然, 即他们的事务是独立的
2.如果设置为REQUIRED
buyGoods2buyGoods是一个整体, 只要有方法的事务错误, 那么两个方法都不会执行成功.

🐲事务传播机制应用实例

需求说明

  • 比如用户去购买两次商品(使用不同的方法), 每个方法都是一个事务, 如何控制呢? => 这就是事务的传播机制

案例

GoodsDao

@Repository //将GoodsDao-对象 注入到spring容器
public class GoodsDao {

    @Resource
    private JdbcTemplate jdbcTemplate;

    /**
     * 根据商品id返回价格
     * @param id
     * @return
     */
    public Float queryPriceById(Integer goods_id) {
        String sql = "SELECT price FROM goods WHERE goods_id = ?";
        Float price = jdbcTemplate.queryForObject(sql, Float.class, goods_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, Integer amount) {
        String sql = "UPDATE goods_amount SET goods_num = goods_num - ? WHERE goods_id = ?";
        jdbcTemplate.update(sql, amount, goods_id);
    }

    public Float queryPriceById2(Integer goods_id) {
        String sql = "SELECT price FROM goods WHERE goods_id = ?";
        Float price = jdbcTemplate.queryForObject(sql, Float.class, goods_id);
        return price;
    }

    public void updateBalance2(Integer user_id, Float money) {
        String sql = "UPDATE user_account SET money = money - ? WHERE user_id = ?";
        jdbcTemplate.update(sql, money, user_id);
    }

    public void updateAmount2(Integer goods_id, Integer amount) {
        String sql = "UPDATE goods_amount SET goods_num = goods_num - ? WHERE goods_id = ?";
        jdbcTemplate.update(sql, amount, goods_id);
    }
}

GoodsService

@Service //将GoodsService-对象 注入到spring容器
public class GoodsService {

    //定义属性GoodsDao
    @Resource
    private GoodsDao goodsDao;

    /**
     * @Transactional 注解解读
     * 1.使用@Transactional 可以进行声明式事务控制
     * 2.即将标识的方法中的对数据库的操作作为一个事务管理
     * 3.@Transaction 底层使用的仍然是AOP
     * 4.底层使用动态代理对象来调用buyGoodsByTx
     * 5.在执行buyGoodsByTx()方法时, 先调用 事务管理器的 doBegin(), 再调用buyGoodsByTx()
     *   如果执行没有发生异常, 则调用 事务管理器的 doCommit(), 如果发生异常, 调用 事务管理器的
     *   doRollback()
     * @param userId
     * @param goodsId
     * @param amount
     */
    @Transactional
    public void buyGoodsByTx(int userId, int goodsId, int amount) {

        //输出购买相关信息
        System.out.println("用户购买信息 用户id=" + userId + ", 商品id=" +
                goodsId + ", 购买数量=" + amount);

        //1.得到商品的价格
        Float price = goodsDao.queryPriceById(goodsId);
        //2.减少用户的余额
        goodsDao.updateBalance(userId, price * amount);
        //3.减少库存量
        goodsDao.updateAmount(goodsId, amount);

        System.out.println("用户购买成功");
    }

    /**
     * 这个方法是第二套进行商品购买的方法
     * @param userId
     * @param goodsId
     * @param amount
     */
    @Transactional
    public void buyGoodsByTx2(int userId, int goodsId, int amount) {

        //输出购买相关信息
        System.out.println("用户购买信息 用户id=" + userId + ", 商品id=" +
                goodsId + ", 购买数量=" + amount);

        //1.得到商品的价格
        Float price = goodsDao.queryPriceById2(goodsId);
        //2.减少用户的余额
        goodsDao.updateBalance2(userId, price * amount);
        //3.减少库存量
        goodsDao.updateAmount2(goodsId, amount);

        System.out.println("用户购买成功");
    }
}

com.zzw.spring.tx.service包下新建 MultiplyService

@Service
public class MultiplyService {

    @Resource
    private GoodsService goodsService;

    /**
     * 1.multiBuyGoodsByTx 这个方法 有两次购买商品的操作
     * 2.buyGoodsByTx 和 buyGoodsByTx2 都是使用了声明式事务
     * 3.当前 buyGoodsByTx 和 buyGoodsByTx2 使用的传播属性是默认的, 即REQUIRED
     *   即会当做一个整体事务管理, 比如buyGoodsByTx方法成功, 但是buyGoodsByTx2失败,
     *   会造成整个事务的回滚, 即会回滚buyGoodsByTx.
     */
    public void multiBuyGoodsByTx() {

        goodsService.buyGoodsByTx(1, 1, 1);

        goodsService.buyGoodsByTx2(1,1,1);
    }
}

测试

public class TxTest {
    //测试事务的传播机制
    @Test
    public void multiBuyGoodsByTest() {
        //获取到容器
        ApplicationContext ioc =
                new ClassPathXmlApplicationContext("tx_ioc.xml");

        MultiplyService multiplyService = ioc.getBean(MultiplyService.class);

        multiplyService.multiBuyGoodsByTx();
		
		System.out.println("ok");
    }
}

测试后, 代码无异常, 此时数据库表信息如下
在这里插入图片描述
在这里插入图片描述


1.将GoodsDao类下的updateAmount2方法 UPDATE改成UPDATEX, 运行后, 代码回滚, 查看数据库
在这里插入图片描述
在这里插入图片描述

2.在此基础上, 将GoodsServicebuyGoodsByTxbuyGoodsByTx2 的事务传播属性修改成 REQUIRES_NEW. 查看数据库
在这里插入图片描述

在这里插入图片描述

🐘事务隔离级别说明

mysql中的事务隔离级别

1.声明式事务中, 默认的隔离级别, 就是mysql数据库默认的隔离级别, 一般为 repeatable read

2.看源码可知 Isolation.DEFAULT 是: Use the default isolation level of the underlying datastore.
在这里插入图片描述
在这里插入图片描述

3.查看数据库的默认隔离级别 selec t @@global.tx_isolation;

🐲事务隔离级别应用实例

1.修改GoodsService.java, 先测默认隔离级别, 增加方法 buyGoodsByTxISOLATION()

@Service //将GoodsService-对象 注入到spring容器
public class GoodsService {

    //定义属性GoodsDao
    @Resource
    private GoodsDao goodsDao;

    /**
     * 说明
     * 1.在默认情况下, 声明式事务的隔离级别是 REPEATABLE READ
     */
    @Transactional
    public void buyGoodsByTxISOLATION() {
        //查询两次商品的价格
        Float price = goodsDao.queryPriceById(1);
        System.out.println("第一次查询的price=" + price);

        Float price2 = goodsDao.queryPriceById(1);
        System.out.println("第二次查询的price=" + price2);

    }
}
public class TxTest {
    //测试声明式事务的隔离级别
    @Test
    public void buyGoodsByTxISOLATIONTest() {
        //获取到容器
        ApplicationContext ioc =
                new ClassPathXmlApplicationContext("tx_ioc.xml");

        GoodsService goodsService = ioc.getBean(GoodsService.class);

        goodsService.buyGoodsByTxISOLATION();
    }
}

1.测试,默认隔离级别: 可重复读
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.测试,隔离级别: 读已提交
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

🐲事务超时回滚

●基本介绍
1.如果一个事务执行的时间超过某个时间限制, 就让该事务回滚
2.可以通过设置事务超时回滚来实现

案例
1.修改GoodsService.java, 增加buyGoodsByTxTimeout()

@Service //将GoodsService-对象 注入到spring容器
public class GoodsService {

    //定义属性GoodsDao
    @Resource
    private GoodsDao goodsDao;

    /**
     * 解读
     * 1.@Transactional(timeout = 2)
     * 2.timeout = 2 表示 buyGoodsByTxTimeout方法 如果执行时间执行了2秒钟
     *   , 该事务就进行回滚.
     * 3.如果你没有设置 timeout, 默认是-1, 表示使用事务的默认超时时间
     *   或者不支持
     */
    @Transactional(timeout = 2)
    public void buyGoodsByTxTimeout(int userId, int goodsId, int amount) {

        //输出购买相关信息
        System.out.println("用户购买信息 用户id=" + userId + ", 商品id=" +
                goodsId + ", 购买数量=" + amount);

        //1.得到商品的价格
        Float price = goodsDao.queryPriceById2(goodsId);
        //2.减少用户的余额
        goodsDao.updateBalance2(userId, price * amount);
        //3.减少库存量
        goodsDao.updateAmount2(goodsId, amount);

        System.out.println("用户购买成功");
    }
}

测试代码是否正确

public class TxTest {
    //测试timeout 属性
    @Test
    public void buyGoodsByTxTimeoutTest() {
        //获取到容器
        ApplicationContext ioc =
                new ClassPathXmlApplicationContext("tx_ioc.xml");

        GoodsService goodsService = ioc.getBean(GoodsService.class);

        goodsService.buyGoodsByTxTimeout(1,1,1);
    }
}

模拟超时

/**
 * 解读
 * 1.@Transactional(timeout = 2)
 * 2.timeout = 2 表示 buyGoodsByTxTimeout方法 如果执行时间执行了2秒钟
 *   , 该事务就进行回滚.
 * 3.如果你没有设置 timeout, 默认是-1, 表示使用事务的默认超时时间
 *   或者不支持
 */
@Transactional(timeout = 2)
public void buyGoodsByTxTimeout(int userId, int goodsId, int amount) {

    //输出购买相关信息
    System.out.println("用户购买信息 用户id=" + userId + ", 商品id=" +
            goodsId + ", 购买数量=" + amount);

    //1.得到商品的价格
    Float price = goodsDao.queryPriceById2(goodsId);
    //2.减少用户的余额
    goodsDao.updateBalance2(userId, price * amount);

    //模拟超时
    System.out.println("========超时开始========");
    try {
        Thread.sleep(4000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    System.out.println("========超时结束========");
    //3.减少库存量
    goodsDao.updateAmount2(goodsId, amount);

    System.out.println("用户购买成功");
}

超时会报异常, 进入doRollback方法, 进行回滚在这里插入图片描述

🐲课后练习

模拟一个用户, 进行银行转账购买淘宝商品的业务, 数据表/dao/service自己设计, 保证数据一致性.
1)seller[卖家]
2)buyer[买家]
3)goods[商品表(库存量)]
4)taoBao[提取入账成交额的10%]
5)简单实现, 使用声明式事务完成
6)要求创建一个新的spring容器配置文件 shopping_ioc.xml, 完成测试


1.sql代码

-- create database spring_homework;
USE spring_homework;

-- 删除表
DROP TABLE goods;
DROP TABLE seller;
DROP TABLE buyer;
DROP TABLE taoBao;

-- 买家表
CREATE TABLE buyer (
	id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
	`name` VARCHAR(32) NOT NULL DEFAULT '',
	balance DOUBLE NOT NULL DEFAULT 0.0
)CHARSET=utf8;
INSERT INTO buyer VALUES(1, '小明', 500);
-- update buyer set balance = balance + 1 where id = 1;
SELECT * FROM buyer;

-- 卖家表
CREATE TABLE seller (
	id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
	`name` VARCHAR(32) NOT NULL DEFAULT '',
	balance DOUBLE NOT NULL DEFAULT 0.0
)CHARSET=utf8;
INSERT INTO seller VALUES(1, '王守义', 500);
-- update seller set balance = balance + 1 where id = 1;
SELECT * FROM seller;

-- 商品表
CREATE TABLE goods (
	id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
	seller_id INT UNSIGNED,
	`name` VARCHAR(32) NOT NULL DEFAULT '',
	price DOUBLE NOT NULL DEFAULT 0.0,
	inventory INT UNSIGNED
)CHARSET=utf8;
INSERT INTO goods VALUES(1, 1, '王守义十三香', 10, 5000);
-- update goods set inventory = inventory - 1 where id = 1;
SELECT * FROM goods WHERE id = 1;

-- taoBao表
CREATE TABLE taoBao (
	id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
	scale DOUBLE NOT NULL DEFAULT 0.1,
	balance DOUBLE NOT NULL DEFAULT 0.0
)CHARSET=utf8;
INSERT INTO taoBao VALUES(1, 0.1, 500);
-- update taoBao set balance = balance + 1 where id = 1;
SELECT * FROM taoBao WHERE id = 1;

2.com.zzw.spring.tx.homework.dao包

@Repository
public class GoodsDao {

    @Resource
    private JdbcTemplate jdbcTemplate;

    /**
     * 查询商品价格. 根据商品id, 查询商品价格
     * @param id
     * @return price
     */
    public Double queryPrice(int id) {
        String sql = "SELECT price FROM goods WHERE id = ?";
        Double price = jdbcTemplate.queryForObject(sql, Double.class, id);
        return price;
    }

    /**
     * 更新商品库存. 根据商品id, 减去库存
     * @param id
     * @param count
     */
    public void updateInventory(int id, int count) {
        String sql = "update goods set inventory = inventory - ? where id = ?";
        int affected = jdbcTemplate.update(sql, count, id);
        System.out.println("affected=" + affected);
    }
}
@Repository
public class BuyerDao {
    @Resource
    private JdbcTemplate jdbcTemplate;

    /**
     * 更新买家余额. 从买家表中, 扣除金额
     * @param id
     * @param money
     */
    public void updateBalance(int id, double money) {
        String sql = "UPDATE buyer SET balance = balance - ? WHERE id = ?";
        int affected = jdbcTemplate.update(sql, money, id);
        System.out.println("affected=" + affected);
    }
}
@Repository
public class SellerDao {
    @Resource
    private JdbcTemplate jdbcTemplate;

    /**
     * 更新卖家余额. 对卖家账号, 增加金额
     * @param id
     * @param money
     */
    public void updateBalance(int id, double money) {
        String sql = "UPDATE seller SET balance = balance + ? WHERE id = ?";
        int affected = jdbcTemplate.update(sql, money, id);
        System.out.println("affected=" + affected);
    }
}
@Repository
public class TaobaoDao {
    @Resource
    private JdbcTemplate jdbcTemplate;

    /**
     * 更新淘宝余额. 给id等于1的淘宝账号, 增加金额
     * @param id
     * @param count
     */
    public void updateBalance(int id, double money) {
        String sql = "UPDATE taoBao SET balance = balance + ? WHERE id = ?";
        int affected = jdbcTemplate.update(sql, money, id);
        System.out.println("affected=" + affected);
    }
}

3.com.zzw.spring.tx.homework.service

@Service
public class GoodsService {

    @Resource
    private BuyerDao buyerDao;
    @Resource
    private SellerDao sellerDao;
    @Resource
    private GoodsDao goodsDao;
    @Resource
    private TaobaoDao taobaoDao;


    //用户购买商品的行为涉及多张表, 视为一个事务进行管理
    @Transactional
    public void buyGoods(int buyerId, int taoBaoId, int sellerId, int goodsId, int count) {

        //1.查询商品价格
        Double price = goodsDao.queryPrice(goodsId);
        //计算花费多少钱
        double money = price * count;

        //2.更新买家余额
        buyerDao.updateBalance(buyerId, money);
        3.更新卖家余额. 将成交额的90%转入卖家余额
        sellerDao.updateBalance(sellerId, money * 0.9);
        //4.更新淘宝余额. 将成交额的10%转入淘宝余额
        taobaoDao.updateBalance(taoBaoId, money * 0.1);
        //5.更新商品库存
        goodsDao.updateInventory(goodsId, count);

        System.out.println("用户 id=" + buyerId + " 在平台 id=" + taoBaoId + " 商家 id=" + sellerId
                + " 购买了商品 id=" + goodsId + " 数量 count=" + count);
    }
}

4.配置文件
jdbc_homework.properties

jdbc.user=root
jdbc.pwd=zzw
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_homework

shopping_ioc.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" xmlns:tx="http://www.springframework.org/schema/tx"
       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 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--配置扫描包-->
    <context:component-scan base-package="com.zzw.spring.tx.homework"/>

    <!--引入外部文件 jdbc.properties-->
    <context:property-placeholder location="classpath:jdbc_homework.properties"/>

    <!--配置数据源-->
    <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">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--配置事务管理器-对象
    1.DataSourceTransactionManager 这个对象是进行事务管理的
    2.一定要配置数据源属性 这样指定这个事务管理器 是对哪个数据源进行事务控制
    -->
    <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--启用基于注解的声明式事务管理功能-->
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

在这里插入图片描述

5.测试

public class buyGoodsTest {
    @Test
    public void buyGoods() {
        //获取容器
        ApplicationContext ioc =
                new ClassPathXmlApplicationContext("shopping_ioc.xml");

        GoodsService goodsService = ioc.getBean(GoodsService.class);

        goodsService.buyGoods(1,1,1,1,2);
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/78797.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【爬虫】爬取旅行评论和评分

以马蜂窝“普达措国家公园”为例&#xff0c;其评论高达3000多条&#xff0c;但这3000多条并非是完全向用户展示的&#xff0c;向用户展示的只有5页&#xff0c;数了一下每页15条评论&#xff0c;也就是75条评论&#xff0c;有点太少了吧&#xff01; 因此想了个办法尽可能多爬…

Swagger

目录 简介 使用方式&#xff1a; 常用注解 简介 使用Swagger你只需要按照他的规范去定义接口及接口相关信息再通过Swagger衍生出来的一系列项目和工具&#xff0c;就可以做到生成各种格式的接口文档&#xff0c;以及在线接口调试页面等等。 官网&#xff1a;https://swagger…

【0基础入门Python笔记】二、python 之逻辑运算和制流程语句

二、python 之逻辑运算和制流程语句 逻辑运算控制流程语句条件语句&#xff08;if语句&#xff09;循环结构&#xff08;for循环、while循环&#xff09;控制流程语句的嵌套以及elif 逻辑运算 Python提供基本的逻辑运算&#xff1a;不仅包括布尔运算&#xff08;and、or、not&…

基于深度信念神经网络+长短期神经网络的降雨量预测,基于dbn-lstm的降雨量预测,dbn原理,lstm原理

目录 背影 DBN神经网络的原理 DBN神经网络的定义 受限玻尔兹曼机(RBM) LSTM原理 DBN-LSTM的降雨量预测 基本结构 主要参数 数据 MATALB代码 结果图 展望 背影 DBN是一种深度学习神经网络,拥有提取特征,非监督学习的能力,通过dbn进行无监督学习提取特征,然后长短期神经…

mysql 插入数据锁等待超时报错:Lock wait timeout exceeded; try restarting transaction

报错信息 Lock wait timeout exceeded; try restarting transaction 锁等待超时 Lock wait timeout exceeded; try restarting transaction&#xff0c;是当前事务在等待其它事务释放锁资源造成的 解决办法 1、数据库中执行如下sql&#xff0c;查看当前数据库的线程情况&…

[C++ 网络协议编程] TCP/IP协议

目录 1. TCP/IP协议栈 2. TCP原理 2.1 TCP套接字中的I/O缓冲 2.2 TCP工作原理 2.2.1 三次握手&#xff08;连接&#xff09; 2.2.2 与对方主机的数据交换 2.2.3 四次握手&#xff08;断开与套接字的连接&#xff09; TCP&#xff08;Transmission Control Protocol传输控…

python bytes基本用法

目录 1 第一个字符变大写&#xff0c;其余字符变小写 capitalize() 2 生成指定长度内容&#xff0c;然后把指定的bytes放到中间 center() 3 计数 count() 4 解码 decode() 5 是否以指定的内容结尾 endswith() 6 将制表符调整到指定大小 expandtabs() 7 寻找指…

Springboot 实践(7)springboot添加html页面,实现数据库数据的访问

前文讲解&#xff0c;项目已经实现了数据库Dao数据接口&#xff0c;并通过spring security数据实现了对系统资源的保护。本文重点讲解Dao数据接口页面的实现&#xff0c;其中涉及页面导航栏、菜单栏及页面信息栏3各部分。 1、创建html页面 前文讲解中&#xff0c;资源目录已经…

2023年服贸会在哪里举行?北京有哪些媒体可以做宣传?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 8月14日&#xff0c;”2023年服贸会新闻发布会在北京首钢园举行&#xff0c;宣布2023年服贸会将于9月2日至6日在国家会议中心和首钢园区举办&#xff0c;采用‘线下线上’‘综合专题’办…

【mysql报错解决】MySql.Data.MySqlClient.MySqlException (0x80004005)或1366

场景&#xff1a;c#使用mysql数据库执行数据库迁移&#xff0c;使用了新增inserter的语句&#xff0c;然后报错 报错如下&#xff1a; 1.MySql.Data.MySqlClient.MySqlException (0x80004005): Incorrect string value: ‘\xE6\x9B\xB4\xE6\x94\xB9…’ for column ‘Migratio…

2023.8.8巨人网络数据开发工程师面试复盘

1 概述 问题一览 总体感觉良好&#xff0c;通过面试官的介绍可知这个岗位偏向离线数仓。 1.自我介绍 2.询问了其中一段实习经历 3.讲下你说用过的Linux命令 4.讲下HIVE的内部表和外部表有什么不同 *5.讲下你使用过的Hive函数&#xff08;好好在复习下多准备几个吧&#xff09…

CentOS 7 安装MySQL8.0.33

一、查看 CentOS 版本 要查看当前 CentOS 版本&#xff0c;你可以执行以下命令&#xff1a; cat /etc/centos-release 该命令将显示当前 CentOS 的版本信息&#xff0c;例如&#xff1a; CentOS Linux release 7.9.2009 (Core) 在这个示例中&#xff0c;CentOS 版本为 7.…

【历史上的今天】8 月 18 日:硅谷神话的衰落;微软发布 QuickBASIC;Adobe Audition 问世

整理 | 王启隆 透过「历史上的今天」&#xff0c;从过去看未来&#xff0c;从现在亦可以改变未来。 8 月 18 日&#xff0c;这是科技历史上难以翻过的一页&#xff0c;因为今天发生的几个主要事件很巧合地都集中在惠普这家公司。在《浪潮之巅》中&#xff0c;吴军评价惠普“某…

IntelliJ IDEA热部署:JRebel插件的安装与使用

热部署 概述JRebel 概述 热部署&#xff0c;指修改代码后&#xff0c;无需停止应用程序&#xff0c;即可使修改后的代码生效&#xff0c;其有利于提高开发效率。 热部署方式&#xff1a; 手动热部署&#xff1a;修改代码后&#xff0c;重新编译项目&#xff0c;然后启动应用程…

音视频实时通话解决方案

1、问题提出 想要实现音视频通话,对于大部分人可能会觉得很难,但是实际上,有些事情并没有大家想的那样困难,只要功夫深,铁杵磨成针。 机缘巧合下,在业务中,我也遇到了一个业务场景需要实现音视频通话,我们不可能自己从零开始干,我本次用到的核心是WebRTC。 2、WebRT…

基于python+django+mysql的校园影院售票系统(可做计算机毕设)

开发柚子校园影院&#xff0c;不仅可以改善用户查看信息难的局面&#xff0c;还可以提高管理效率&#xff0c;同时也可以增强系统的竞争力。利用柚子校园影院的可以有效地提高系统的人事的效率和信息化水平&#xff0c;快速了解信息更新及服务的进度。这既可以确保系统服务的品…

什么是服务网格,为什么 Kubernetes 需要它?

​企业现在热衷于采用微服务架构&#xff0c;因为它具有敏捷性和灵活性。容器和作为首选的容器编排工具—Kubernetes的兴起使得从单体架构向微服务架构的转变变得更加容易。然而&#xff0c;在大规模使用微服务架构时出现了一系列新的挑战&#xff1a; DevOps和架构师很难管理…

Shell学习笔记之基础部分

Shell基础&#xff1a; 查看操作系统支持的shell&#xff1a; [rootrhel9 ansible]# cat /etc/shells /bin/sh /bin/bash /usr/bin/sh /usr/bin/bashShell的基本元素&#xff1a; 声明&#xff1a;声明用哪个命令解释器来解释并执行当前脚本文件中的语句&#xff0c;一般写的…

期待相聚|官宣!2023 Google 谷歌开发者大会来了

对 5 月 Google I/O 大会 记忆犹新&#xff1f;更多精彩即将来临&#xff01; I/O Connect 系列活动的环球之旅 经历迈阿密&#xff0c;阿姆斯特丹&#xff0c;班加罗尔 将技术灵感带到中国 2023 Google 开发者大会 &#xff08;I/O Connect&#xff5c;China&#xff09;…

LVS负载均衡集群

目录 1、什么是集群(含义) 2、集群使用在哪一个场景 3、集群的分类 4、负载均衡器的集群架构 5、负载均衡器的群集的工作模式 1、地址转换(NAT模式) 2、IP隧道(TUN模式) 3、直接路由(DR模式) 6、关于LVS的虚拟服务器 7、LVS的负载均衡器的调度算法 8、LVS组成和作用 …