SqlSession的线程安全问题源码分析

🎮 作者主页:点击
🎁 完整专栏和代码:点击
🏡 博客主页:点击

文章目录

  • SqlSession 是线程安全的吗?
  • 为什么说是线程不安全的?
      • 事务管理问题
    • 数据库连接的共享问题
  • 一级缓存线程安全问题
  • 一级缓存占位符EXECUTION_PLACEHOLDER线程安全问题分析
  • 如何避免线程安全问题?
  • Spring是如何解决这个问题的?

SqlSession 是线程安全的吗?

SqlSession 本身并不是线程安全的。这意味着,不同线程不应当共享同一个 SqlSession 实例。如果在多线程环境下共享 SqlSession,可能会引发并发问题。
MyBatis 官方文档明确指出,SqlSession 是 非线程安全的,并且推荐每个线程都应该拥有独立的 SqlSession 实例。通常做法是为每个请求创建一个 SqlSession,并在操作完成后关闭它。
在这里插入图片描述

为什么说是线程不安全的?

事务管理问题

SqlSession 中包含了对事务的管理,事务在数据库连接上下文中是绑定的。如果多个线程同时使用同一个 SqlSession,就有可能在同一个事务中执行不同的操作,造成不可预知的结果。例如:

SqlSession sqlSession = sqlSessionFactory.openSession();
Thread thread1 = new Thread(() -> {sqlSession.update("update User set name = 'zhangsan' where id = 1");sqlSession.commit();  // 提交事务
});
Thread thread2 = new Thread(() -> {sqlSession.delete("delete from User where id = 2");sqlSession.commit();  // 提交事务
});thread1.start();
thread2.start();

在上述例子中,thread1 和 thread2 会同时操作同一个 SqlSession 实例,执行不同的 SQL 操作。如果 SqlSession 是线程安全的,两个线程的事务提交应该不会互相干扰,但实际上,由于事务是由同一个数据库连接维护的,在并发环境下会出现事务不一致、提交顺序错误等问题。因此,SqlSession 必须是每个线程独立的。

数据库连接的共享问题

SqlSession 会持有数据库连接,这些连接是不可共享的。多个线程如果共享同一个 SqlSession,就可能在同一时刻使用同一个数据库连接,这会导致连接池中的连接竞争,进而引发连接池溢出、死锁等问题。

一级缓存线程安全问题

MyBatis 支持缓存机制,包括一级缓存和二级缓存。一级缓存是 SqlSession 局部的缓存,它的生命周期与 SqlSession 一致。二级缓存是跨 SqlSession 的缓存,与 SqlSessionFactory 绑定。虽然二级缓存是线程安全的,但一级缓存的设计并没有考虑到并发情况下的安全性。
假设有两个线程同时使用同一个 SqlSession 查询数据,并且 SqlSession 内部的一级缓存被修改:

SqlSession sqlSession = sqlSessionFactory.openSession();
Thread thread1 = new Thread(() -> {User user1 = sqlSession.selectOne("select * from Users where id = 1");System.out.println(user1);
});
Thread thread2 = new Thread(() -> {User user2 = sqlSession.selectOne("select * from Users where id = 2");System.out.println(user2);
});thread1.start();
thread2.start();

这里,两个线程可能在同一个 SqlSession 中同时操作数据,SqlSession 内部的一级缓存会被并发修改,导致缓存中的数据不一致。一个线程查询缓存的数据可能是另一个线程未提交的内容,从而引发数据错误。

一级缓存占位符EXECUTION_PLACEHOLDER线程安全问题分析

org.apache.ibatis.executor.BaseExecutor#queryFromDatabase

    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List<E> list;localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);try {list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {localCache.removeObject(key);}// 存入缓存localCache.putObject(key, list);return list;}

这段代码定义了一个泛型方法 queryFromDatabase,其主要功能是从数据库查询数据并利用缓存机制优化查询性能。首先,方法通过 localCache.putObject(key, EXECUTION_PLACEHOLDER) 将查询的 key 和一个占位符存入本地缓存,表示该查询正在执行。接着,它调用 doQuery 方法进行实际的数据库查询操作,并将查询结果存入 list。查询完成后,无论成功与否,都会在 finally 代码块中移除缓存中的占位符。然后,查询结果 list 被存入缓存,以便后续相同的查询可以直接从缓存中获取,避免重复查询。若该查询为存储过程(StatementType.CALLABLE),则输出参数被存入 localOutputParameterCache。最后,方法返回查询结果列表 list。

public enum ExecutionPlaceholder {EXECUTION_PLACEHOLDER
}

【重点分析】为什么在查询数据库前将key插入缓存中,并且值是一个占位符呢?
ExecutionPlaceholder.EXECUTION_PLACEHOLDER 是一个查询标记,这个占位符可以避免在查询缓存时出现“脏读”,当多个线程同时查询同一个 key 的缓存,线程 A 还在数据库查询过程中,线程 B 也开始查询相同的 key,但此时线程 A 还没完成查询,缓存中的数据尚未更新,假设此时是同一个 SqlSession,因为cacheKey 是一模一样的,线程B会去一级缓存中取值,取出的数据就是旧的值。

MyBatis是如何解决这个问题的呢,它在执行数据库查询前,将改变缓存的值为一个“错误的标记值”,这个值是一个枚举类型,假设此时线程B过来,会经过下面的代码

 List<E> list;try {queryStack++;// 从一级缓存获取结果list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {// 若缓存获取不到,从数据库获取list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}

在执行 (List) localCache.getObject(key) ,此时获取到是标记的值EXECUTION_PLACEHOLDER,那么就是出现类型转换异常,Mybatis是直接通过设置一个异常标记值,直接抛出异常的方式避免这种多线程同一个SqlSession问题。SqlSession不是线程安全,所以尽量不要多个线程混用一个SqlSession,应该是一个线程一个SqlSession,每个线程独立的connection。

如何避免线程安全问题?

每个线程使用独立的 SqlSession:在多线程环境下,每个线程应该创建一个独立的 SqlSession 实例,避免共享实例。

请求范围内管理 SqlSession:对于 Web 应用程序,通常在每个请求中创建并使用 SqlSession,请求结束后关闭 SqlSession。

使用 ThreadLocal:如果需要在多个方法或类中共享 SqlSession,可以使用 ThreadLocal 来确保每个线程都有自己的 SqlSession 实例。

ThreadLocal<SqlSession> threadLocalSession = new ThreadLocal<SqlSession>() {@Overrideprotected SqlSession initialValue() {return sqlSessionFactory.openSession();}
};

Spring是如何解决这个问题的?

在 Spring 中,SqlSession 是通过 Spring 提供的事务管理和依赖注入机制来管理的。Spring 通过一系列的技术(如 @Transactional 注解、@Autowired 注解、TransactionManager 等)来避免 SqlSession 的线程安全问题,确保每个线程(通常是每个请求)都能使用一个独立的 SqlSession 实例。
Spring 会为每个请求创建独立的 SqlSession,并在请求结束时自动关闭,从而避免了线程共享 SqlSession 实例的问题。
Spring 内部使用 ThreadLocal 来为每个线程提供独立的 SqlSession。ThreadLocal 是一种线程局部存储机制,它可以确保每个线程都有自己的 SqlSession 实例。
具体来说,TransactionSynchronizationManager 类通过 ThreadLocal 维护了与当前事务相关的资源(如 SqlSession)。当一个请求(或一个线程)执行时,Spring 会将该请求的 SqlSession 实例绑定到当前线程的 ThreadLocal 中,这样其他线程就无法访问同一个 SqlSession 实例,从而避免了线程安全问题。

在 Spring 中,事务的生命周期通常由 PlatformTransactionManager 管理。当事务开始时,Spring 会在当前线程上通过 TransactionSynchronizationManager 来保存当前事务的信息。这个信息包括了当前事务管理器以及任何与事务相关的资源(如 SqlSession)。

    private SqlSession getSqlSession() {// 根据当前线程的事务上下文来获取 SqlSession 实例SqlSession session = (SqlSession) TransactionSynchronizationManager.getResource(sqlSessionFactory);if (session == null) {session = sqlSessionFactory.openSession();// 将 SqlSession 绑定到当前线程TransactionSynchronizationManager.bindResource(sqlSessionFactory, session);}return session;}

当 Spring 开始一个新的事务时,SqlSession 会被绑定到当前线程的 ThreadLocal 上。这个绑定操作使得每个线程都有自己的独立 SqlSession。
当线程请求 SqlSession 时,Spring 会首先从当前线程的 ThreadLocal 中获取已经绑定的 SqlSession,如果没有绑定的 SqlSession,则会通过 SqlSessionFactory 创建新的 SqlSession。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

gitlab的搭建及使用

1、环境准备 服务器准备 CentOS Linux release 7.9.2009 (Core)&#xff0c;内存至少4G。 修改主机名和配置ip地址 hostnamectl set-hostname <hostname> 关闭主机的防火墙 # 关闭防火墙 systemctl stop firewalld #临时关闭防火墙 systemctl disable firewalld …

【面试系列】深入浅出 Spring Boot

熟悉SpringBoot&#xff0c;对常用注解、自动装配原理、Jar启动流程、自定义Starter有一定的理解&#xff1b; 面试题 Spring Boot 的核心注解是哪个&#xff1f;它主要由哪几个注解组成的&#xff1f;Spring Boot的自动配置原理是什么&#xff1f;你如何理解 Spring Boot 配置…

2024国城杯 Web

这四道题目Jasper大佬都做了镜像可以直接拉取进行复现 https://jaspersec.top/2024/12/16/0x12%20%E5%9B%BD%E5%9F%8E%E6%9D%AF2024%20writeup%20with%20docker/ n0ob_un4er 这道题没有复现成功, 不知道为啥上传了文件, 也在 /tmp目录下生成了sess_PHPSESSID的文件, 但是就是…

SpringBoot教程(十四) SpringBoot之集成Redis

SpringBoot教程&#xff08;十四&#xff09; | SpringBoot之集成Redis 一、Redis集成简介二、集成步骤 2.1 添加依赖2.2 添加配置2.3 项目中使用之简单使用 &#xff08;举例讲解&#xff09;2.4 项目中使用之工具类封装 &#xff08;正式用这个&#xff09;2.5 序列化 &…

【开源免费】基于SpringBoot+Vue.JS校园社团信息管理系统(JAVA毕业设计)

本文项目编号 T 107 &#xff0c;文末自助获取源码 \color{red}{T107&#xff0c;文末自助获取源码} T107&#xff0c;文末自助获取源码 目录 一、系统介绍二、数据库设计三、配套教程3.1 启动教程3.2 讲解视频3.3 二次开发教程 四、功能截图五、文案资料5.1 选题背景5.2 国内…

FFmpeg 4.3 音视频-多路H265监控录放C++开发二十一.4,SDP协议分析

SDP在4566 中有详细描述。 SDP 全称是 Session Description Protocol&#xff0c; 翻译过来就是描述会话的协议。 主要用于两个会话实体之间的媒体协商。 什么叫会话呢&#xff0c;比如一次网络电话、一次电话会议、一次视频聊天&#xff0c;这些都可以称之为一次会话。 那为什…

git 中 工作目录 和 暂存区 的区别理解

比喻解释 可以把工作目录和暂存区想象成两个篮子&#xff1a; 工作目录是你把所有东西&#xff08;文件和更改&#xff09;扔进去的地方。你正在修改的东西都放在这里。暂存区则是你整理好的东西放进第二个篮子&#xff0c;准备提交给老板&#xff08;提交到仓库&#xff09;…

机器人C++开源库The Robotics Library (RL)使用手册(四)

建立自己的机器人3D模型和运动学模型 这里以国产机器人天机TR8为例,使用最普遍的DH运动学模型,结合RL所需的描述文件,进行生成。 最终,需要的有两个文件,一个是.wrl三维模型描述文件;一个是.xml运动学模型描述文件。 1、通过STEP/STP三维文件生成wrl三维文件 机器人的…

接口测试Day04-postman生成测试报告ihrm项目

测试报告-利用newman插件 安装node.js 安装 双击 .msi 文件&#xff0c;一路下一步安装即可。无需特殊设定。测试安装成功 npm -v 安装npm 安装newman 安装newman npm install -g newman试安装成功 newman -v安装newman插件 - 扩展版 npm install -g newman-reporter-htmlex…

使用Locust对Redis进行负载测试

1.安装环境 安装redis brew install redis 开启redis服务 brew services start redis 停止redis服务 brew services stop redis 安装Python库 pip install locust redis 2.编写脚本 loadTest.py # codingutf-8 import json import random import time import redis …

【Vim Masterclass 笔记01】Section 1:Course Overview + Section 2:Vim Quickstart

文章目录 Section 1&#xff1a;Course Introduction 课程概述S01L01 Course Overview 课程简介课程概要 S01L02 Course Download 课程资源下载S01L03 What Vim Is and Why You Should Learn It 何为 Vim&#xff1f;学来干啥&#xff1f;1 何为 Vim2 为何学 Vim Section 2&…

【服务器】上传文件到服务器并训练深度学习模型下载服务器文件到本地

前言&#xff1a;本文教程为&#xff0c;上传文件到服务器并训练深度学习模型&#xff0c;与下载服务器文件到本地。演示指令输入&#xff0c;完整的上传文件到服务器&#xff0c;并训练模型过程&#xff1b;并演示完整的下载服务器文件到本地的过程。 本文使用的服务器为云服…

高效使用AI完成编程项目任务的指南:从需求分析到功能实现

随着人工智能工具的普及&#xff0c;即便是零编程基础或基础薄弱的用户&#xff0c;也可以借助AI完成许多技术任务。然而&#xff0c;要高效地使用AI完成编程任务&#xff0c;关键在于如何清晰表达需求&#xff0c;并逐步引导AI实现目标。 在本文中&#xff0c;我们将通过开发…

vs2022编译opencv 4.10.0

参考&#xff1a;Windosw下Visual Studio2022编译OpenCV与参考区别在于&#xff0c;没有用cmake GUI&#xff0c;也没有创建build目录&#xff0c;直接用vs2022打开了C:\code\opencv目录&#xff0c;即CMakeLists.txt所在根目录。没有修改默认下载地址&#xff0c;采用手动下载…

centos7 免安装mysql5.7及配置(支持多个mysql)

一&#xff09; 下载免安装包&#xff1a; mysql下载地址: https://dev.mysql.com/downloads/mysql/下载时&#xff0c;选择以前5.7版本&#xff1a; image 下载第一个TAR压缩包&#xff1a; image 二&#xff09; 定义安装路径并解压安装包 1、假设需要把MySQL放到 /usr/local…

怎样在 Word 文档中插入附件(其他文件)?

在 Office &#xff08;比如 Word、Excel 等&#xff09;中是可以插入附件的&#xff0c;比如插入文本文档、视频文件、音乐文件等。本经验就来讲一讲&#xff0c;怎样在 Word 文档中插入附件或其他文件&#xff1f; 在打开的“对象”对话框中&#xff0c;单击【由文件创建】选…

springboot集成websokcet+H5开发聊天原型(二)

本文没有写完~~~~ 聊天相关数据结构&#xff1a; 我们初步设计了如下几个数据结构。 //存放 sessionId 与 userId 的map private Map<String,String> sessionId_userId new HashMap<>(); // 用于存储用户与群组的关联关系&#xff0c;键为用户ID&#xff0c;值…

vue实现下拉多选、可搜索、全选功能

最后的效果就是树形的下拉多选&#xff0c;可选择任意一级选项&#xff0c;下拉框中有一个按钮可以实现全选&#xff0c;也支持搜索功能。 在mounted生命周期里面获取全部部门的数据&#xff0c;handleTree是讲接口返回的数据整理成树形结构&#xff0c;可以自行解决 <div c…

Pytorch | 利用DTA针对CIFAR10上的ResNet分类器进行对抗攻击

Pytorch | 利用DTA针对CIFAR10上的ResNet分类器进行对抗攻击 CIFAR数据集DTA介绍算法流程 DTA代码实现DTA算法实现攻击效果 代码汇总dta.pytrain.pyadvtest.py 之前已经针对CIFAR10训练了多种分类器&#xff1a; Pytorch | 从零构建AlexNet对CIFAR10进行分类 Pytorch | 从零构建…

攻防世界web第六题upload1

这是题目&#xff0c;可以看出是个上传文件的题目&#xff0c;考虑文件上传漏洞&#xff0c;先随便上传一个文件试试&#xff0c;要求上传的是图片。 可以看到上传成功。 考虑用一句话木马解决&#xff0c;构造文件并修改后缀为jpg,然后上传。 <?php eval($_POST[attack])…