从零开始学Spring-事务与框架集成


1. Spring与事务

1.1 事务概述

什么是事务

  • 在一个业务流程当中,通常需要多条DML语句共同联合才能完成,这多条DML语句必须同时成功或者同时失败,这样次才能保证数据的安全,事务Transaction(tx)

事务的四个处理过程

  • 开启事务
  • 执行核心业务代码
  • 提交事务
  • 回滚事务

事务的四个特性

  • A原子性:事务是最小的工作单元,不可再分
  • C一致性:事务要求要么同时成功,要么同时失败,事务前和事务后的总量不变
  • I隔离性:事务和事务之间因为有隔离性,才可以保证互不干扰
  • D持久性:持久性是事务结束的标志

1.2 Spring事务管理器接口

PlatformTransactionManager接口:Spring事务管理器的核心接口,在Spring中有两个实现

  • DataSourceTransactionManager:支持JdbcTemplate、Mybatis、Hibernate等事务管理
  • JtaTransactionManager:支持分布式事务管理

声明式事务之注解的实现方式

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource"/>
</bean>
?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 http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

配置事务管理命名空间

然后在Spring配置文件中配置事务注解驱动器

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/orm"/>
    <property name="username" value="root"/>
    <property name="password" value="root"/>
</bean>

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>

1.3 事务的传播行为

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";
    @AliasFor("value")
    String transactionManager() default "";
    Propagation propagation() default Propagation.REQUIRED;//事务的传播行为
    Isolation isolation() default Isolation.DEFAULT;//事务的隔离级别  
    int timeout() default -1;//配置事务的超时时间
    boolean readOnly() default false;//只读事务
    Class<? extends Throwable>[] rollbackFor() default {};//出现什么情况下回滚?
    String[] rollbackForClassName() default {};/
    Class<? extends Throwable>[] noRollbackFor() default {};//出现什么情况下不回滚?
    String[] noRollbackForClassName() default {};
}
public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);

    private final int value;

    private Propagation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

当事务中嵌套了其他的事务的时候,这时候就可能会产生事务的传播问题,七种传播行为

  • REQUIRED:支持当前事务,如果不存在就新建一个,也就是说,原本如果有事务的话,那么就加入到原来的那个事务中去执行,否则的话就自己新建一个事务,比如说A()去调用B(),如果B()失败了,那么A之前执行的就会回滚,如果执行完B()了而A()失败了,那么都回滚
  • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务的方式执行,如果原本有事务的话,那么就加入,没有的话就不管了
  • MANDATORY:必须运行在一个事务中,如果当前没有事务发生,就抛出一个异常
  • REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,那么就将这个存在的事务挂起的,开启的新事务和之前的事务不存在嵌套关系,之前的事务被挂起了,比如说A()去调用B(),而此时B()失败了,而A()不会受到影响而回滚
  • NOT_SUPPORTED:以非事务的方式运行,如果有事务存在,则挂起当前的事务
  • NEVER:以非事务的方式运行,如果有事务存在,那么就抛出异常
  • NESTED:如果有一个事务在进行中,则该方法应该允许在一个嵌套式事务中,被嵌套的事务可以独立于外层事务进行提交或者回滚,如果外层事务不存在,行为就像REQUIRED一样

1.4 事务的隔离级别

数据库中读取数据存在三大问题

  • 脏读:读取到还没有提交到数据库的数据
  • 不可重复读:在同一个事务中,第一次和第二次读取到的数据是不一样的
  • 幻读:读取到的数据是假的

简单理解一下以上三种问题

  • 脏读就是读取到了缓存当中的数据,当事务A和事务B同时操作内存,对内存进行写入的时候,A写入了内存,但是还没有提交,但是A的时间片到了,让B去执行,这时候B同样会去读缓存,假设A回滚了,A写入的数据作废,那么这时候就产生了脏读的问题
  • 不可重复读:实际上不可重复读是程序正确执行的表现,但是可能会影响程序后续的执,这个会有什么危害呢?我们说数据库有两个区域,第一个区域是每个事务所独占的工作空间,这个工作空间每个事务之间相互独立,比如说当A执行读取,它会将第二个区域,也就是磁盘区域中的磁盘块通过bus传输到工作空间中待用,假设A执行了两次读取,第一次读取了a变量为100,第二次读取了a变量还是100,然而这时候A的时间片到,到B执行,B修改了a变量为300,然而由于A后续不再执行读取操作,因此这时候它一直认为a变量为100,这时候依然会按照 a=100来进行处理,最终可能会导致程序的错误。
  • 幻读:幻读就是读取到的数据与实际的操作是不一致的,比如说事务A先delete了表中的所有数据,然后A挂起,B去执行插入了,然后A再执行查询,发现又多了一条数据,那么这时候就会产生问题,一次事务中前后数据量发生变化
  • 要特别注意不可重复读和幻读的区别
    • 不可重复读指的是并发更新的时候,另一个事务前后查询相同的数据的时候,数据不符合预期
    • 幻读指的是并发新增、删除这种会产生数量变化的操作的时候,另一个事务前后查询相同数据的时候不符合预期
    • 如果还是不懂可以看下面的

解决这些问题本质上就是并发的问题,因此可以通过CAS,悲观锁等方式进行解决

  • 最简单的悲观锁就是串行化执行,让这些线程挨个排队,不允许并发执行操作数据

事务隔离级别包括四个级别

  • 读未提交:READ_UNCOMMITTED:存在脏读的问题,也就是说能够读取到其他事务没有提交的数据
  • 读提交:READ_COMMITTED:解决了脏读的问题,其他事务提交之后才能读取到,但是存在不可重复读的问题
  • 可重复读:REPEATABLE_READ:解决了不可重复读,可以达到可重复读的效果,只要当前事务不结束,读取到的数据一直都是一样的,但是存在幻读的问题(mysql)
  • 串行化:SERIALIZAABLE:解决了幻读的问题,事务排队执行,不支持并发

重点理解可重复读的实现,可重复读的实现,可以对已经查询到的结果集进行加锁,如果加锁完了之后,其他事务就不能对结果集进行操作,然后读取这些数据的时候一直就会读取到这个数据。

但是假设,B插入了数据,插入数据是不会造成约束的,假设B插入的数据有符合A事务所查询的条件的话,那么A事务后续就会查询出这些新的数据,这样的话,又造成了相同的查询语句,不同的查询结果,为了更加细粒度的区分问题以及提出解决方案,因此为这些问题划分为不可重复读和幻读

public enum Isolation {
    DEFAULT(-1),
    READ_UNCOMMITTED(1),
    READ_COMMITTED(2),
    REPEATABLE_READ(4),
    SERIALIZABLE(8);

    private final int value;

    private Isolation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

1.5 事务常见设置

事务超时

@Transactional(timeout = 10)

以上代码标识设置事务的超时时间为10s

表示超过10s后如果该事务中的所有DML语句还没有执行完毕的话,最终结果会选择回滚

默认值为-1,表示没有时间限制

这里有个坑,事务的超时时间指的是哪段时间

在当前事务中,最后一条DML语句执行完毕之前的时间,而后续的业务代码是不算的

只读事务

将当前的事务设置为只读事务,在该事务执行过程中只允许select语句执行,而delete insert update都不可以执行

这个特性的作用是:启动spring的优化策略,提高select语句的执行效率,这是最主要的。

如果该事务中确实没有增伤查改的操作,建议设置为只读事务

遇到哪些异常时回滚

遇到这个异常或者异常的子类异常,都回滚

@Transactional(rollbackFor = {NullPointerException.class,ClassCastException.class})

遇到哪些异常时不回滚

@Transactional(noRollbackFor = {NullPointerException.class,ClassCastException.class})

1.6 事务的全注解式开发

@Configuration
@ComponentScan(value = {"com.dao","com.service"})
@EnableTransactionManagement
public class SpringConfig {
    //纳入SpringIoc容器进行管理,返回的对象就是bean了
    @Bean(name = "dataSource")
    public DruidDataSource getDataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql//localhost:3306/orm");
        return dataSource;
    }
    //传参原理:Spring在生产这个Bean的时候,会扫描你的方法中的参数,然后看你方法中的参数有没有在IoC容器中
    @Bean(name = "jdbcTemplate")
    public JdbcTemplate getJdbcTemplate(DataSource dataSource){//会自动注入
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }
    //采用Bean注解进行标记
    @Bean(name = "txManager")
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }

}
  • 外部的Bean通过这种方式进行注入
  • 内部的Bean通过普通Compoent进行注入

1.6 基于XML开启事务

<!--配置通知(具体的增强代码),配置切面-->
<tx:advice id="txAdvice">
    <!--配置通知的相关属性-->
    <tx:attributes>
        <tx:method name="addStudent" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

<aop:config>
    <aop:pointcut id="txPointCut" expression="execution(* com.service..*(..))"/>
    <!--切面=通知+切点-->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>

2. Spring整合JUnit

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.2.10.RELEASE</version>
</dependency>
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:Spring.xml")
public class Test01 {
    @Resource(name = "studentService")
    private StudentService service;
    
}

3. Spring集成Mybatis

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>3.0.1</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.10</version>
</dependency>
  • 编写jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/orm
jdbc.username=root
jdbc.password=root
  • 编写spring
<?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:tx="http://www.springframework.org/schema/tx"
       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/tx http://www.springframework.org/schema/tx/spring-tx.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--配置通知(具体的增强代码),配置切面-->
    <tx:advice id="txAdvice">
        <!--配置通知的相关属性-->
        <tx:attributes>
            <tx:method name="addStudent" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>

    <aop:config>
        <aop:pointcut id="txPointCut" expression="execution(* com.service..*(..))"/>
        <!--切面=通知+切点-->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
    </aop:config>

    <bean id = "studentService" class="com.service.impl.StudentServiceImpl"/>
    <!--配置信息-->
    <!--引入外部信息-->
    <context:property-placeholder location="jdbc.properties"/>
    <!--配置数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <!--配置SqlSessionFactoryBean-->
    <bean class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--数据源-->
        <property name="dataSource" ref="dataSource"/>
        <!--指定mybatis核心配置文件-->
        <property name="configLocation" value="mybatis-config.xml"/>
        <!--别名-->
        <property name="typeAliasesPackage" value="com.domain"/>
    </bean>
    <!--配置mapper扫描配置器-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.dao"/>
    </bean>
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--启用事务注解-->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    
</beans>

4. Spring框架的八大设计模式

4.1 简单工厂模式

BeanFactorygetBean()方法,通过唯一标识来获取Bean对象,是简单工厂模式,也就是静态工厂模式

4.2 工厂方法模式

FactoryBean是典型的工厂模式,在配置文件中通过factory-method来指定工厂方法,是一个实例方法

4.3 单例模式

Spring用的是双重判断加锁的单例模式

4.4 代理模式

典型案例是AOP

4.5 装饰器模式

JavaSE中的IO流是非常经典的装饰器模式

Spring在配置DataSource的时候,这些DataSource可能是各种不同类型的,Spring想要达到在尽可能少的修改原有类代码下的情况下,做到动态切换不同的数据源,这时候就使用到了装饰器模式

Spring根据每次的请求的不同,将dataSource属性设置成不同的数据源,以达到切换数据源的目的

Spring中类名中带有DecoratorWrapper单词的类,都是装饰器模式

4.6 观察者模式

定义对象间的一对多的关系,当一个对象的状态发生改变的时候,所有依赖于它的对象都得到通知并且自动更新,Spring中的观察者模式一般用来listener的实现,Spring中的事件编程模型就是观察者模式的实现

在Spring中定义了ApplicationListener接口,用来监听Application的事件,Application其实就是ApplicationContext,ApplicationContext内置了几个事件

4.7 策略模式

策略模式是行为性模式,调用不同的方法,适应行为的变化,强调父类调用子类的特性

getHandler是HandlerMapping接口中的唯一方法,用于根据请求找到匹配的处理器

比如写了AccountDAO接口,然后这个接口下有不同的实现类,对于service不需要关心底层的具体实现,只需要使用AccountDAO进行调用即可,底层可以灵活切换

4.8 模板设计模式

Spring中的JdbcTemplate类就是一个模板类。它就是一个模板方法设计模式的体现。在模板类的模板方法execute中编写核心算法,具体的实现步骤在子类中完成。


文章作者: 穿山甲
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 穿山甲 !
  目录