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 {@Resourceprivate 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 {@Testpublic 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 {@Testpublic 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);}@Testpublic void updateBalance() {//获取到容器ApplicationContext ioc =new ClassPathXmlApplicationContext("tx_ioc.xml");GoodsDao goodsDao = ioc.getBean(GoodsDao.class);goodsDao.updateBalance(1, 1f);System.out.println("用户余额减少成功");}@Testpublic 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@Resourceprivate 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 {//测试用户购买商品业务@Testpublic 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@Resourceprivate GoodsDao goodsDao;/*** @Transactional 注解解读* 1.使用@Transactional 可以进行声明式事务控制* 2.即将标识的方法中的对数据库的操作作为一个事务管理* @param userId* @param goodsId* @param amount*/@Transactionalpublic 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 {//测试用户购买商品业务@Testpublic 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 {@Resourceprivate 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@Resourceprivate GoodsDao goodsDao;/*** @Transactional 注解解读* 1.使用@Transactional 可以进行声明式事务控制* 2.即将标识的方法中的对数据库的操作作为一个事务管理* 3.@Transaction 底层使用的仍然是AOP* 4.底层使用动态代理对象来调用buyGoodsByTx* 5.在执行buyGoodsByTx()方法时, 先调用 事务管理器的 doBegin(), 再调用buyGoodsByTx()*   如果执行没有发生异常, 则调用 事务管理器的 doCommit(), 如果发生异常, 调用 事务管理器的*   doRollback()* @param userId* @param goodsId* @param amount*/@Transactionalpublic 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*/@Transactionalpublic 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 {@Resourceprivate 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 {//测试事务的传播机制@Testpublic 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@Resourceprivate GoodsDao goodsDao;/*** 说明* 1.在默认情况下, 声明式事务的隔离级别是 REPEATABLE READ*/@Transactionalpublic 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 {//测试声明式事务的隔离级别@Testpublic 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@Resourceprivate 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 属性@Testpublic 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 {@Resourceprivate 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 {@Resourceprivate 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 {@Resourceprivate 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 {@Resourceprivate 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 {@Resourceprivate BuyerDao buyerDao;@Resourceprivate SellerDao sellerDao;@Resourceprivate GoodsDao goodsDao;@Resourceprivate TaobaoDao taobaoDao;//用户购买商品的行为涉及多张表, 视为一个事务进行管理@Transactionalpublic 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 {@Testpublic void buyGoods() {//获取容器ApplicationContext ioc =new ClassPathXmlApplicationContext("shopping_ioc.xml");GoodsService goodsService = ioc.getBean(GoodsService.class);goodsService.buyGoods(1,1,1,1,2);}
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ZooKeeper的应用场景(分布式锁、分布式队列)

7 分布式锁 分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源&#xff0c;那么访问这些资源的时候&#xff0c;往往需要通过一些互斥手段来防止彼此之间的干扰&#xff0c;以保证一致性&#xff0c;…

岛屿的最大面积(力扣)递归 JAVA

给你一个大小为 m x n 的二进制矩阵 grid 。 岛屿 是由一些相邻的 1 (代表土地) 构成的组合&#xff0c;这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0&#xff08;代表水&#xff09;包围着。 岛屿的面积是岛上值为 1 的…

error_Network Error

此页面为订单列表&#xff0c;是混合开发(页面嵌入在客户端中) 此页面为订单列表&#xff0c;此需求在开发时后端先将代码发布在测试环境&#xff0c;我在本地调试时调用的后端接口进行联调没有任何问题。 此后我将代码发布在测试环境&#xff0c;在app中打开页面&#xff0c…

【C与C++的相互调用方法】

C与C的相互调用方法 C与C为什么相互调用的方式不同C中调用CC中调用C致谢 C与C为什么相互调用的方式不同 C 和 C 之间的相互调用方式存在区别&#xff0c;主要是由于 C 和 C 语言本身的设计和特性不同。 函数调用和参数传递方式不同&#xff1a;C 和 C 在函数调用和参数传递方面…

docker oracle linux命令执行sql

docker 安装参照 https://blog.csdn.net/arcsin_/article/details/123707618 docker container ls -a命令查看容器名 打开容器 docker exec -it orcl19c_03 /bin/bashsys 用户登录容器 sqlplus / as sysdbashow pdbs;什么是pdb数据库&#xff1f;什么是CDB&#xff1f; 参…

微信小程序 蓝牙设备连接,控制开关灯

1.前言 微信小程序中连接蓝牙设备&#xff0c;信息写入流程 1、检测当前使用设备&#xff08;如自己的手机&#xff09;是否支持蓝牙/蓝牙开启状态 wx:openBluetoothAdapter({}) 2、如蓝牙已开启状态&#xff0c;检查蓝牙适配器的状态 wx.getBluetoothAdapterState({}) 3、添加…

第十三章 SpringBoot项目(总)

1.创建SpringBoot项目 1.1.设置编码 1.4.导入已有的spring boot项目 2.快速搭建Restfull风格的项目 2.1.返回字符串 RestController public class IndexController {RequestMapping("/demo1")public Object demo1() {System.out.println("demo1 ran...."…

kafka的位移

文章目录 概要消费位移__consumer_offsets主题位移提交 概要 本文主要总结kafka的位移是如何管理的&#xff0c;在broker端如何通过命令行查看到位移信息&#xff0c;并从代码层面总结了位移的提交方式。 消费位移 对于 Kafka 中的分区而言&#xff0c;它的每条消息都有唯一…

0基础学习VR全景平台篇 第86篇:智慧眼-为什么要设置分组选择?

一、功能说明 分组选择&#xff0c;也就是给全景的每个分组去设置其所属的行政区划&#xff0c;设置后只有属于同行政区划的成员才可进入其场景进行相关操作&#xff0c;更便于实现城市的精细化管理。 二、后台编辑界面 分组名称&#xff1a;场景的分组名称。 对应分类&…

网络安全--linux下Nginx安装以及docker验证标签漏洞

目录 一、Nginx安装 二、docker验证标签漏洞 一、Nginx安装 1.首先创建Nginx的目录并进入&#xff1a; mkdir /soft && mkdir /soft/nginx/cd /soft/nginx/ 2.下载Nginx的安装包&#xff0c;可以通过FTP工具上传离线环境包&#xff0c;也可通过wget命令在线获取安装包…

【数据结构与算法】队列

文章目录 一&#xff1a;队列1.1 队列的概念1.2 队列的介绍1.3 队列示意图 二&#xff1a;数组模拟队列2.1 介绍2.2 思路2.3 代码实现2.3.1 定义队列基本信息2.3.2 初始化队列2.3.3 判断队列是否满&#xff0c;是否为空2.3.4 添加数据到队列2.3.5 获取队列数据&#xff0c;出队…

图数据库_Neo4j和SpringBoot整合使用_创建节点_删除节点_创建关系_使用CQL操作图谱---Neo4j图数据库工作笔记0009

首先需要引入依赖 springboot提供了一个spring data neo4j来操作 neo4j 可以看到它的架构 这个是下载下来的jar包来看看 有很多cypher对吧 可以看到就是通过封装的驱动来操作graph database 然后开始弄一下 首先添加依赖

【实用黑科技】如何 把b站的缓存视频弄到本地——数据恢复软件WinHex 和 音视频转码程序FFmpeg

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;效率…

Baumer工业相机堡盟工业相机如何通过BGAPISDK设置相机的固定帧率(C#)

Baumer工业相机堡盟工业相机如何通过BGAPI SDK设置相机的固定帧率&#xff08;C#&#xff09; Baumer工业相机Baumer工业相机的固定帧率功能的技术背景CameraExplorer如何查看相机固定帧率功能在BGAPI SDK里通过函数设置相机固定帧率 Baumer工业相机通过BGAPI SDK设置相机固定帧…

蓝牙资讯|中国智能家居前景广阔,蓝牙Mesh照明持续火爆

据俄罗斯卫星通讯社报道&#xff0c;中国已成为全球最大的智能家居消费国&#xff0c;占全球50%—60%的市场份额。未来&#xff0c;随着人工智能技术的发展以及智能家居生态的不断进步&#xff0c;智能家居在中国的渗透率将加速提升。德国斯塔蒂斯塔调查公司数据显示&#xff0…

win10系统docker创建ubuntu容器解决开发环境问题

一、win10系统使用docker的原因 最近啊&#xff0c;在学习人工智能-深度学习&#xff0c;用的win10系统进行开发&#xff0c;老是出现一些莫名其妙的问题&#xff0c;无法解决&#xff0c;每天都在为环境问题搞得伤透了脑筋。 说到底还是要使用Linux系统进行开发比较合适。 …

【MT32F006】MT32F006之HT1628驱动LED

本文最后修改时间&#xff1a;2023年03月30日 一、本节简介 本文介绍如何使用MT32F006连接HT1628芯片驱动LED。 二、实验平台 库版本&#xff1a;V1.0.0 编译软件&#xff1a;MDK5.37 硬件平台&#xff1a;MT32F006开发板&#xff08;主芯片MT32F006&#xff09; 仿真器&a…

LeetCode算法心得——限制条件下元素之间的最小绝对差(TreeSet)

大家好&#xff0c;我是晴天学长&#xff0c;今天用到了Java一个非常实用的类TreeSet&#xff0c;能解决一些看起来棘手的问题。 1 &#xff09;限制条件下元素之间的最小绝对差 2) .算法思路 初始化变量&#xff1a;n为列表nums的大小。 min为整型最大值&#xff0c;用于记录…

python3 0学习笔记之基本知识

0基础学习笔记之基础知识 &#x1f4da; 基础内容1. 条件语句 if - elif - else2. 错误铺捉try - except(一种保险策略&#xff09;3. 四种开发模式4. 函数&#xff1a;def用来定义函数的5. 最大值最小值函数&#xff0c;max &#xff0c;min6. is 严格的相等&#xff0c;is no…

机器学习:基本介绍

机器学习介绍 Hnad-crafted rules Hand-crafted rules&#xff0c;叫做人设定的规则。那假设今天要设计一个机器人&#xff0c;可以帮忙打开或关掉音乐&#xff0c;那做法可能是这样&#xff1a; 设立一条规则&#xff0c;就是写一段程序。如果输入的句子里面看到**“turn of…