Spring与事务

事务的特性 ACID

  • 原子性:事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生
  • 一致性:事务执行前后数据的完整性保持一致
  • 隔离性:多个用户并发访问数据库时,一个用户的事务不能被其他事务所干扰
  • 持久性:一个事务一旦被提交,它对数据库的改变就是永久性的

并发事务的三大问题

  • 脏读:一个事务读取另一个事务改写但还未提交的数据,如果这些数据被回滚,则读到的是无效的
  • 不可重复读:在同一事务中,多次读同一数据返回的结果有所不同,由读到另一个事务更新的数据操作所引起
  • 幻读:一个事务读取了几行记录后,另一个事务插入一些记录,在后来的查询中,第一个事务就会发现原来没有的记录



事务的隔离级别

DEFAULT使用数据库默认的隔离级别
READ_UNCOMMITED允许读取还未提交的改变了的数据
READ_COMMITED允许在并发事务已经提交后读取
REPEATABLE_RAED对相同字段的多次读取是一致的,除非数据被事务本身改变
SERIALIZABLE完全服从ACID隔离级别,但在所有隔离级别中是最慢的

MySQL默认采用 REPEATABLE_READ级别


事务的传播行为

  解决业务层方法相互调用问题。比如在方法A中调用了方法B。

PROPAGATION_REQUIRED支持当前事务,如果不存在则新建一个
PROPAGATION_SUPPORTS支持当前事务,如果不存在则不使用事务
PROPAGATION_MANDATORY支持当前事务,如果不存在则抛出异常
PROPAGATION_REQUIRES_NEW如果有事务存在,挂起当前事务,创建一个新的事务
PROPAGATION_NOT_SUPPORTED以非事务方式运行,如果有事务存在,挂起当前事务
PROPAGATION_NEVER以非事务方式运行,如果有事务存在,抛出异常
PROPAGATION_NESTED如果当前事务存在,则嵌套事务执行



Spring 事务管理(基于注解方式)

  以下是基于mybatis数据库框架,文件结构如下:

.
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── website
    │   │       └── yuchen
    │   │           ├── controller
    │   │           ├── dao
    │   │           │   └── AccountMappper.java
    │   │           ├── po
    │   │           │   └── Account.java
    │   │           └── service
    │   │               ├── AccountServiceImpl.java
    │   │               └── AccountService.java
    │   └── resources
    │       ├── jdbc.properties
    │       ├── log4j.properties
    │       ├── mapper
    │       │   └── AccountMapper.xml
    │       ├── mybatis-config.xml
    │       └── spring.xml
    └── test
        └── java
            └── website
                └── yuchen
                    └── Main.java

环境配置整合

  用maven导入外部包,配置pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com</groupId>
  <artifactId>first</artifactId>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>first Maven Webapp</name>
  <url>http://maven.apache.org</url>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring.version>5.0.3.RELEASE</spring.version>
    <mybatis.version>3.4.4</mybatis.version>
  </properties>
  <dependencies>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context-support</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>${spring.version}</version>
      <scope>test</scope>
    </dependency>

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

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

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>${mybatis.version}</version>
    </dependency>

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.1</version>
    </dependency>

    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.1.1</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>first</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>


  在resources中配置spring.xml,配置c3p0连接池、整合mybatis以及配置事务管理

<?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">

    <context:property-placeholder location="classpath:jdbc.properties"/>

    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="user" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="maxPoolSize" value="30"/>
        <property name="minPoolSize" value="10"/>
        <property name="autoCommitOnClose" value="false"/>
        <property name="checkoutTimeout" value="10000"/>
        <property name="acquireRetryAttempts" value="2"/>
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <property name="typeAliasesPackage" value="website.yuchen.po"/>
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>
    </bean>

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
        <property name="basePackage" value="website.yuchen.dao"/>
    </bean>


    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <context:component-scan base-package="website.yuchen.service" />
    <tx:annotation-driven transaction-manager="transactionManager" />

</beans>


  mybatis-config.xml 配置如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <settings>
        <setting name="useGeneratedKeys" value="true" />
        <setting name="useColumnLabel" value="true" />
        <setting name="mapUnderscoreToCamelCase" value="true" />
    </settings>

</configuration>


  分别配置 jdbc.properties 以及 log4j.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/Test?useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=123456
log4j.rootLogger=ERROR, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n


银行转账案例

  首先创建数据库表:

create table account (
    id int primary key,
    name varchar(11),
    money double
) charset=utf8;

insert into account values(1, 'aaa', 1000);
insert into account values(2, 'bbb', 1000);
insert into account values(3, 'ccc', 1000);

  在dao包下创建AccountMapper接口,分别规定存取钱的方法:

package website.yuchen.dao;

import org.apache.ibatis.annotations.Param;

public interface AccountMappper {
    public void outMoney(@Param("out") String out, @Param("money") Double money);
    public void inMoney(@Param("in") String in, @Param("money") Double Money);
}

  在resources下定义对应的mapper配置:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="website.yuchen.dao.AccountMappper">

    <insert id="inMoney">
        update account set money = money + #{money}
          where name = #{in}
    </insert>


    <insert id="outMoney">
        update account set money = money - #{money}
            where name = #{out}
    </insert>

</mapper>


  定义与数据库表映射的PO对象:

package website.yuchen.po;

public class Account {
    Integer id;  String name;  Double money;

    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Double getMoney() {
        return money;
    }
    public void setMoney(Double money) {
        this.money = money;
    }
}


  在service包下定义转账的接口AccountService以及起实现类AccountServiceImpl:

package website.yuchen.service;

public interface AccountService {
    public void transfer(String out, String in, Double money);
}
package website.yuchen.service;

import website.yuchen.dao.AccountMappper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service("accountServiceImpl")
public class AccountServiceImpl implements AccountService
{
    @Autowired
    private AccountMappper mapper;

    @Override
    public void transfer(String out, String in, Double money) {
        mapper.outMoney(out, money);
        mapper.inMoney(in, money);
    }
}

在实现类中定义了转账的操作,从某个账户中取钱并转给另一个账户。


  最后编写测试类,从aaa账户转出200块给bbb账户:

package website.yuchen;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import website.yuchen.service.AccountService;


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class Main
{
    @Autowired
    private AccountService service;

    @Test
    public void testDemo() {
        service.transfer("aaa", "bbb", 200d);
    }
}

运行之后,发现aaa账户少了200,同时bbb账户多了200,转账成功。


  但如果在转账的过程中出现了某些异常,比如在 AccountServiceImpl 中如下修改,并将数据库数据恢复原状:

package website.yuchen.service;

import website.yuchen.dao.AccountMappper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service("accountServiceImpl")
public class AccountServiceImpl implements AccountService
{
    @Autowired
    private AccountMappper mapper;

    @Override
    public void transfer(String out, String in, Double money) {
        mapper.outMoney(out, money);
        int i = 1/0;    // 发生了异常
        mapper.inMoney(in, money);
    }
}

那么就会发生钱从源账户转出,却没有转到目标账户!


  所以此时,就利用事务来解决这个问题,为AccountServiceImpl添加事务注解,并将数据库数据恢复原状:

package website.yuchen.service;

import website.yuchen.dao.AccountMappper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @Transactional 注解的一些属性
 * propagation 传播行为
 * isolation 隔离级别
 */

@Service("accountServiceImpl")
@Transactional
public class AccountServiceImpl implements AccountService
{
    @Autowired
    private AccountMappper mapper;

    @Override
    public void transfer(String out, String in, Double money) {
        mapper.outMoney(out, money);
        int i = 1/0;    // 发生了异常
        mapper.inMoney(in, money);
    }
}

  转账失败,源账户取钱的数据被回滚,避免了丢失的情况

@Transactional注解的一些属性:

  • propagation 传播行为
  • isolation 隔离级别

根据前文概念中的传播行为和隔离级别进行相应的设置

-------------本文结束-------------