文章目录
- 1. Spring事务简介
- 2. Spring事务的案例
- 案例代码
- 代码目录结构
- 数据库
- pom.xml
- Resource/jdbc.properties
- config/SpringConfig.java
- config/JdbcConfig.java
- config/MyBatisConfig.java
- dao/AccountDao.java
- service/AccountService.java
- service/impl/AccountServiceImpl.java
- 测试方法
- 问题分析
- 事务管理三步
- 第一步:在业务层接口上加上注解@Transactional
- 第二步:在JdbcConfig.java中注册事务管理器
- 第三步:在SpringConfig.java上加上开启事务管理的注解@EnableTransactionManagement
- 3. Spring事务角色
- 4. Spring事务属性
- 事务配置
- 案例:转账业务追加日志
- 案例代码
- 代码结构
- 数据库表
- dao/LogDao.java
- service/LogService.java
- service/LogServiceImpl.java
- 修改service/impl/AccountServiceImpl.java如下
- 改进
- 事务传播行为
1. Spring事务简介
事务作用: 在数据层保障一系列的数据库操作同成功、同失败
Spring事务作用: 在数据层或业务层保障一系列的数据库操作同成功、同失败
Spring为事务提供的接口和实现类:
// 接口
public interface PlatformTransactionManager{void commit(TransactionStatus status) throws TransactionException;void rollback(TransactionStatus status) throws TransactionException;
}
// 实现类
public class DataSourceTransactionManager{...
}
2. Spring事务的案例
需求: 实现两个账户间的转账操作
需求微缩: A账户减钱,B账户加钱
分析:
案例代码
代码目录结构
数据库
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.1.5</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>project5</artifactId><version>0.0.1-SNAPSHOT</version><name>project5</name><description>project5</description><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>6.0.3</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><version>6.0.3</version></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>8.0.33</version></dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.11</version></dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId><version>3.0.1</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.13</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.2</version><scope>test</scope></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-test</artifactId><version>5.3.25</version></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.5</version></dependency></dependencies></project>
Resource/jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=123456
config/SpringConfig.java
package com.example.project5.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;@Configuration
@PropertySource("classpath:jdbc.properties")
@ComponentScan("com.example.project5")
@Import({JdbcConfig.class, MyBatisConfig.class})
public class SpringConfig {
}
config/JdbcConfig.java
package com.example.project5.config;import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;public class JdbcConfig {@Value("${jdbc.driver}")String driver;@Value("${jdbc.url}")String url;@Value("${jdbc.username}")String username;@Value("${jdbc.password}")String password;@Beanpublic DataSource dataSource(){DruidDataSource ds = new DruidDataSource();ds.setDriverClassName(driver);ds.setUsername(username);ds.setPassword(password);ds.setUrl(url);return ds;}}
config/MyBatisConfig.java
package com.example.project5.config;import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;import javax.sql.DataSource;public class MyBatisConfig {@Beanpublic SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();ssfb.setTypeAliasesPackage("com.example.project5.domain");ssfb.setDataSource(dataSource);return ssfb;}@Beanpublic MapperScannerConfigurer mapperScannerConfigurer(){MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();mapperScannerConfigurer.setBasePackage("com.example.project5.dao");return mapperScannerConfigurer;}}
dao/AccountDao.java
package com.example.project5.dao;import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;@Repository
public interface AccountDao {@Update("update account set money = money + #{money} where username = #{name}")void addMoney(@Param("name") String username, @Param("money") Double money);@Update("update account set money = money - #{money} where username = #{name}")void outMoney(@Param("name") String username, @Param("money") Double money);
}
service/AccountService.java
package com.example.project5.service;public interface AccountService {/*** 转账操作* @param out 转出方* @param in 转入方* @param money 金额*/public void transfer(String out, String in, double money);
}
service/impl/AccountServiceImpl.java
package com.example.project5.service.impl;import com.example.project5.dao.AccountDao;
import com.example.project5.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class AccountServiceImpl implements AccountService {@AutowiredAccountDao accountDao;@Overridepublic void transfer(String out, String in, double money) {accountDao.outMoney(out, money);accountDao.addMoney(in, money);}
}
测试方法
package com.example.project5;import com.example.project5.config.SpringConfig;
import com.example.project5.service.AccountService;
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;@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class Project5ApplicationTests {@Autowiredprivate AccountService accountService;@Testpublic void testTransfer() {accountService.transfer("aaa", "bbb", 20);}}
执行测试代码后,测试代码不会产生任何输出,但数据库中aaa的金额会由100变成80,bbb的金额会由111变成131:
问题分析
假如在AccountServiceImpl中手动制造一个错误:
package com.example.project5.service.impl;import com.example.project5.dao.AccountDao;
import com.example.project5.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class AccountServiceImpl implements AccountService {@AutowiredAccountDao accountDao;@Overridepublic void transfer(String out, String in, double money) {accountDao.outMoney(out, money);int a = 1/0;accountDao.addMoney(in, money);}
}
这时候,程序在执行完outMoney
方法,也就是aaa转出了20之后就不会继续执行了,这20并没有转入到bbb的账户之中,这就是事务的不一致性。接着上面的aaa金额为80,bbb的金额为131执行这个会报错的代码,结果是:
对运行的结果简单进行分析:
我们需要进行事务管理,使得数据层中的数据同加同减,而不是分开操作
事务管理三步
第一步:在业务层接口上加上注解@Transactional
package com.example.project5.service;import org.springframework.transaction.annotation.Transactional;public interface AccountService {/*** 转账操作* @param out 转出方* @param in 转入方* @param money 金额*/@Transactionalpublic void transfer(String out, String in, double money);
}
第二步:在JdbcConfig.java中注册事务管理器
package com.example.project5.config;import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;import javax.sql.DataSource;
import java.lang.management.PlatformLoggingMXBean;public class JdbcConfig {@Value("${jdbc.driver}")String driver;@Value("${jdbc.url}")String url;@Value("${jdbc.username}")String username;@Value("${jdbc.password}")String password;@Beanpublic DataSource dataSource(){DruidDataSource ds = new DruidDataSource();ds.setDriverClassName(driver);ds.setUsername(username);ds.setPassword(password);ds.setUrl(url);return ds;}@Beanpublic PlatformTransactionManager transactionManager(DataSource dataSource){DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();dataSourceTransactionManager.setDataSource(dataSource);return dataSourceTransactionManager;}}
第三步:在SpringConfig.java上加上开启事务管理的注解@EnableTransactionManagement
package com.example.project5.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
@PropertySource("classpath:jdbc.properties")
@ComponentScan("com.example.project5")
@Import({JdbcConfig.class, MyBatisConfig.class})
@EnableTransactionManagement
public class SpringConfig {
}
启用事务管理后,保持刚才会报错的AccountServiceImpl.java的代码,恢复aaa金额为80,bbb金额为131,并再次进行测试,此时数据库中的内容不会发生任何改变:
3. Spring事务角色
在事务没有开启的时候:
outMoney
和inMoney
分别对应一个事务,我们手动写的异常是写在事务T1和事务T2之间的,则事务T1执行完毕以后发生了异常,所以事务T2不再执行
为了将两个事务统一起来,统一执行,或者统一不执行,我们在transfer
方法上加了注解@Transactional,此时transfer本身是一个事务,我们将outMoney
和inMoney
都加入到这个事务中来:
此时我们将transfer
方法称为事务管理员,outMoney
和inMoney
称为事务协调员,具体定义如下:
4. Spring事务属性
事务配置
在@Transactional中还有很多属性
这里需要说明的是rollbackFor
,默认的事务回滚,在我们没有定义rollbackFor
的时候,只会在程序中出现运行时异常时候进行回滚,比如我们刚才手动指定的1/0就属于一个运行时抛出异常,假如修改这个异常如下:
package com.example.project5.service.impl;import com.example.project5.dao.AccountDao;
import com.example.project5.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.io.IOException;@Service
public class AccountServiceImpl implements AccountService {@AutowiredAccountDao accountDao;@Overridepublic void transfer(String out, String in, double money) throws IOException {accountDao.outMoney(out, money);if(true) throw new IOException();accountDao.addMoney(in, money);}
}
再执行测试代码,就会发现数据库中的内容会从(aaa:80,bbb:131)->(aaa:60,bbb:131)
再次印证:没有定义rollbackFor
的时候,只会在程序中出现运行时异常时候进行回滚
那么我们定义一下rollbackFor
属性,如下:
package com.example.project5.service;import org.springframework.transaction.annotation.Transactional;import java.io.IOException;public interface AccountService {/*** 转账操作* @param out 转出方* @param in 转入方* @param money 金额*/@Transactional(rollbackFor = {IOException.class})public void transfer(String out, String in, double money) throws IOException;
}
再执行测试代码,就会发现数据库中的内容(aaa:60,bbb:131)->(aaa:60,bbb:131),没有发生改变,所以我们需要通过rollbackFor
来指定一些非运行时异常,在定义rollbackFor
以后,程序在遇到运行时异常仍会回滚。
案例:转账业务追加日志
案例代码
在上述案例代码中加上如下内容:
代码结构
数据库表
dao/LogDao.java
package com.example.project5.dao;import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;import java.util.Date;@Repository
public interface LogDao {@Insert("insert into log(content, date) VALUES(#{content}, #{date})")void insertLog(@Param("content") String content, @Param("date") Date date);
}
service/LogService.java
注意,该方法上也要加上事务注解
package com.example.project5.service;import java.util.Date;public interface LogService {@Transactionalvoid insertLog(String content, Date date);
}
service/LogServiceImpl.java
package com.example.project5.service.impl;import com.example.project5.dao.LogDao;
import com.example.project5.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.Date;@Service
public class LogServiceImpl implements LogService {@AutowiredLogDao logDao;@Overridepublic void insertLog(String content, Date date) {logDao.insertLog(content, date);}
}
修改service/impl/AccountServiceImpl.java如下
package com.example.project5.service.impl;import com.example.project5.dao.AccountDao;
import com.example.project5.service.AccountService;
import com.example.project5.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.io.IOException;
import java.util.Date;@Service
public class AccountServiceImpl implements AccountService {@AutowiredAccountDao accountDao;@AutowiredLogService logService;@Overridepublic void transfer(String out, String in, double money) throws IOException {try{accountDao.outMoney(out, money);accountDao.addMoney(in, money); } finally {logService.insertLog(out + "向" + in + "转账" + money + "元", new Date());}}
}
当捕捉到异常时执行日志记录。
将数据库中的金额恢复为:aaa->100,bbb->111,并执行测试代码,得到account表和log表的结果:
正常执行的时候,会修改数据库中的金额、向日志记录中添加日志
假设我们在AccountServiceImpl的try中加上:
accountDao.outMoney(out, money);
int a = 1/0;
accountDao.addMoney(in, money);
我们期望的结果是:不修改数据库中的金额、向日志记录中添加日志,使用修改后的代码再执行测试方法,得到结果是account表和log表中的内容都没有发生任何变化,所以我们归纳总结出存在的问题:
改进
我们需要定义事务的传播属性propagation
,在LogService.java下重新写注解,改为:
@Transactional(propagation = Propagation.REQUIRES_NEW)
此时,再次运行上面的代码,结果为:
account表中的内容不变,log表中新添了日志:
我认为这样的改进可以理解为,使用默认的propagation时,事务协调员都被添加到事务管理员的事务中,从而统一提交或统一回滚:
当我们在LogService上写明了事务的传播行为为Requires_New后,即使原有了事务,我们还是会为这个service实例开启一个新事务,如下,这样就不是统一受到事务t的控制了: