Spring-声明式事务
声明式事务
1.事务分类
● 分类:
- 编程式事务: 示意代码, 传统方式
Connection connection = JdbcUtils.getConnection();
try {
//1. 先设置事务不要自动提交
connection.setAutoCommint(false);
//2. 进行各种 crud
//多个表的修改,添加 ,删除
//3. 提交
connection.commit();
} catch (Exception e) {
//4. 回滚
conection.rollback();
}
- 声明式事务
2.声明式事务-使用实例
1.需求说明-用户购买商品
- 通过商品 id 获取价格.
- 购买商品(某人购买商品,修改用户的余额.)
- 修改库存量
- 其实大家可以看到,这时,我们需要涉及到三张表商品表,用户表,商品存量表。 应该使用事务处理
2.解决方案分析
- 使用传统的编程事务来处理,将代码写到一起[缺点: 代码冗余,效率低,不利于扩展, 优 点是简单,好理解]
Connection connection = JdbcUtils.getConnection();
try {
//1. 先设置事务不要自动提交 connection.setAutoCommit(false);
//2. 进行各种 crud
//多个表的修改,添加 ,删除 select from 商品表 => 获取价格 修改用户余额 update ... 修改库存量 update
//3. 提交
connection.commit();
} catch (Exception e) {
//4. 回滚
conection.rollback();
}
- 使用 Spring 的声明式事务处理, 可以将上面三个子步骤分别写成一个方法,然后统一 管理
[这个是 Spring 很牛的地方,在开发中使用的很多,优点是无代码冗余,效率高,扩展方便, 缺点是理解较困难]==> 底层使用 AOP (动态代理+动态绑定+反射+注解)
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);
GoodsDao
@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);
}
}
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("用户购买信息 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("用户购买成功~");
}
/**
* @Transactional 注解解读
* 1. 使用@Transactional 可以进行声明式事务控制
* 2. 即将标识的方法中的,对数据库的操作作为一个事务管理
* 3. @Transactional 底层使用的仍然是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("用户购买信息 userId=" + userId
+ " goodsId=" + goodsId + " 购买数量=" + amount);
//1.得到商品的价格
Float price = goodsDao.queryPriceById(userId);
//2. 减少用户的余额
goodsDao.updateBalance(userId, price * amount);
//3. 减少库存量
goodsDao.updateAmount(goodsId, amount);
System.out.println("用户购买成功~");
}
}
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.llp.spring.tx.dao"/>
<context:component-scan base-package="com.llp.spring.tx.service"/>
<!--引入外部的jdbc.properties文件-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--配置数据源对象-DataSoruce-->
<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. DataSourceTransactionManager 这个对象是进行事务管理-debug源码
2. 一定要配置数据源属性,这样指定该事务管理器 是对哪个数据源进行事务控制
-->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置启动基于注解的声明式事务管理功能-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
测试
public class TxTest {
@Test
public void queryPriceByIdTest() {
//获取到容器
ApplicationContext ioc =
new ClassPathXmlApplicationContext("tx_ioc.xml");
GoodsDao goodsDao = ioc.getBean(GoodsDao.class);
Float price = goodsDao.queryPriceById(1);
System.out.println("id=100 的price=" + price);
}
@Test
public void updateBalance() {
//获取到容器
ApplicationContext ioc =
new ClassPathXmlApplicationContext("tx_ioc.xml");
GoodsDao goodsDao = ioc.getBean(GoodsDao.class);
goodsDao.updateBalance(1, 1.0F);
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("减少库存成功...");
}
//测试用户购买商品业务
@Test
public void buyGoodsTest() {
//获取到容器
ApplicationContext ioc =
new ClassPathXmlApplicationContext("tx_ioc.xml");
GoodsService goodsService = ioc.getBean(GoodsService.class);
goodsService.buyGoods(1, 1, 1);
}
//测试用户购买商品业务
@Test
public void buyGoodsByTxTest() {
//获取到容器
ApplicationContext ioc =
new ClassPathXmlApplicationContext("tx_ioc.xml");
GoodsService goodsService = ioc.getBean(GoodsService.class);
goodsService.buyGoodsByTx(1, 1, 1);//这里我们调用的是进行了事务声明的方法
}
}
4.声明式事务机制-Debug
3.事务的传播机制
1.事务的传播机制说明
-
当有多个事务处理并存时,如何控制?
-
比如用户去购买两次商品(使用不同的方法), 每个方法都是一个事务,那么如何控制呢?
-
这个就是事务的传播机制,看一个具体的案例(如图)
2.事务传播机制种类
● 事务传播的属性/种类一览图
● 事务传播的属性/种类机制分析,重点分析了 REQUIRED 和 REQUIRED_NEW 两种事务 传播属性, 其它知道即可(看上图)
● 事务的传播机制的设置方法
● REQUIRES_NEW 和 REQUIRED 在处理事务的策略
- 如果设置为 REQUIRES_NEW buyGoods2 如果错误,不会影响到 buyGoods()反之亦然,即它们的事务是独立的.
- 如果设置为 REQUIRED buyGoods2 和 buyGoods 是一个整体,只要有方法的事务错误,那么两个方法都不会执行成功.!
3.事务的传播机制-应用实例
GoodsDao新增第二套方法
/**
* 根据商品id,返回对应的价格
* @param id
* @return
*/
public Float queryPriceById2(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 updateBalance2(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 updateAmount2(Integer goods_id, int amount){
String sql = "UPDATE goods_amount SET goods_num=goods_num-? Where goods_id=?";
jdbcTemplate.update(sql, amount , goods_id);
}
GoodsService
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void buyGoodsByTx(int userId, int goodsId, int amount) {
//输出购买的相关信息
System.out.println("用户购买信息 userId=" + userId
+ " goodsId=" + goodsId + " 购买数量=" + amount);
//1.得到商品的价格
Float price = goodsDao.queryPriceById(userId);
//2. 减少用户的余额
goodsDao.updateBalance(userId, price * amount);
//3. 减少库存量
goodsDao.updateAmount(goodsId, amount);
System.out.println("用户购买成功~");
}
/**
* 这个方法是第二套进行商品购买的方法
*
* @param userId
* @param goodsId
* @param amount
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void buyGoodsByTx2(int userId, int goodsId, int amount) {
//输出购买的相关信息
System.out.println("用户购买信息 userId=" + userId
+ " goodsId=" + goodsId + " 购买数量=" + amount);
//1.得到商品的价格
Float price = goodsDao.queryPriceById2(userId);
//2. 减少用户的余额
goodsDao.updateBalance2(userId, price * amount);
//3. 减少库存量
goodsDao.updateAmount2(goodsId, amount);
System.out.println("用户购买成功~");
}
MultiplyService
@Service
public class MultiplyService {
@Resource
private GoodsService goodsService;
/**
* 1. multiBuyGoodsByTx 这个方法 有两次购买商品操作
* 2. buyGoodsByTx 和 buyGoodsByTx2 都是声明式事务
* 3. 当前buyGoodsByTx 和 buyGoodsByTx2 使用的传播属性是默认的 REQUIRED [这个含义老师前面讲过了
* 即会当做一个整体事务进行管理 , 比如buyGoodsByTx方法成功,但是buyGoodsByTx2() 失败,会造成 整个事务的回滚
* 即会回滚buyGoodsByTx]
*
* 4. 如果 buyGoodsByTx 和 buyGoodsByTx2 事务传播属性修改成 REQUIRES_NEW
* , 这时两个方法的事务是独立的,也就是如果 buyGoodsByTx成功 buyGoodsByTx2失败
* , 不会造成 buyGoodsByTx回滚.
*
*/
@Transactional
public void multiBuyGoodsByTx() {
goodsService.buyGoodsByTx(1, 1, 1);
goodsService.buyGoodsByTx2(1, 1, 1);
}
}
4.事务的隔离级别
1.事务隔离级别说明
● 事务隔离级别说明
- 默认的隔离级别, 就是 mysql 数据库默认的隔离级别 一般为 REPEATABLE_READ
- 看源码可知 Isolation.DEFAULT 是 :Use the default isolation level of the underlying datastore
- 查看数据库默认的隔离级别 SELECT @@global.tx_isolation
2.事务隔离级别的设置和测试
- 修改 GoodsService.java , 先测默认隔离级别,增加方法 buyGoodsByTxISOLATION()
/**
* 1. 在默认情况下 声明式事务的隔离级别是 REPEATABLE_READ
* 2. 我们将buyGoodsByTxISOLATION的隔离级别设置为 Isolation.READ_COMMITTED
* ,表示只要是提交的数据,在当前事务是可以读取到最新数据
*/
@Transactional(isolation = Isolation.READ_COMMITTED)
public void buyGoodsByTxISOLATION() {
//查询两次商品的价格
Float price = goodsDao.queryPriceById(1);
System.out.println("第一次查询的price= " + price);
Float price2 = goodsDao.queryPriceById(1);
System.out.println("第二次查询的price= " + price2);
}
3.事务的超时回滚
● 基本介绍
- 如果一个事务执行的时间超过某个时间限制,就让该事务回滚。
- 可以通过设置事务超时回顾来实现
● 基本语法
● 超时回滚-代码实现
/**
* 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("用户购买信息 userId=" + userId
+ " goodsId=" + goodsId + " 购买数量=" + amount);
//1.得到商品的价格
Float price = goodsDao.queryPriceById2(userId);
//2. 减少用户的余额
goodsDao.updateBalance2(userId, price * amount);
//模拟超时
System.out.println("=====超时开始4s=====");
try {
//睡眠4秒
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("=====超时结束4s=====");
//3. 减少库存量
goodsDao.updateAmount2(goodsId, amount);
System.out.println("用户购买成功~");
}
测试方法
//测试timeout 属性
@Test
public void buyGoodsByTxTimeoutTest() {
//获取到容器
ApplicationContext ioc =
new ClassPathXmlApplicationContext("tx_ioc.xml");
GoodsService goodsService = ioc.getBean(GoodsService.class);
goodsService.buyGoodsByTxTimeout(1,1,1);
}