在现代的软件开发领域,事务管理是一项至关重要的技术。无论是在企业应用程序还是在网站开发中,保证数据的一致性和完整性都是至关重要的。Spring Framework 提供了丰富的功能来简化事务管理,并与其它模块无缝集成,使得开发人员可以轻松地处理事务操作。
在本篇博客中,我们将深入探讨 Spring 框架中的事务管理,并介绍如何整合事务管理到我们的应用程序中。我们将学习什么是事务管理,为什么使用事务管理,以及如何配置和使用 Spring 的事务管理器来实现数据的一致性。
无论你是一个有经验的 Spring 开发人员,还是刚开始接触 Spring 的初学者,本篇博客都会为你提供清晰而全面的指导。让我们一同进入 Spring 整合事务管理的世界,探索其强大的功能和灵活性。
本次通过一个简单的银行转账的案例来介绍事务管理。
create table card_infos(
card_id int auto_increment primary key ,
card_num varchar(20), -- 卡号
balance decimal -- 余额
);
insert into card_infos(card_num, balance) VALUE ('456736478372714678',1000),('564738264736475823',1000);
com.alibaba
druid
1.2.15
org.springframework
spring-context
5.3.23
org.springframework
spring-jdbc
5.3.23
org.springframework
spring-tx
5.3.23
ch.qos.logback
logback-classic
1.4.5
mysql
mysql-connector-java
8.0.33
org.mybatis
mybatis
3.5.6
org.mybatis
mybatis-spring
2.0.6
org.projectlombok
lombok
1.18.30
@Data
public class CardInfos {
private Integer cardId;
private String cardNum;
private BigDecimal balance;
}
这个实体类
CardInfos
主要用于表示一个卡片信息的数据模型。在软件开发中,实体类用于封装和表示某个具体对象的属性和行为。
public interface CardDao {
/**
* 查询转账卡号是否有足够余额
* @param cardNum
* @return
*/
CardInfos getAccountByCardNum(String cardNum);
/**
* 更新账号信息
* @param cardInfos
*/
void update(CardInfos cardInfos);
}
根据题目要求:转账前需要插叙自己的余额是否充足。所以我们需要定义一个查询的方法,查询余额。那么当 A、B交易成功后,它们的余额都发生了改变,所以需要定义一个 update 修改的方法,对相应的用户的余额进行修改
CardService 接口
public interface CardService {
/**
*
* @param formCardNum 转账人卡号
* @param toCardNum 收帐人卡号
* @param money 金额
*/
void transfer(String formCardNum, String toCardNum, BigDecimal money);
/**
* 查询转账卡号是否有足够余额
* @param cardNum
* @return
*/
CardInfos getAccountByCardNum(String cardNum);
/**
* 更新账号信息
* @param cardInfos
*/
void update(CardInfos cardInfos);
}
CardServiceImpl 实现类
@Service
@RequiredArgsConstructor
@Slf4j
public class CardServiceImpl implements CardService {
private final CardDao cardDao;
private final DemoService demoService;
@Override
public CardInfos getAccountByCardNum(String cardNum) {
return cardDao.getAccountByCardNum(cardNum);
}
@Override
public void update(CardInfos cardInfos) {
cardDao.update(cardInfos);
}
/**
* @param formCardNum 转账人卡号
* @param toCardNum 收帐人卡号
* @param money 金额
*/
@Override
public void transfer(String formCardNum, String toCardNum, BigDecimal money) {
// 获取转账人的信息
CardInfos formAccount = cardDao.getAccountByCardNum(formCardNum);
// 获取接收人的信息
CardInfos toAccount = cardDao.getAccountByCardNum(toCardNum);
// 转账人的金额
BigDecimal formbalance = formAccount.getBalance();
// 接收人的金额
BigDecimal tobalance = toAccount.getBalance();
// 转账人如果有足够的余额才进行转账
if (formbalance.doubleValue() >= money.doubleValue()) {
// 设置转账人的余额(扣除转账金额)
formAccount.setBalance(formbalance.subtract(money));
formAccount.setCardNum(formCardNum);
// 设置接收人的余额(添加转账的金额)
toAccount.setBalance(tobalance.add(money));
toAccount.setCardNum(toCardNum);
// 更新转账人和接收人的金额
cardDao.update(formAccount);
cardDao.update(toAccount);
log.info("转账完成!");
} else {
log.info("卡号:" + formCardNum + ",余额不足 ");
throw new RuntimeException("余额不足");
}
}
}
作用:处理转账的业务逻辑
db.properties
driver = com.mysql.cj.jdbc.Driver
url = jdbc:mysql://localhost:3306/psm
username= root
password = 123456
maxActive = 200
initialSize = 5
minIdle = 5
maxWait = 2000
minEvictableIdleTimeMillis = 300000
timeBetweenEvictionRunsMillis = 60000
testWhileIdle = true
testOnReturn = false
validationQuery = select 1
poolPreparedStatements = false
作用:这段配置是一个典型的数据库连接池的配置,用于连接和管理与 MySQL 数据库的连接。 以上配置项提供了对数据库连接池的一些基本配置和管理,包括连接数量、连接等待时间、连接回收策略等。通过连接池来管理数据库连接可以提升系统性能,并且避免频繁地创建和销毁连接,从而减少资源开销和提高响应速度。
CardMapper.xml
update card_infos set balance = #{balance} where card_num = #{cardNum}
作用:用于定义 DAO 接口
edu.nf.ch03.dao.CardDao
中的 SQL 映射关系和操作语句。通过这个配置文件,Mybatis 可以自动生成对应的 SQL 语句,并提供给
edu.nf.ch03.dao.CardDao
接口的实现类使用。在程序中,通过调用相应的方法,就可以执行对应的数据库操作,例如查询账户余额或更新账户余额。
@Configuration
@MapperScan(basePackages = "edu.nf.ch03.dao")
@ComponentScan(basePackages = "edu.nf.ch03")
public class AppConfig {
@Bean(initMethod = "init",destroyMethod = "close")
public DruidDataSource dataSource() throws Exception {
Properties properties = new Properties();
InputStream stream = AppConfig.class.getClassLoader().getResourceAsStream("db.properties");
properties.load(stream);
return (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
}
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws IOException {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setTypeAliasesPackage("edu.nf.homework.entity");
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath:mappers/*.xml");
sqlSessionFactoryBean.setMapperLocations(resources);
//启用mybatis日志功能
org.apache.ibatis.session.Configuration conf = new org.apache.ibatis.session.Configuration();
conf.setLogImpl(StdOutImpl.class);
//将日志配置设置到SqlSessionFactoryBean中
sqlSessionFactoryBean.setConfiguration(conf);
return sqlSessionFactoryBean;
}
}
作用:通过
dataSource
方法配置了一个 Druid 数据源,通过sqlSessionFactoryBean
方法配置了一个SqlSessionFactoryBean
对象,设置了数据源、实体类别名包扫描路径和映射文件位置。这里还启用了 MyBatis 的日志功能,可以打印 SQL 执行的日志信息。
我们先测试一下保证转账功能可以正常运行。
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
CardService bean = context.getBean(CardService.class);
// 转账金额
BigDecimal bigDecimal = new BigDecimal("500");
// 执行转账
bean.transfer("564738264736475823","456736478372714678",bigDecimal);
}
}
运行结果
事务管理是为了确保数据库操作的一致性、可靠性和完整性而引入的机制。在一个业务操作中可能涉及多个数据库操作,而这些操作要么全部成功,要么全部失败,不能出现部分操作成功、部分操作失败的情况。事务管理可以确保这种一致性。
以下是几个使用事务管理的原因:
数据库一致性:如果一个业务操作需要执行多个数据库操作,例如插入、更新、删除等,那么这些操作之间可能存在依赖关系,需要保证在事务提交前所有操作都成功执行,如果其中某个操作失败了,整个事务可以回滚,保持数据的一致性。
并发控制:在多用户、多线程环境下,多个事务可能同时对数据库进行读写操作,如果没有事务管理,可能会导致数据的不一致或并发冲突。通过使用事务管理,可以有效控制并发访问,保证数据的正确性和完整性。
异常处理:在业务操作中可能发生各种异常情况,例如数据库连接异常、事务超时、网络异常等。如果没有事务管理,这些异常可能导致数据不一致或资源泄露。通过使用事务管理,可以对异常进行捕获和处理,保证数据的完整性,并进行相应的回滚操作。
性能优化:事务管理还可以提供一些性能优化的手段,例如批量提交、脏读、隔离级别设置等,可以根据业务需求和数据库性能要求进行相应的配置,提高数据库操作的效率和性能。
综上所述,事务管理是确保数据库操作的一致性和可靠性的重要机制,通过对多个操作进行事务管理,可以保证数据的正确性,并提供异常处理和性能优化的功能
在 CardServiceImpl 实现类中,引发了一个异常,如果是正常的转账,如果有异常的话,A和B 的余额都不应该发生改变,只要有异常这个流程就要终止,我们现在来测试一下。
运行结果:
看到问题没有!我们的异常已经生效了,但是是不是还扣除了转账人的金额。这就是我们为什么要使用事务的原因,那我们现在就来使用事务来管理这个实现类吧。
在 CardServiceImpl 中的 transfet 方法上使用 @Transactional 注解
/**
* @param formCardNum 转账人卡号
* @param toCardNum 收帐人卡号
* @param money 金额
* @Transactional 注解使用 spring 提供的声明式事务,可以用在方法上,也可以用在类上
* 当用在类上的时候,表示这个类的所有方法都享有事务功能
*/
@Transactional
@Override
public void transfer(String formCardNum, String toCardNum, BigDecimal money) {
// 获取转账人的信息
CardInfos formAccount = cardDao.getAccountByCardNum(formCardNum);
// 获取接收人的信息
CardInfos toAccount = cardDao.getAccountByCardNum(toCardNum);
// 转账人的金额
BigDecimal formbalance = formAccount.getBalance();
// 接收人的金额
BigDecimal tobalance = toAccount.getBalance();
// 转账人如果有足够的余额才进行转账
if (formbalance.doubleValue() >= money.doubleValue()) {
// 设置转账人的余额(扣除转账金额)
formAccount.setBalance(formbalance.subtract(money));
formAccount.setCardNum(formCardNum);
// 设置接收人的余额(添加转账的金额)
toAccount.setBalance(tobalance.add(money));
toAccount.setCardNum(toCardNum);
// 更新转账人和接收人的金额
cardDao.update(formAccount);
// 引发异常
System.out.println(10 / 0);
cardDao.update(toAccount);
log.info("转账完成!");
// demoService.add();
// 调用本类的其他方法,不会启用事务
// this.update();
} else {
log.info("卡号:" + formCardNum + ",余额不足 ");
throw new RuntimeException("余额不足");
}
}
@Transactional
是 Spring 框架提供的一个注解,用于标记方法或类,表示该方法或类需要进行事务管理。使用
@Transactional
注解时,Spring 会在方法执行前开启一个事务,并在方法执行完成后根据执行情况决定是提交事务还是回滚事务。在方法中如果发生了异常,则事务会回滚,保证数据的一致性。
@Transactional
注解可以应用于方法级别和类级别:
- 方法级别:标注在方法上,表示该方法需要进行事务管理。
- 类级别:标注在类上,表示该类的所有方法都需要进行事务管理。
@Configuration
@MapperScan(basePackages = "edu.nf.ch03.dao")
@ComponentScan(basePackages = "edu.nf.ch03")
// 启用事务注解驱动,这样就可以在业务类中使用 @Transaction 注解
@EnableTransactionManagement
public class AppConfig {
@Bean(initMethod = "init",destroyMethod = "close")
public DruidDataSource dataSource() throws Exception {
Properties properties = new Properties();
InputStream stream = AppConfig.class.getClassLoader().getResourceAsStream("db.properties");
properties.load(stream);
return (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
}
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws IOException {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setTypeAliasesPackage("edu.nf.homework.entity");
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath:mappers/*.xml");
sqlSessionFactoryBean.setMapperLocations(resources);
//启用mybatis日志功能
org.apache.ibatis.session.Configuration conf = new org.apache.ibatis.session.Configuration();
conf.setLogImpl(StdOutImpl.class);
//将日志配置设置到SqlSessionFactoryBean中
sqlSessionFactoryBean.setConfiguration(conf);
return sqlSessionFactoryBean;
}
/**
* 装配事务管理器,并注入数据源,
* 这样事务管理器就可以基于 AOP 来管理 Connection 对象的事务操作
* @param dataSource
* @return
*/
@Bean
public PlatformTransactionManager txManager(DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}
}
@EnableTransactionManagement
是一个 Spring 框架提供的注解,用于启用Spring的事务管理功能。
通过添加
@EnableTransactionManagement
注解,告诉 Spring 启用事务管理。Spring 将会根据配置寻找合适的事务管理器。确保已经配置了一个与数据源关联的事务管理器。可以使用
@Bean
注解创建一个PlatformTransactionManager
的实例,并将其与数据源相关联。
运行结果:
在引发异常后是不是并没有扣除转账人的余额了,因为使用事务管理,在引发异常时,就会终止这个流程,只要引发异常就不会再执行下去了。一句话总结:
在一个业务操作中可能涉及多个数据库操作,而这些操作要么全部成功,要么全部失败,不能出现部分操作成功、部分操作失败的情况。
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(rollbackFor = RuntimeException.class,
propagation = Propagation.REQUIRED
)
public class CardServiceImpl implements CardService {
private final CardDao cardDao;
private final DemoService demoService;
@Override
public CardInfos getAccountByCardNum(String cardNum) {
return cardDao.getAccountByCardNum(cardNum);
}
@Override
public void update(CardInfos cardInfos) {
cardDao.update(cardInfos);
}
/**
* @param formCardNum 转账人卡号
* @param toCardNum 收帐人卡号
* @param money 金额
* @Transactional 注解使用 spring 提供的声明式事务,可以用在方法上,也可以用在类上
* 当用在类上的时候,表示这个类的所有方法都享有事务功能
*/
// @Transactional
@Override
public void transfer(String formCardNum, String toCardNum, BigDecimal money) {
// 获取转账人的信息
CardInfos formAccount = cardDao.getAccountByCardNum(formCardNum);
// 获取接收人的信息
CardInfos toAccount = cardDao.getAccountByCardNum(toCardNum);
// 转账人的金额
BigDecimal formbalance = formAccount.getBalance();
// 接收人的金额
BigDecimal tobalance = toAccount.getBalance();
// 转账人如果有足够的余额才进行转账
if (formbalance.doubleValue() >= money.doubleValue()) {
// 设置转账人的余额(扣除转账金额)
formAccount.setBalance(formbalance.subtract(money));
formAccount.setCardNum(formCardNum);
// 设置接收人的余额(添加转账的金额)
toAccount.setBalance(tobalance.add(money));
toAccount.setCardNum(toCardNum);
// 更新转账人和接收人的金额
cardDao.update(formAccount);
// 引发异常
System.out.println(10 / 0);
cardDao.update(toAccount);
log.info("转账完成!");
// demoService.add();
// 调用本类的其他方法,不会启用事务
// this.update();
} else {
log.info("卡号:" + formCardNum + ",余额不足 ");
throw new RuntimeException("余额不足");
}
}
public void update(){
}
}
@Transactional(rollbackFor = RuntimeException.class, propagation = Propagation.REQUIRED )当 @Transactional 写在类上时,表示这个类中的所有方法都享有事务功能 rollbackFor 属性表示当遇到什么异常将进行事务的回滚, 默认是遇到任意的运行时异常将自动回滚事务。 propagation 属性:表示事务传播级别,不同的事务传播级别,支持的事务范围将不一样(传播级别的类型说明参考文档)
注意:在这种情况下事务传播级别是不会生效的, 当一个业务类的方法启用了一个事务,然后再调用本类的其他方法时,事务是失效的
@Transactional
注解的rollbackFor
属性用于指定在哪些异常发生时需要回滚事务。在示例中,rollbackFor = RuntimeException.class
表示只有当方法抛出运行时异常(RuntimeException
)时才会回滚事务。同时,
@Transactional
注解的propagation
属性用于指定事务的传播行为。在示例中,propagation = Propagation.REQUIRED
表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。REQUIRED
是默认的传播行为。综合起来,使用
@Transactional(rollbackFor = RuntimeException.class, propagation = Propagation.REQUIRED)
注解在方法上,表示:
- 如果方法抛出运行时异常,则事务将会回滚。
- 如果当前存在事务,则方法会加入该事务进行执行。
- 如果当前没有事务,则会创建一个新的事务。
这样可以确保在方法执行过程中发生异常时,事务会根据设置进行回滚,并保持数据的一致性。
这里只是讲了常用的而已,还有很多其他的传播级别没有讲到!
本次案例通过整合 Mybatis 和事务管理,综合起来讲了事务管理最常用的一些配置,并没有讲的很细,事务管理还有很多的属性配置,和传播级别还有事务类型等。不过这里讲的都是常用的应该差不多了,不过呢剩下的还是需要去了解的。
完整代码地址:ch03 · qiuqiu/conformity-study - 码云 - 开源中国 (gitee.com)