《精通Spring4.x 企业应用开发实战》第12章 Spring 的事务管理难点剖析

news2025/7/19 5:19:55

目录标题

  • 前言
  • 一、DAO 和事务管理的牵绊
  • 二、应用分层的迷惑
  • 三、事务方法嵌套调用的迷茫(事务传播行为)
    • 1.Spring 事务传播机制回顾
    • 2.相互嵌套的服务方法
  • 四、多线程的困惑
    • 1. Spring 通过单实例化 Bean 简化多线程问题
    • 2.启动独立线程调用事务方法
  • 五、联合军种作战的混乱
    • 1.Spring 事务管理器的应对

前言

汇总:《精通Spring4.x 企业应用开发实战》

一、DAO 和事务管理的牵绊

很少有使用 Spring 而不使用 Spring 事务管理器的应用,因此常常有人会问:是否用了 Spring, 就一定要用 Spring 事务管理器,否则就无法进行数据的持久化操作呢?事务管理器和 DAO 是什么关系呢?
也许是 DAO 和事务管理如影随形的缘故吧,这个看似简单的问题实实在在地存在着,从初学者心中涌出,萦绕在老把式的脑际。答案当然是否定的。我们都知道,事务管理的目的是保证数据操作的事务性(原子性、一致性、隔离性、持久性,即所谓的ACID)。脱离了事务,DAO 照样可以顺利地进行数据操作

二、应用分层的迷惑

1、Web、 Service 及 DAO 三层是Web 应用开发常见的模式,但有些开发人员却错误地认为: 如果要使用 Spring 的事务管理就一定要先进行三层的划分,更有甚者认为每层一定要先定义一个接口,然后再定义一个实现类。
2、对将“面向接口编程” 奉为圭臬,认为放之四海而皆准的论调,笔者并不是很赞同。
3、是的,“面向接口编程”是 Martin Fowler、Rod Johnson 这些大师所提倡的行事原则。如果拿这条原则去开发框架、产品或大型项目,怎么强调都不为过。
4、但是,对于一般的开发人员来说,也许做的是一个普通工程项目,往往只是一些对数据库增、删、查、改的功能。此时,过分强制“面向接口编程”除了会带来更多的类文件,并不会有什么好处。

三、事务方法嵌套调用的迷茫(事务传播行为)

1.Spring 事务传播机制回顾

Spring 事务的一个被讹传很广的说法是:一个事务方法不应该调用另一个事务方法,否则将产生两个事务。结果造成开发人员在设计事务方法时束手束脚,生怕一不小心就踩到地雷。

其实这是未正确认识 Spring 事务传播机制而造成的误解。Spring 对事务控制的支持统一在TransactionDefinition 类中描述,该类有以下几个重要的接口方法。
● int getPropagationBehavior():事务的传播行为。
● int getlsolationLevel():事务的隔离级别。
● int getTimeout():事务的过期时间。
● boolean isReadOnly():事务的读/写特性。

很明显,除了事务的传播行为,对于事务的其他特性,Spring 是借助底层资源的功能来完成的,Spring 无非充当了一个代理的角色但是事务的传播行为却是 Spring 凭借自身的框架提供的功能,是 Spring提供给开发者最珍贵的礼物,讹传的说法玷污了 Spring事务框架最美丽的光环。
所谓事务传播行为,就是多个事务方法相互调用时,事务如何在这些方法间传播。Spring 支持以下 7 种事务传播行为:
在这里插入图片描述Spring 默认的事务传播行为是 PROPAGATION_ REQUIRED,它适合绝大多数情况。如果多个 ServiveX#methodX()均工作在事务环境下(均被 Spring 事务增强),且程序中存在调用链 Service1#method1()->Service2#method2()->Service3# method3(),那么这3个服务类的3 个方法通过 Spring 的事务传播机制都工作在同一个事务中。

2.相互嵌套的服务方法

线程同步场景下,UserService#logon()方法内部调用了 UserService#updateLastLogonTime()和 ScoreService#addScore()方法,这两个类都继承于 BaseService。它们之间的类结构如图 12-1 所示:
在这里插入图片描述同一条线程下,UserService#logon()方法内部调用了 ScoreService#addScore()方法,二者分别通过Spring AOP 进行了事务增强,则它们工作在同一事务中。

同一个线程下,ScoreService#addScore()方法添加到了UserService#logon()方法的事务上下文中,二者共享同一个事务。所以最终的结果是UserService 的 logon()和 updateLastLogonTime()方法及 ScoreService 的 addScore()方法工作在同一个事务中。

具体验证代码,请看书或者语雀版。

四、多线程的困惑

1. Spring 通过单实例化 Bean 简化多线程问题

由于 Spring 的事务管理器是通过线程相关的 ThreadLocal来保存数据访问基础设施(Connection 实例)的,再结合 IoC 和 AOP 实现高级声明式事务的功能,所以 Spring 的事务天然地和线程有着千丝万缕的联系

我们知道 Web 容器本身就是多线程的,Web 容器为一个 HTTP 请求创建一个独立的线程(实际上大多数 Web 容器采用共享线程池),所以由此请求所涉及的 Spring 容器中的 Bean 也运行在多线程环境下。在绝大多数情况下,Spring 的 Bean 都是单实例的(singleton), 单实例 Bean 的最大好处是线程无关性,不存在多线程并发访问的问题,也就是线程安全的。

一个类能够以单实例的方式运行的前提是“无状态”,即一个类不能拥有状态化的成员变量。我们知道,在传统的编程中,DAO 必须持有一个 Connection, 而 Connection就是状态化的对象(非线程安全)。所以传统的 DAO 不能做成单实例的,每次要用时都必须创建一个新的实例。传统的 Service 由于内部包含了若干有状态的 DAO 成员变量,所以其本身也是有状态的。

但在 Spring 中,DAO 和 Service 都以单实例的方式存在。Spring 通过 ThreadLocal将有状态的变量(如 Connection 等)本地线程化,达到另一个层面上的“线程无关”,从而实现线程安全。Spring 不遗余力地将有状态的对象无状态化,就是要达到单实例化Bean 的目的。
由于 Spring 已经通过 ThreadLocal 的设施将 Bean 无状态化,所以 Spring 中的单实例 Bean 对线程安全问题拥有了一种天生的免疫能力。不但单实例的 Service 可以成功运行在多线程环境中,Service 本身还可以自由地启动独立线程以执行其他的 Service。

2.启动独立线程调用事务方法

下面对 logon()方法进行改造,让其在方法内部再启动一个新线程,在这个新线程中执行积分添加的操作,看看究竟会发生哪些事务行为,如代码清单12-12 所示。

package com.smart.multithread;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Service;
import org.apache.commons.dbcp.BasicDataSource;

/**
 * @author 陈雄华
 * @version 1.0
 */
@Service("userService")
public class UserService extends BaseService {
    private JdbcTemplate jdbcTemplate;
    private ScoreService scoreService;

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Autowired
    public void setScoreService(ScoreService scoreService) {
        this.scoreService = scoreService;
    }

    @Transactional
    public void logon(String userName) {
        System.out.println("before userService.updateLastLogonTime method...");
        updateLastLogonTime(userName);
        System.out.println("after userService.updateLastLogonTime method...");
        // ①在同一个线程中调用scoreService#addScore(),将运行在同一个事务中
//      scoreService.addScore(userName, 20);

        //②在一个新线程中执行scoreService#addscore(),将启动一个新的事务
        Thread myThread = new MyThread(this.scoreService, userName, 20);//使用一个新线程运行
        myThread.start();
    }

    // ③负责执行scoreService#addscore () 的线程类
    @Transactional
    public void updateLastLogonTime(String userName) {
        String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";
        jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
    }

    private class MyThread extends Thread {
        private ScoreService scoreService;
        private String userName;
        private int toAdd;
        private MyThread(ScoreService scoreService, String userName, int toAdd) {
            this.scoreService = scoreService;
            this.userName = userName;
            this.toAdd = toAdd;
        }

        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("before scoreService.addScor method...");
            scoreService.addScore(userName, toAdd);
            System.out.println("after scoreService.addScor method...");
        }
    }

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/multithread/applicatonContext.xml");
        UserService service = (UserService) ctx.getBean("userService");

        JdbcTemplate jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
        //插入一条记录,初始分数为10
        jdbcTemplate.execute("INSERT INTO t_user(user_name,password,score,last_logon_time) VALUES('tom','123456',10," + System.currentTimeMillis() + ")");


        //调用工作在无事务环境下的服务类方法,将分数添加20分
        System.out.println("before userService.logon method...");
        service.logon("tom");
        System.out.println("after userService.logon method...");
        jdbcTemplate.execute("DELETE FROM t_user WHERE user_name='tom'");

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述在①处,在主线程(main)中执行的 UserService#logon0方法的事务启动;在②处,其对应的事务提交。而在子线程(Thread-2)中执行的 ScoreService#addScore()方法的事务在③处启动;在④处提交其对应的事务。

所以可以得出这样的结论:在相同线程中进行相互嵌套调用的事务方法工作在相同的事务中。如果这些相互嵌套调用的方法工作在不同的线程中,则不同线程下的事务方法工作在独立的事务中

五、联合军种作战的混乱

1.Spring 事务管理器的应对

让我想起来了在EDM中,基于原先的mybatis,然后我又整合了MongoDB。造成了mybatis事务失效。
Spring 抽象的 DAO 体系兼容多种数据访问技术,它们各有特色、各有干秋。如Hibernate 是一个非常优秀的 ORM实现方案,但对底层 SQL 的控制不太方便; 而 MyBatis则通过模板化技术让用户方便地控制 SQL,但没有 Hibernate 那样高的开发效率;自由度最高的当然是直接使用 Spring JDBC 了,但它也是底层的,灵活的代价是代码的繁复。很难说哪种数据访问技术是最优秀的,只有在某种特定的场景下才能给出答案。所以在一个应用中往往采用多种数据访问技术,一般是两种,一种采用 ORM 技术框架,而另一种采用偏 JDBC 的底层技术;二者珠联璧合,形成联合军种,共同御敌。
但是,这种联合军种如何应对事务管理的问题呢?我们知道,Spring 为每种数据访问技术提供了相应的事务管理器,难道需要分别为它们配置对应的事务管理器吗?它们到底是如何协同工作的呢?这些层出不穷的问题往往压制了开发人员使用联合军种的想法。
其实,在这个问题上,我们低估了 Spring 事务管理的能力。如果用户采用了一种高端的 ORM 技术(Hibernate、JPA、JDO),同时还采用了一种 JDBC 技术(Spring JDBC、MyBatis):由于前者的会话(Session)是对后者连接(Connection)的封装,Spring 会“足够智能地”在同一个事务线程中让前者的会话封装后者的连接。所以,只要直接采用前者的事务管理器就可以了。表12-1 给出了混合数据访问技术框架所对应的事务管理器。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/347324.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用了 ChatGPT 后,我迫不及待的问了它这 10 个问题

前言20230208 日,ChatGPT 已经联手微软登录必应搜索引擎,谷歌、百度等巨头更是紧随其后着急麻慌的推出自己的 AI 聊天产品,有理由相信,传统搜索引擎时代已经结束,不远的未来,每个人家里都会有一个可以陪你聊…

数字芯片是怎样设计出来的?

芯片在我们的生活和工作中无处不在。例如,交通智能卡就嵌入了一颗带有微处理器、储存单元、芯片操作系统的芯片;而手机的主板则集成了数百颗芯片,有的负责无线电收发、有的负责功率放大、还有的负责存储照片和文件、处理音频,完成…

如何查找专用 IP 地址?

专用 IP 地址:这些地址在网络内部使用,例如,平板电脑、Wi-Fi 相机、无线打印机和台式电脑使用的家庭网络。这些类型的 IP 地址为设备提供了一种与路由器和专用家庭网络上的其他设备进行通信的方法。私有IP地址可以手动设置,也可以…

Mr. Cappuccino的第40杯咖啡——Kubernetes之Pod生命周期

Kubernetes之Pod生命周期Pod生命周期官方文档Pod的状态初始化容器案例钩子函数Exec命令TCPSocketHTTPGet案例容器探测Exec命令TCPSocketHTTPGet探测时间重启策略Pod生命周期官方文档 Pod生命周期官方文档 Pod的状态 pending:挂起,apiserver创建了pod资…

2月第2周榜单丨飞瓜数据B站UP主排行榜(哔哩哔哩平台)发布!

飞瓜轻数发布2023年2月6日-2月12日飞瓜数据UP主排行榜(B站平台),通过充电数、涨粉数、成长指数三个维度来体现UP主账号成长的情况,为用户提供B站号综合价值的数据参考,根据UP主成长情况用户能够快速找到运营能力强的B站…

python基于django+vue微信小程序的校园跑腿平台

随着计算机技术的高速发展,现代计算机系统已经从以计算为中心向以信息化处理为中心的方向发展。而校园跑腿服务系统,不仅需要在硬件上为现代社会的学生们提供一个学习知识,获取知识的环境,更要在软件上为愿意上课的人提供必要的便利。于是校园跑腿服务系统系统便应运而生。 目前…

企业与第三方供应商合作时,会存在哪些安全风险?

随着现代社会的发展,企业供应链、产业供应链已日渐成熟。其中,供应商与企业的关系也由最初的纯粹买卖关系发展成了合作伙伴关系。在整个供应链体系中,供应商与其受众承担着供应链中环环相扣的责任,可以说,企业安全的薄…

站在行业C位,谷医堂打开健康管理服务新思路

对于农村及贫困地区老百姓来说,由于交通因素和家庭经济条件制约,看病难致身体调理情况一直不太乐观,这也导致心理压力很大。然而,随着近年中医药产业崛起与快速发展,这种局面很快就会得到改观,以湖南谷医堂…

MySQL InnoDB表的碎片量化和整理(data free能否用来衡量碎片?)

网络上有很多MySQL表碎片整理的问题,大多数是通过demo一个表然后参考data free来进行碎片整理,这种方式对myisam引擎或者其他引擎可能有效(本人没有做详细的测试).对Innodb引擎是不是准确的,或者data free是不是可以参…

让逆向工程师们头疼的代码混淆,就像永远也走不出的“浪浪山”

目录 代码混淆究竟是什么? 如何做代码混淆? 代码混淆不等于加密 App 加固非一时之功 “我想离开浪浪山。” 在数次尝试破解某个App 时,某个逆向工程师无奈感慨道。 逆向工程师顾名思义就是把一个个完整的软件逆推,还原成一段段…

【MySQL】数据库操作

文章目录1、创建和管理数据库1.1 创建数据库1.2 查看数据库1.3 修改数据库1.4 删除数据库2、 创建表2.1 创建表CREATE2.2 创建表AS3、修改表3.1 添加列 ALTERT TABLE ADD3.2 修改列 ALTER TABLE MODIFY3.3 重命名列 ALTER TABLE CHANGE3.4 删除列 ALTER TABLE DROP4、重命名表 …

一文讲透丨如何破解安全应用容器架构的17大挑战!

众所皆知,云计算/云原生技术因能极大地提高云上资源利用率以及应用交付效率而被广泛采用。然而,云计算/云原生技术的发展也让用户遭受了更多高级威胁与攻击。如何构建有效的云原生安全管理体系应对层出不穷的安全威胁这一问题也一直受到千行百业用户的关…

Keras深度学习实战——使用深度Q学习进行SpaceInvaders游戏

Keras深度学习实战——使用深度Q学习进行SpaceInvaders游戏 0. 前言1. 问题与模型分析2. 使用深度 Q 学习进行 SpaceInvaders 游戏相关链接0. 前言 在《深度Q学习算法详解》一节中,我们使用了深度 Q 学习来进行 Cart-Pole 游戏。在本节中,我们将利用深度Q学习来玩“太空侵略…

CHAPTER 2 Zabbix界面操作

Zabbix界面操作2.1 Zabbix界面操作1.zabbix的web界面安装2.添加监控信息3.查看监控内容4.查看图像2.2 自定义监控与监控报警1.自定义监控1.1 说明1.2 预备知识2.实现自定义监控2.1 自定义语法2.2 agent注册2.3 在server端注册(web操作)2.4 查看监控图形2.3 监控报警1.第三方报警…

hive实现oracle merge into matched and not matched

create database cc_test; use cc_test; table1 可以理解为记录学生最好成绩的表。 table2可以理解为每次学生的考试成绩。 我们要始终更新table1的数据 create table table1 (id string ,maxScore string );create table table2 (id string ,score string );insert into table…

用于汽车传感器的混合点云语义压缩:性能评估

Hybrid Point Cloud Semantic Compression for Automotive Sensors: A Performance Evaluation https://arxiv.org/pdf/2103.03819.pdf 在自动驾驶中,车辆与车辆之间的信息共享起着重要作用。在所有传感器中,激光雷达产生的3D点云的数据量通常较高。因…

设计模式实践示例

以下是我针对自己所掌握的知识出的设计模式题目,有不足的地方欢迎指摘。 一、设计模式原则 设计模式原则有哪些? 1.开闭原则:对扩展开放,对修改关闭 2.接口隔离原则:每个接口只完成单独业务的部分,不要将…

设计模式之工厂模式(C++)

作者:翟天保Steven 版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处 一、工厂模式是什么? 工厂模式是一种创建型的软件设计模式。定义一个用于创建对象的工厂接口,并让工厂子类…

MybatisPlus------application文件配置新增SQL打印以及测试类编写(二)

MybatisPlus------application文件配置新增SQL语句打印以及测试类编写 增加日志打印,具体打印执行的SQL语句。 操作: 只需在application配置文件中增加如下配置: mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout…

快递员配送手机卡,要求当面激活有“猫腻”吗?

咨询:快递员配送手机卡,要求当面激活有“猫腻”吗?有些朋友可能在网上看到了一些关于快递小哥激活会采集信息的文章,所以觉得让快递小哥激活流量卡并不安全,其实,哪有这么多的套路,只要你自己在…