Spring 事务快速入门详解

Spring事务简介

什么是事务?

事务指数据库中多个操作合并在一起形成的操作序列

事务特征(ACID)

事务有四个特性:

  • 原子性(Atomicity)指事务是一个不可分割的整体,其中的操作要么全执行或全不执行

  • 一致性(Consistency)事务前后数据的完整性必须保持一致

  • 隔离性(Isolation)事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离

  • 持久性(Durability)持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响

image-20210724165045186

Spring事务的作用

  • 事务作用:在数据层保障一系列的数据库操作同成功同失败
  • Spring事务作用:在数据层或**业务层**保障一系列的数据库操作同成功同失败

当数据库操作序列中个别操作失败时,提供一种方式使数据库状态恢复到正常状态(A),保障数据库即使在异常状态下仍能保持数据一致性(C)(要么操作前状态,要么操作后状态)。

当出现并发访问数据库时,在多个访问间进行相互隔离,防止并发访问操作结果互相干扰(I),从而最终保持数据的持久性(D)。

Spring事务核心接口

在Spring中,数据层的Mybatis使用的是DataSourceTransactionManager实现类来进行事务控制,它实现的是Spring中的事务管理的核心接口PlatformTransactionManager

image-20220724112829869

image-20220724113059666

image-20220724113030489

Spring事务角色

你可以看完案例再来看这个事务角色的概念

  • 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
  • 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法

例如下面事务流程案例:业务层的方法就充当事务管理员,负责开启事务,调用的两个DAO方法加入事务,是业务协调员

image-20210801192453227

Spring事务实战分析

需求和分析

需求:实现任意两个账户间转账操作

例如:A账户像B账户转账,那么就是A账户减钱,B账户加钱

分析:

  1. 数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)
  2. 业务层提供转账操作(transfer),调用减钱与加钱的操作
  3. 提供2个账号和操作金额执行转账操作
  4. 基于Spring整合MyBatis环境搭建上述操作

结果分析:

  1. 程序正常执行时,账户金额A减B加,没有问题
  2. 程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败

代码实现

测试环境准备

项目结构如下:

image-20220724114021879

创建一个数据库:transaction_test,建一个简单的测试表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tb_account
-- ----------------------------
DROP TABLE IF EXISTS `tb_account`;
CREATE TABLE `tb_account`  (
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  `money` DOUBLE NULL DEFAULT NULL
) ENGINE = MyISAM CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of tb_account
-- ----------------------------
INSERT INTO `tb_account` VALUES ('Tong', 1000);
INSERT INTO `tb_account` VALUES ('Meng', 200);

SET FOREIGN_KEY_CHECKS = 1;

Spring整合Mybatis相关代码(依赖、JdbcConfig、MybatisConfig、SpringConfig)

相关依赖

<?xml version="1.0" encoding="UTF-8"?>

<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/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.yyl</groupId>
  <artifactId>spring_24_case_transfer</artifactId>
  <version>1.0-SNAPSHOT</version>

  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.16</version>
    </dependency>

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.6</version>
    </dependency>

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

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

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

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

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>

  </dependencies>
</project>

jdbc配置文件

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/transaction_test?useSSL=false
jdbc.username=root
jdbc.password=root

配置类

// JdbcConfig 
package com.yyl.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;


public class JdbcConfig {
    
    
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String userName;
    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource(){
    
    
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        return ds;
    }
}
package com.yyl.config;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;

public class MybatisConfig {
    
    

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
    
    
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        ssfb.setTypeAliasesPackage("com.yyl.domain");
        ssfb.setDataSource(dataSource);
        return ssfb;
    }

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
    
    
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.yyl.dao");
        return msc;
    }
}
package com.yyl.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@ComponentScan("com.yyl")
@PropertySource("classpath:jdbc.properties")
@Import({
    
    JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
    
    
}

这里我们先不开启事务

编写DAO层

public interface AccountDao {
    
    

    @Update("update tb_account set money = money + #{money} where name = #{name}")
    void inMoney(@Param("name") String name, @Param("money") Double money);

    @Update("update tb_account set money = money - #{money} where name = #{name}")
    void outMoney(@Param("name") String name, @Param("money") Double money);
}

public interface AccountService {
    
    
    /**
     * 转账操作
     * @param out 传出方
     * @param in 转入方
     * @param money 金额
     */
    public void transfer(String out,String in ,Double money) ;
}

@Service
public class AccountServiceImpl implements AccountService {
    
    
    @Autowired
    private AccountDao accountDao;

    public void transfer(String out,String in ,Double money) {
    
    
        accountDao.outMoney(out,money);
        int i = 1/0;
        accountDao.inMoney(in,money);
    }
}

此时运行结果,Tong的钱少了200,Meng的钱没加,因为执行到除0操作,程序挂掉了

image-20220724115354076

上面的实现应用到现实中肯定是不行的,那么下面我们引入事务再来改善一下代码

使用Spring事务管理修改案例

在业务层接口上添加Spring事务管理

public interface AccountService {
    
    
    //配置当前接口方法具有事务
    @Transactional
    public void transfer(String out,String in ,Double money) ;
}

注意事项

  1. Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合
  2. 注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务

设置事务管理器(将事务管理器添加到IOC容器中)

说明:可以在JdbcConfig中配置事务管理器

//配置事务管理器,mybatis使用的是jdbc事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
    
    
    DataSourceTransactionManager dtm = new DataSourceTransactionManager();
    transactionManager.setDataSource(dataSource);
    return transactionManager;
}

注意事项

  1. 事务管理器要根据实现技术进行选择
  2. MyBatis框架使用的是JDBC事务

开启注解式事务驱动

@Configuration
@ComponentScan("com.yyl")
@PropertySource("classpath:jdbc.properties")
@Import({
    
    JdbcConfig.class,MybatisConfig.class})
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
    
    
}

运行测试类,查看结果

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
    
    

    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer() throws IOException {
    
    
        accountService.transfer("Tong","Meng",100D);
    }
}

运行结果如下:即使报错也不会发生数据库误操作,因为进行了事务回滚操作

image-20220724115539733

去掉除0操作,再运行结果如下:一个减钱,一个价钱,没有问题

image-20220724115511390

还有一个问题

问题1:Spring提供的事务管理是数据层的事务还是业务层的事务?

首先那肯定是业务层。Spring事务为业务逻辑进行事务管理,保证业务逻辑上数据的原子性

扩展:事务分类

事务得根据项目性质来细分:事务可以设置到三个层面(dao层、service层和web层)。

第一:web层事务,这一般是针对那些安全性要求较高的系统来说的。例如电子商务网站。粒度小,一般系统用不着这么细。
第二:service层事务,这是一常见的事务划分, 将事务设置在业务逻辑上,只要业务逻辑出错或异常就事务回滚。粒度较小,一般推荐这种方式。
第三:数据持久层数据务,也就是常说的数据库事务。这种事务在安全性方面要求低。就是给一个简单的增删改之类的操作增加事务操作。粒度大

问题2:为什么只用服务层的事务?

给Service层配置事务,因为一个Service层方法操作可以关联到多个DAO的操作。在Service层执行这些Dao操作,多DAO操作有失败全部回滚,成功则全部提交。

事务分为业务事务和系统事务,业务事务也就是业务逻辑上操作的一致性,系统事务自然就是指真正的数据库事务,

Dao层是数据访问层,是不应该包含业务逻辑的,这就是和Service层的不同;
Service层就是业务逻辑层,事务的管理就是为Service层上的保证。

猜你喜欢

转载自blog.csdn.net/weixin_45525272/article/details/125957700