Spring数据库事务典型错误用法剖析

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
云数据库 RDS PostgreSQL,高可用系列 2核4GB
简介: Spring数据库事务典型错误用法剖析

数据事务是企业应用关注的核心内容,也是开发者最容易犯错的地方。

本文就来讲解一些使用中的不良习惯,注意,它们可以避免一些错误和性能的丢失。


错误使用Service


互联网往往采用模型—视图—控制器(Model View Controller,MVC)来搭建开发环境,因此在Controller中使用Service是十分常见的。


为了方便测试,角色服务(roleService)和角色列表(RoleListService)进行测。假设我们想在一个Controller中插入两个角色,并且两个角色需要在同一个事务中处理,下面先给出错误使用Service的Controller,代码如下。


package com.learn.ssm.chapter14.controller;
/****************** imports ******************/
@Controller
    public class RoleController {
        @Autowired    
        private RoleService roleService = null;    
            @Autowired    
            private RoleListService roleListService = null;        // 方法无事务    public void errerUseServices() {        Role role1 = new Role();        role1.setRoleName("role_name_1");        role1.setNote("role_note_1");        // 带事务方法        roleService.insertRole(role1);        Role role2 = new Role();        role2.setRoleName("role_name_2");        role2.setNote("role_note_2");        // 带事务方法        roleService.insertRole(role2);    }}


类似的代码在工作中常常出现,甚至拥有多年开发经验的开发人员也会犯这类错误。这里存在的问题是两个insertRole方法根本不在同一个事务里的问题。


当一个Controller使用Service方法时,如果这个Service方法标注有@Transactional,那么它就会启用一个事务,而一个Service方法完成后,它就会释放该事务,所以前后两个insertRole的方法是在两个不同的事务中完成的。


下面是笔者测试这段代码的日志,可以清晰地看出它们并不存在于同一个事务中。


org.mybatis.logging.Logger: Creating a new SqlSessionorg.mybatis.logging.Logger: Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53f0a4cb]org.mybatis.logging.Logger: JDBC Connection [1746570062, URL=jdbc:mysql://localhost:3306/ssm, UserName=root@localhost, MySQL Connector Java] will be managed by Springorg.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==>  Preparing: insert into t_role(role_name, note) values (?, ?)org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: role_name_1(String), role_note_1(String)org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <==    Updates: 1org.mybatis.logging.Logger: Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53f0a4cb]org.mybatis.logging.Logger: Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53f0a4cb]org.mybatis.logging.Logger: Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53f0a4cb]org.mybatis.logging.Logger: Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53f0a4cb]org.mybatis.logging.Logger: Creating a new SqlSessionorg.mybatis.logging.Logger: Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@640f11a1]org.mybatis.logging.Logger: JDBC Connection [2059592603, URL=jdbc:mysql://localhost:3306/ssm, UserName=root@localhost, MySQL Connector Java] will be managed by Springorg.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==>  Preparing: insert into t_role(role_name, note) values (?, ?)org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters: role_name_2(String), role_note_2(String)org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <==    Updates: 1org.mybatis.logging.Logger: Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@640f11a1]

这样如果第一个插入成功了,而第二个插入失败了,就会使数据库数据不完全同时成功或者失败,可能产生严重的数据不一致的问题,给生产带来严重的损失。


这个例子明确地告诉大家使用带有事务的Service方法时,应该只有一个入口,然后使用传播行为来定义事务策略。


如果错误地进行多次调用,就不会在同一个事务中,这会造成不同时提交或回滚的数据一致性问题。


每一个Java EE开发者都要注意这类问题,以避免一些不必要的错误


过长时间占用事务


在企业的生产系统中,数据库事务资源是最宝贵的资源之一,使用了数据库事务之后,要及时释放数据库事务资源,甚至要评估数据库事务处理业务的耗时。


换言之,我们应该尽可能地缩短使用数据库事务资源的时间去完成所需工作,为此我们需要区分哪些业务是需要事务的,哪些又是不需要的,而不需要的耗时又如何?



比如,在工作中需要使用到文件、对外连接调用等操作,而这些操作往往会占用较长时间,且不需要事务,在这样的场景下,如果开发者不注意细节,就很容易出现系统宕机的问题。


假设在插入角色后还需要操作一个文件,那么我们要改造insertRole方法,如代码如下。

@Override@Transactional(propagation = Propagation.REQUIRED,        isolation= Isolati


假设doSomethingForFile方法是一个与数据库事务无关的操作,比如处理图片的上传之类的操作,但是笔者必须告诉读者这是一段糟糕的代码。


当insertRole方法结束后,Spring才会释放数据库事务资源,也就是说在运行doSomethingForFile方法时,Spring并没有释放数据库事务资源,而等到doSomethingForFile方法运行完成返回result后,才会关闭数据库资源。


在大型互联网系统中,一个数据库的链接可能也就是50条左右,然而同时并发的请求则可能是成百上千。


而其中大部分的并发请求都在等待这50条占有数据库连接资源的文件操作,假如平均一个doSomethingForFile的操作需要1秒,对于同时出现1 000条并发请求的网站,就会出现请求卡顿的状况。


因为大部分的请求都在等待数据库事务资源的分配,这是一个糟糕的结果,如图1所示。

49.png


图1 在事务中的文件操作

从图1可以看出,当操作文件这步占用较长时间时,数据库事务将长期得不到释放,这个时候如果发生高并发的情况,会造成大量的并发请求得不到数据库的事务资源而导致的系统宕机。

对此,应该修改为在Controller层中操作文件,代码如下。

@Autowiredprivate RoleService roleService = null;
public Role addRole(Role role) {    // 带事务方法    roleService.insertRole(role);    // 不需事务方法    doSomethingForFile();    return role;}


注意,当程序运行完insertRole方法后,Spring会释放数据库事务资源。


对于doSomethingForFile方法而言,已经在一个没有事务的环境中运行了,这样当前的请求就不会长期占用数据库事务资源,使得其他并发的请求被迫等待其释放了,这个改写分析如图2所示。49.png

图2 不在事务中的文件操作

从图2可以看出,在操作文件时,事务早已被关闭了,这时操作文件就避免了数据库事务资源被当前请求占用,从而导致其他请求得不到事务的情况发生了。

其实,不仅是文件操作,还有一些系统之间的通信及一些可能需要花费较长时间的操作,都要注意这个问题。


错误捕捉异常


模拟一段购买商品的代码,其中ProductService是产品服务类,而TransactionService是记录交易信息,需求显然就是产品减库存和保存交易在同一个事务里面,要么同时成功,要么同时失败,并且假设减库存和保存交易的传播行为都为REQUIRED,现在让我们来看如下代码。

@Autowiredprivate PrudoctService prudoctService;
@AutowiredPrivate TransactionService transactionService;
@Override@Transactional(propagation = Propagation.REQUIRED,        isolation = Isolation.READ_COMMITTED)public int doTransaction(TransactionBean trans) {    int resutl = 0;    try {        // 减少库存        int result =  prudoctService.decreaseStock(            trans.getProductId, trans.getQuantity());        // 如果减少库存成功则保存记录        if (result >0) {            transactionService.save(trans);        }    } catch(Exception ex) {        // 自行处理异常代码        // 记录异常日志        log.info(ex);    }    return result;}


这里的问题是方法已经存在异常了,由于开发者不了解Spring的事务约定,在两个操作的方法里面加入了自己的try…catch…语句。


这样实际也没有什么错误,只是显得冗余,之前我们分析过当PrudoctService的decreaseStock方法没有异常,而TransactionService的save方法发生异常时,也会发生事务的回滚,只是它会抛出“Transaction rolled back because it has been marked as rollback-only(事务看起来已经标注了只能回滚)”的异常,对于一些初级开发者可能就难以找到发生异常的原因了。


在那些需要处理大量异常的代码中,我们要小心这样的问题,避免代码复杂化,让定位问题出现很大的困难。有时候也确实需要我们自己处理异常,为此对上述代码进行改造,代码如下

@Autowiredprivate PrudoctService prudoctService;
@AutowiredPrivate TransactionService transactionService;
@Override@Transactional(propagation = Propagation.REQUIRED,        isolation = Isolation.READ_COMMITTED)public int doTransaction(TransactionBean trans) {    int resutl = 0;    try {        // 减少库存        int result =  prudoctService.decreaseStock(            trans.getProductId, trans.getQuantity());        // 如果减少库存成功则保存记录        if (result >0) {            transactionService.save(trans);        }    } catch(Exception ex) {        // 自行处理异常代码        // 记录异常日志        log.info(ex);        // 自行抛出异常,让Spring事务管理流程获取异常,进行事务管理        throw new RuntimeException(ex);    }    return result;}

注意,它抛出了一个运行异常,这样在Spring的事务流程中,就会捕捉到抛出的这个异常,进行事务回滚。这样在发生异常时,会更有利于定位,这才是合适使用数据库事务的方式。

} return result;}

注意,它抛出了一个运行异常,这样在Spring的事务流程中,就会捕捉到抛出的这个异常,进行事务回滚。这样在发生异常时,会更有利于定位,这才是合适使用数据库事务的方式。
相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
15天前
|
Java 关系型数据库 数据库
深度剖析【Spring】事务:万字详解,彻底掌握传播机制与事务原理
在Java开发中,Spring框架通过事务管理机制,帮我们轻松实现了这种“承诺”。它不仅封装了底层复杂的事务控制逻辑(比如手动开启、提交、回滚事务),还提供了灵活的配置方式,让开发者能专注于业务逻辑,而不用纠结于事务细节。
|
6月前
|
Java Spring
Spring中事务失效的场景
因为Spring事务是基于代理来实现的,所以某个加了@Transactional的⽅法只有是被代理对象调⽤时, 那么这个注解才会⽣效 , 如果使用的是被代理对象调用, 那么@Transactional会失效 同时如果某个⽅法是private的,那么@Transactional也会失效,因为底层cglib是基于⽗⼦类来实现 的,⼦类是不能重载⽗类的private⽅法的,所以⽆法很好的利⽤代理,也会导致@Transactianal失效 如果在业务中对异常进行了捕获处理 , 出现异常后Spring框架无法感知到异常, @Transactional也会失效
|
2月前
|
安全 Java Nacos
0代码改动实现Spring应用数据库帐密自动轮转
Nacos作为国内被广泛使用的配置中心,已经成为应用侧的基础设施产品,近年来安全问题被更多关注,这是中国国内软件行业逐渐迈向成熟的标志,也是必经之路,Nacos提供配置加密存储-运行时轮转的核心安全能力,将在应用安全领域承担更多职责。
|
6月前
|
Java 关系型数据库 数据库
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——常见问题总结
本文总结了Spring Boot中使用事务的常见问题,虽然通过`@Transactional`注解可以轻松实现事务管理,但在实际项目中仍有许多潜在坑点。文章详细分析了三个典型问题:1) 异常未被捕获导致事务未回滚,需明确指定`rollbackFor`属性;2) 异常被try-catch“吃掉”,应避免在事务方法中直接处理异常;3) 事务范围与锁范围不一致引发并发问题,建议调整锁策略以覆盖事务范围。这些问题看似简单,但一旦发生,排查难度较大,因此开发时需格外留意。最后,文章提供了课程源代码下载地址,供读者实践参考。
123 0
|
6月前
|
Java 关系型数据库 数据库
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——Spring Boot 事务配置
本文介绍了 Spring Boot 中的事务配置与使用方法。首先需要导入 MySQL 依赖,Spring Boot 会自动注入 `DataSourceTransactionManager`,无需额外配置即可通过 `@Transactional` 注解实现事务管理。接着通过创建一个用户插入功能的示例,展示了如何在 Service 层手动抛出异常以测试事务回滚机制。测试结果表明,数据库中未新增记录,证明事务已成功回滚。此过程简单高效,适合日常开发需求。
798 0
|
6月前
|
Java 数据库 微服务
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——事务相关
本文介绍Spring Boot事务配置管理,阐述事务在企业应用开发中的重要性。事务确保数据操作可靠,任一异常均可回滚至初始状态,如转账、购票等场景需全流程执行成功才算完成。同时,事务管理在Spring Boot的service层广泛应用,但根据实际需求也可能存在无需事务的情况,例如独立数据插入操作。
114 0
|
3月前
|
中间件 关系型数据库 Go
Go语言数据库编程:数据迁移与事务控制
本文介绍了《Go语言实战指南》中关于数据库编程的核心内容,涵盖使用 GORM 进行数据迁移与事务控制。主要内容包括:AutoMigrate 方法自动创建或更新表结构;事务控制的自动与手动实现方式;事务隔离级别的设置;以及在 Gin 框架中统一管理事务的实践建议。适合开发阶段的数据库结构管理和事务性操作需求。
|
2月前
|
SQL XML Java
配置Spring框架以连接SQL Server数据库
最后,需要集成Spring配置到应用中,这通常在 `main`方法或者Spring Boot的应用配置类中通过加载XML配置或使用注解来实现。
208 0
|
4月前
|
人工智能 Java 数据库连接
Spring事务失效场景
本文深入探讨了Spring框架中事务管理可能失效的几种常见场景及解决方案,包括事务方法访问级别不当、方法内部自调用、错误的异常处理、事务管理器或数据源配置错误、数据库不支持事务以及不合理的事务传播行为或隔离级别。通过合理配置和正确使用`@Transactional`注解,开发者可以有效避免这些问题,确保应用的数据一致性和完整性。
201 10
|
3月前
|
Java 关系型数据库 MySQL
【Spring】【事务】初学者直呼学会了的Spring事务入门
本文深入解析了Spring事务的核心概念与使用方法。Spring事务是一种数据库事务管理机制,通过确保操作的原子性、一致性、隔离性和持久性(ACID),维护数据完整性。文章详细讲解了声明式事务(@Transactional注解)和编程式事务(TransactionTemplate、PlatformTransactionManager)的区别与用法,并探讨了事务传播行为(如REQUIRED、REQUIRES_NEW等)及隔离级别(如READ_COMMITTED、REPEATABLE_READ)。
236 1

热门文章

最新文章