likes
comments
collection
share

Spring 声明式事务

作者站长头像
站长
· 阅读数 48

Spring 声明式事务

事务分类

1.编程式事务:

示意代码, 传统方式

Connection connection = JdbcUtils.getConnection();
try {
//1. 先设置事务不要自动提交
connection.setAutoCommint(false);
//2. 进行各种 crud
//多个表的修改,添加 ,删除
//3. 提交
connection.commit();
} catch (Exception e) {
//4. 回滚
conection.rollback();
}

 

声明式事务-使用实例

需求说明-用户购买商品

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

1. 通过商品 id 获取价格. 2. 购买商品(某人购买商品,修改用户的余额) 3. 修改库存量 4. 其实大家可以看到,这时,我们需要涉及到三张表商品表,用户表,商品存量表。 应该使用事务处理

解决方案分析

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

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

2. 使用 Spring 的声明式事务处理,

可以将上面三个子步骤分别写成一个方法,然后统一管理.

[这个是 Spring 很牛的地方,在开发使用的很多,优点是无代码冗余,效率高,扩展方便,缺点是理解较困难]==> 底层使用 AOP (动态代理+动态绑定+反射+注解)

声明式事务使用-代码实现

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);

创建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);
    }
}

创建 src\tx_ioc.xm

<?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.spring.tx.dao"/>

    <!--引入外部的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>

创建TxTest类

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("减少库存成功...");
    }
}

 创建GoodsService类

编写方法,验证不使用事务就会出现数据不一致现象.

@Service
public class GoodsService {
    @Autowired
    private GoodsDao goodsDao;
    /**
     * 购买商品[没有使用事务]
     * @param user_id
     * @param goods_id
     * @param num
     */
    public void buyGoods(int user_id, int goods_id, int num) {
        //查询到商品价格
        Float goods_price = goodsDao.queryPriceById(goods_id);
        //购买商品,减去余额
        goodsDao.updateBalance(user_id, goods_price * num);
        // //: 模拟一个异常, 会发生数据库数据不一致现象
        // int i = 10 / 0;
        //更新库存
        goodsDao.updateAmount(goods_id, num);
    }
}

 修改tx_ioc.xml, 加入对 Service 的扫描

<?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.spring.tx.dao"/>
    <context:component-scan base-package="com.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>

测试 TxTest类

@Test
public void buyGoodsTest() {
    ApplicationContext ioc = new ClassPathXmlApplicationContext("tx_ioc.xml");
        GoodsService bean = ioc.getBean(GoodsService.class);
        bean.buyGoods(1, 2, 1);
        System.out.println("====购买商品成功====");
}

修改 GoodsService.java, 增加测试方法,加入声明式事务注解

@Transactional
public void buyGoodsByTx(int user_id, int goods_id, int num) {
            //查询到商品价格
        Float goods_price = goodsDao.queryPriceById(goods_id);
        //购买商品,减去余额
        goodsDao.updateBalance(user_id, goods_price * num);
        // // 模拟一个异常, 会发生数据库数据不一致现象
        // int i = 10 / 0;
        //更新库存
        goodsDao.updateAmount(goods_id, num);
        }

 修改 TxTest.java, 增加测试方法, 对声明式事务进行测试,看看是否保证了数据一致性

/**
* 测试购买商品(使用了声明式事务)
*/
@Test
public void buyGoodsByTxTest() {
    ApplicationContext ioc = new ClassPathXmlApplicationContext("tx_ioc.xml");
        GoodsService bean = ioc.getBean(GoodsService.class);
        //使用 buyGoodsByTx()
        bean.buyGoodsByTx(1, 2, 1);
        System.out.println("====购买商品成功====");
}

 声明式事务机制-Debug

事务的传播机制

事务的传播机制说明

1. 当有多个事务处理并存时,如何控制? 2. 比如用户去购买两次商品(使用不同的方法), 每个方法都是一个事务,那么如何控制呢? 3. 这个就是事务的传播机制,看一个具体的案例(如图)

Spring 声明式事务

 Spring 声明式事务

事务传播机制种类

● 事务传播的属性/种类一览图  

Spring 声明式事务

● 事务传播的属性/种类机制分析,

重点分析了 REQUIRED 和 REQUIRED_NEW 两种事务 传播属性, 其它知道即可(看上图)

Spring 声明式事务

Spring 声明式事务

 ● 事务的传播机制的设置方法

 Spring 声明式事务

● REQUIRES_NEW 和 REQUIRED 在处理事务的策略 

Spring 声明式事务

 1. 如果设置为 REQUIRES_NEW

buyGoods2 如果错误,不会影响到 buyGoods()反之亦然,即它们的事务是独立的. 

2. 如果设置为 REQUIRED

buyGoods2 和 buyGoods 是一个整体,只要有方法的事务错误,那么两个方法都不会执行成功.!

事务的传播机制-应用实例

● 事务的传播机制需要说明 1. 比如用户去购买两次商品(使用不同的方法), 每个方法都是一个事务,那么如何控制呢? =>这个就是事务的传播机制 2. 看一个具体的案例(用 required/requires_new 来测试):

修改 GoodsDao.java, 增加方法

public class 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.java 增加 buyGoodsByTx02(), 使用默认的传播机制

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


    @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("用户购买成功~");

    }

@Transactional
    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类 

解读

1. multiBuyGoodsByTx 这个方法 有两次购买商品操作 2. buyGoodsByTx 和 buyGoodsByTx2 都是声明式事务

3. 当前buyGoodsByTx 和 buyGoodsByTx2 使用的传播属性是默认的 REQUIRED [这个含义前面讲过了 即会当做一个整体事务进行管理 , 比如buyGoodsByTx方法成功,但是buyGoodsByTx2() 失败,会造成 整个事务的回滚 即会回滚buyGoodsByTx 4. 如果 buyGoodsByTx 和 buyGoodsByTx2 事务传播属性修改成 REQUIRES_NEW 这时两个方法的事务是独立的,也就是如果 buyGoodsByTx成功 buyGoodsByTx2失败, 不会造成 buyGoodsByTx回滚.

@Service
public class MultiplyService {
    @Resource
    private GoodsService goodsService;


    
    @Transactional
    public void multiBuyGoodsByTx() {

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

测试 TxTest.java,

 可以验证:为 REQUIRED buyGoodsByTx 和 buyGoodsByTx02 是整体, 只要有方法的事务错误,那么两个方法都不会执行成功

@Test
public void buyGoodsByMulTxTest() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("tx_ioc.xml");
MultiplyTxService bean = ioc.getBean(MultiplyTxService.class);
bean.multiTxTest();
System.out.println("------ok--------");
}

 

 故意写错

Spring 声明式事务

修 改 GoodsService.java ,

将 传 播 机 制 改 成 REQUIRES_NEW 可 以 验 证 : 设 置 为 REQUIRES_NEW

buyGoodsByTx 如果错误,不会影响到 buyGoodsByTx02()反之亦然,也就 是说它们的事务是独立的 ,

将二个方法的 @Transactional修改为下面的这种形式   完成测试

@Transactional(propagation = Propagation.REQUIRES_NEW)

 事务的隔离级别

事务隔离级别说明

● 事务隔离级别的概念在这篇博客

【数据库和jdbc】

Spring 声明式事务 

● 事务隔离级别说明

1. 默认的隔离级别, 就是 mysql 数据库默认的隔离级别 一般为 REPEATABLE_READ 2. 看源码可知 Isolation.DEFAULT 是 :Use the default isolation level of the underlying datastore

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

事务隔离级别的设置和测试

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

Spring 声明式事务

测试事务的隔离级别

1. 默认的隔离级别, 就是 mysql 数据库默认的隔离级别 一般为 REPEATABLE_READ

2. 看源码可知 Use the default isolation level of the underlying datastore public enum Isolation { <p> Use the default isolation level of the underlying datastore.  All other levels correspond to the JDBC isolation levels. @see java.sql.Connection DEFAULT(TransactionDefinition.ISOLATION_DEFAULT)}

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

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void buyGoodsByTxISOLATION(int user_id, int goods_id, int num) {
//查询到商品价格
Float goods_price = goodsDao.queryPriceById(goods_id);
System.out.println("第一次读取的价格 = " + goods_price);
//测试一下隔离级别,在同一个事务中,查询一下价格
goods_price = goodsDao.queryPriceById(goods_id);
System.out.println("第二次读取的价格 = " + goods_price);
}

Spring 声明式事务

 完成测试

修改TxTest增 加测试方法, 默认隔离级别 下, 两次读取到的价格是一样的,不会受到 SQLyog 修改影响

@Test
public void buyGoodsByTxISOLATIONTest() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("tx_ioc.xml");
GoodsService bean = ioc.getBean(GoodsService.class);
bean.buyGoodsByTxISOLATION(1, 1, 1);
System.out.println("------ok--------");
}

修改 GoodsService.java , 测试 READ_COMMITTED 隔离级别情况

Spring 声明式事务

Spring 声明式事务

完成测试

使用前面已经创建好的测试方法, 在 READ_COMMITTED 隔离级别 下, 两次 读取到的价格会受到 SQLyog 修改

 

@Test
public void buyGoodsByTxISOLATIONTest() {
    ApplicationContext ioc = new ClassPathXmlApplicationContext("tx_ioc.xml");
        GoodsService bean = ioc.getBean(GoodsService.class);
        bean.buyGoodsByTxISOLATION(1, 1, 1);
        System.out.println("------ok--------");
}

事务的超时回滚

● 基本介绍

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

● 基本语法

Spring 声明式事务 

超时回滚-代码实现

修改 GoodsService.java ,增加 buyGoodsByTxTimeout()

@Transactional(timeout = 2)
public void buyGoodsByTxTimeout(int user_id, int goods_id, int num) {
//查询到商品价格
Float goods_price = goodsDao.queryPriceById02(goods_id);
//购买商品,减去余额
goodsDao.updateBalance02(user_id, goods_price * num);
System.out.println("====超时 start====");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("====超时 end====");
//更新库存
goodsDao.updateAmount02(goods_id, num);
}

测试 TxTest.java, 增加测试方法

Spring 声明式事务

@Test
public void buyGoodsByTxTimeoutTest() {
    ApplicationContext ioc = new ClassPathXmlApplicationContext("tx_ioc.xml");
        GoodsService bean = ioc.getBean(GoodsService.class);
        bean.buyGoodsByTxTimeout(1, 1, 1);
        System.out.println("------ok--------");
}

注意

上面所有的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.spring.tx.dao"/>
    <context:component-scan base-package="com.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>
转载自:https://juejin.cn/post/7240636469462777914
评论
请登录