Spring05_Spring事务

发布时间 2023-04-19 19:20:37作者: Purearc

一、JdbcTemplate 工具

​ JdbcTemplate 类是 Spring 框架提供一个用于操作数据库的模板类,JdbcTemplate 类支持声明式事务管理。该类提供如下方法来执行数据库操作。

​ 1、queryForObject 查询单个对象

queryForObject(String sql,RowMapper mapper,Object[] args)

​ 2、query 查询集合

List query(String sql,RowMapper mapper,Object[] args)

​ 3、update 增加、删除、修改

int update(String sql, Object[] args)

​ 4、batchUpdate 批量增加、删除、修改

int[] batchUpdate(String sql, List list)

package com.qlu.pojo;

import lombok.Data;
import lombok.experimental.Accessors;

import java.util.Date;

@Data
@Accessors(chain = true)
public class Book {
    private Long id;
    private String bookName;
    private String bookAuthor;
    private Date createTime;
    private Date updateTime;
}

​ 相同包的放到一起了,大家当个工具就行,毕竟都用框架了,集成其他的比这方便。

package com.qlu.service;

import com.qlu.pojo.Book;

public interface BookService {

    void add(Book book);

    void add(Integer value);

    void delete();

    void update(Book book);

    void find();

}

package com.qlu.service.impl;

import com.qlu.pojo.Book;
import com.qlu.service.BookService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

@Service("bookService")
public class BookServiceImpl implements BookService {

    private JdbcTemplate jdbcTemplate;

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public void add(Book book) {
        System.out.println("添加图书");
        String sql = "insert into book (book_name,book_author,create_time,update_time) " +
                "values(?,?,?,?)";
        Object[] args = {book.getBookName(),book.getBookAuthor(),book.getCreateTime(),book.getUpdateTime()};
        int result = jdbcTemplate.update(sql, args);
        System.out.println(result>0?"成功":"失败");
    }

    @Override
    public void add(Integer value) {
        System.out.println("有参数的add方法  添加图书");
    }

    @Override
    public void delete() {
        System.out.println("删除图书");
    }

    @Override
    public void update(Book book) {
        System.out.println("修改图书");
        String sql = "update book  set book_name =?,book_author=?,update_time=? where id = ?";
        Object[] args = {book.getBookName(),book.getBookAuthor(),book.getUpdateTime(),book.getId()};
        int result = jdbcTemplate.update(sql, args);
        System.out.println(result>0?"成功":"失败");
    }

    @Override
    public void find() {
        System.out.println("查找图书");
    }
}

​ 就用了一个 @Service 注解,其他的 bean 都是直接配置在 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:p="http://www.springframework.org/schema/p"
       xmlns:c="http://www.springframework.org/schema/c"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/util
        http://www.springframework.org/schema/util/spring-util.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="bookService" class="com.qlu.service.impl.BookServiceImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"></property>
    </bean>


    <!-- 配置 Spring 的事务管理器 -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- 配置 C3P0 数据库链接池 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl"
                  value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=UTF-8&amp;useUnicode=true"/>
        <property name="user" value="root"/>
        <property name="password" value="a.miracle"/>
        <property name="maxPoolSize" value="50"/>
        <property name="minPoolSize" value="20"/>
        <property name="initialPoolSize" value="20"/>
        <property name="maxIdleTime" value="200"/>
    </bean>

    <!--配置jdbc模板工具类-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

</beans>

​ 因为使用到了数据库连接池的配置,所以需要以下东西

<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

​ 到这儿就差不多了,反正我不想写了。

二、Spring 事务

​ 此部分用到了上面的 Jdbc模板工具类

(一)事务概述

​ 事务是恢复和并发控制的基本单位。即一组操作要么全都不做,要不就全都做。事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性

​ 1.原子性:一个事务不可分割,里面的操作只能选择全做或者全不做。

​ 2.一致性:即事务是把数据库从一个一致的状态变到另一个一致的状态。

​ 3.隔离性:事务的执行不能相互干扰。

​ 4.持久性:事务一旦提交对数据库的改变就是永久的。

(二)事务管理举例

​ 我们以银行赚钱为实例,创建如下数据表并且插入数据如下

CREATE TABLE `money` (
  `id` bigint NOT NULL,
  `person_name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `sum` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

image-20230419163504603

​ 依然配置用到了c3p0等数据库连接池,可以参少上面 标记 处,下面给出 dao、pojo、service 层的逻辑。

package com.qlu.service;

public interface MoneyService {
    /**
     * 转钱
     * @param fromId 从哪儿
     * @param toId 转给谁
     * @param money 转账金额
     * @return
     */
    boolean changeMoney(int fromId,int toId,int money);
}

​ 下面是数据持久层,在数据持久层的 changeMoney 有两个参数,你可以认为是改变某个 id 的 sum 值(sum表示总的金额)。

package com.qlu.dao;

public interface MoneyDao {
    /**
     * 根据id修改金额
     * @param id
     * @param money
     * @return
     */
    int changeMoney(int id,int money);
}

@Repository("moneyDao")
public class MoneyDaoImpl implements MoneyDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public int changeMoney(int id, int money) {
        String sql = "update money set sum = sum + ? where id = ?";
        Object[] args = {money,id};
        return jdbcTemplate.update(sql,args);
    }
}

​ 下面是业务逻辑层,service 中的 changeMoney 表示从哪个 id 转移到哪个 id ,转多少钱。

package com.qlu.service;

public interface MoneyService {
    /**
     * 转钱
     * @param fromId 从哪儿
     * @param toId 转给谁
     * @param money 转账金额
     * @return
     */
    boolean changeMoney(int fromId,int toId,int money);
}

package com.qlu.service.impl;

@Service("moneyService")
public class MoneyServiceImpl implements MoneyService {

    @Autowired
    private MoneyDao moneyDao;

    @Override
    public boolean changeMoney(int fromId, int toId, int money) {
        /**
         * from 减少
         * to 增加
         */
        //张三减少
        int result1 = moneyDao.changeMoney(fromId,-1*money);
        //李四增加
        int result2 = moneyDao.changeMoney(toId,money);
        return (result1>0 && result2>0 ? true:false);
    }
}

​ 通过 Ctrl + Shift + T 生成测试业务逻辑的测试类

@Test
public void textTx1() {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-beans.xml");
    MoneyService moneyService = applicationContext.getBean(MoneyService.class);
    boolean result = moneyService.changeMoney(1, 2, 100);
    System.out.println(result);
}

​ 成功后我们可以看到数据库的变化

image-20230419163547954

​ 显然在这种正常的情况下没有错的,我们可以自定义一个 /by zero 异常在张三的钱减少和李四的钱增加之间来触发这个异常,

image-20230419165613851

​ 此时张三的钱没有了,但是李四的钱却没有增加,这一点类似于我们在多线程访问共享资源的情况一样,不给共享资源上锁,就可能出现两个线程信息不同步的情况。

image-20230419165741721

​ 为解决这个情况,Spring 提供了 TransactionManger,该功能依赖于 AOP,下面的配置都是老几样了,无非就是相当于把这个事务管理器当成一个增强函数来用,然后定义切面 aop-config,里面定义切点和增强组成切面,需要注意的是这个 aop:aspect 得换成我们事务管理的 aop:advisor

xmlns:tx="http://www.springframework.org/schema/tx"

http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd"

    <!-- 配置 Spring 的事务管理器 -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置事务的消息通知 -->
    <tx:advice id="tAdvice" transaction-manager="txManager">
        <tx:attributes>
            <!-- 配置消息通知类型列表,监控的方法由上而下开始匹配,以及事务的传播特性 -->
            <tx:method name="find*" propagation="SUPPORTS"/>
            <tx:method name="query*" propagation="SUPPORTS"/>
            <tx:method name="add*"/>
            <tx:method name="delete*"/>
            <tx:method name="update*"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <aop:config>
        <aop:pointcut id="pointcut" expression="execution(* com.qlu.service.impl.*.*(..))"/>
    <!--配置切面,将消息通知切入到Service层-->
        <aop:advisor advice-ref="tAdvice" pointcut-ref="pointcut"></aop:advisor>
    </aop:config>

​ 关于配置事务的消息通知点这里

​ 再来跑一下,那自然还是有异常抛出,但是重点关注于数据库的数据,和上面可以说是完全一样,事务具有原子性(不可分割),这个减少 money 和增加 money 要不都做,要不都别做。

image-20230419183701278

​ 难办?那就都别办了,在张三减少了之后触发异常,程序被迫中止,那已经完成的 “张三减少” 也应该回滚成为没有减少的状态。image-20230419184349218

​ 针对可能抛出异常的代码块,我们可能会选择使用 try...catch 环绕捕获异常

    @Override
    public boolean changeMoney(int fromId, int toId, int money) {
        /**
         * from 减少
         * to 增加
         */
        try {
            //张三减少
            int result1 = moneyDao.changeMoney(fromId,-1*money);

            int exception = 1/0;
            //李四增加
            int result2 = moneyDao.changeMoney(toId,money);
            return (result1>0 && result2>0 ? true:false);
        } catch (Exception e) {
            e.printStackTrace();
            
            //throw e; 
            return false;
        }
    }

image-20230419184936840

​ 此时在这个业务内部就完成了对异常的处理(捕获到打印出信息),那在外部事务管理就不会捕获到这个异常。

image-20230419184951542

​ 解决方法:再把这个异常扔出来就行了,这样外部事务管理就可以捕获到异常信息

image-20230419185352163

​ 数据库没有变化(忘了应该在测试的 before 里面加个当前系统时间的)

image-20230419185400653

(三)Spring 事务的传播

​ 这个配置事务的消息通知就把它当成增强函数看就行,毕竟事务管理就是基于AOP的,你AOP能用注解事务管理也肯定是能用的。

tx:attributes 内的 tx:method 有如下属性:

​ 1、name 属性:表示在执行哪些方法时,会触发消息通知,name 的属性值支持模糊匹配,例如:find*,表示 调用以 find 开头的方法时都会触发消息通知。

​ 2、propagation 属性:表示事务的传播特性:

​ 属性值“SUPPORTS”表示支持当前事务,如果当前没有事务, 就使用无事务机制;

​ 属性值“REQUIRED”支持当前事务,有事务就加入到该事务中,如果没有事务则新建事务,默认值;

​ 属性值 REQUIRES_NEW 如果有当前事务,则挂起当前事务,新建新事务,反之,直接新建事务;

​ 属性值 MANDATORY 支持当前事务,如果没有事务,则抛出异常。

​ 3、timeout 属性:事务超时时间 默认值是-1,-1 表示不超时,以秒为单位。

​ 4、read-only 属性:事务是否只读,默认值是 false

​ 关于事务的传播机制:

传播机制 说明
REQUIRED 如果当前没有事务,就创建一个事务,如果已经存在事务,就加入到这个事务。当前传播机制也是spring默认传播机制
REQUIRES_NEW 新建事务,如果当前存在事务,就抛出异常。
SUPPORTS 支持当前事务,如果当前没有事务, 就使用无事务机制。
NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
MANDATORY 支持当前事务,如果没有事务,则抛出异常。
NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
NESTED 如果当前存在事务,则在嵌套的事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作