【项目设计】高并发内存池(三)[CentralCache的实现]
🎇C++学习历程:入门
- 博客主页:一起去看日落吗
- 持续分享博主的C++学习历程
博主的能力有限,出现错误希望大家不吝赐教- 分享给大家一句我很喜欢的话: 也许你现在做的事情,暂时看不到成果,但不要忘记,树🌿成长之前也要扎根,也要在漫长的时光🌞中沉淀养分。静下来想一想,哪有这么多的天赋异禀,那些让你羡慕的优秀的人也都曾默默地翻山越岭🐾。
💐 🌸 🌷 🍀
目录
- 💐1. centralcache
- 🌷1.1 centralcache整体设计
- 🌷1.2 centralcache结构设计
- 🌷1.3 centralcache核心实现
💐1. centralcache
🌷1.1 centralcache整体设计
当线程申请某一大小的内存时,如果thread cache中对应的自由链表不为空,那么直接取出一个内存块进行返回即可,但如果此时该自由链表为空,那么这时thread cache就需要向central cache申请内存了。
central cache的结构与thread cache是一样的,它们都是哈希桶的结构,并且它们遵循的对齐映射规则都是一样的。这样做的好处就是,当thread cache的某个桶中没有内存了,就可以直接到central cache中对应的哈希桶里去取内存就行了。
- central cache与thread cache的不同之处
central cache与thread cache有两个明显不同的地方,首先,thread cache是每个线程独享的,而central cache是所有线程共享的,因为每个线程的thread cache没有内存了都会去找central cache,因此在访问central cache时是需要加锁的。
但central cache在加锁时并不是将整个central cache全部锁上了,central cache在加锁时用的是桶锁,也就是说每个桶都有一个锁。此时只有当多个线程同时访问central cache的同一个桶时才会存在锁竞争,如果是多个线程同时访问central cache的不同桶就不会存在锁竞争。
central cache与thread cache的第二个不同之处就是,thread cache的每个桶中挂的是一个个切好的内存块,而central cache的每个桶中挂的是一个个的span。

每个span管理的都是一个以页为单位的大块内存,每个桶里面的若干span是按照双链表的形式链接起来的,并且每个span里面还有一个自由链表,这个自由链表里面挂的就是一个个切好了的内存块,根据其所在的哈希桶这些内存块被切成了对应的大小。
🌷1.2 centralcache结构设计
- 页号的类型
每个程序运行起来后都有自己的进程地址空间,在32位平台下,进程地址空间的大小是232;而在64位平台下,进程地址空间的大小就是264。
页的大小一般是4K或者8K,我们以8K为例。在32位平台下,进程地址空间就可以被分成 232 / 2 13 = 219 个页,在64位平台下,进程地址空间就可以被分成264 / 2 13 = 251 个页。页号本质与地址是一样的,它们都是一个编号,只不过地址是以一个字节为一个单位,而页是以多个字节为一个单位。
由于页号在64位平台下的取值范围是[0,251 ),因此我们不能简单的用一个无符号整型来存储页号,这时我们需要借助条件编译来解决这个问题。
#ifdef _WIN64typedef unsigned long long PAGE_ID;
#elif _WIN32typedef size_t PAGE_ID;
#else//linux
#endif
需要注意的是,在32位下,_WIN32有定义,_WIN64没有定义;而在64位下,_WIN32和_WIN64都有定义。因此在条件编译时,我们应该先判断_WIN64是否有定义,再判断_WIN32是否有定义。
- span的结构
central cache的每个桶里挂的是一个个的span,span是一个管理以页为单位的大块内存,span的结构如下:
//管理以页为单位的大块内存
struct Span
{PAGE_ID _pageId = 0; //大块内存起始页的页号size_t _n = 0; //页的数量Span* _next = nullptr; //双链表结构Span* _prev = nullptr;size_t _useCount = 0; //切好的小块内存,被分配给thread cache的计数void* _freeList = nullptr; //切好的小块内存的自由链表
};
对于span管理的以页为单位的大块内存,我们需要知道这块内存具体在哪一个位置,便于之后page cache进行前后页的合并,因此span结构当中会记录所管理大块内存起始页的页号。
至于每一个span管理的到底是多少个页,这并不是固定的,需要根据多方面的因素来控制,因此span结构当中有一个_n成员,该成员就代表着该span管理的页的数量。
此外,每个span管理的大块内存,都会被切成相应大小的内存块挂到当前span的自由链表中,比如8Byte哈希桶中的span,会被切成一个个8Byte大小的内存块挂到当前span的自由链表中,因此span结构中需要存储切好的小块内存的自由链表。
span结构当中的_useCount成员记录的就是,当前span中切好的小块内存,被分配给thread cache的计数,当某个span的_useCount计数变为0时,代表当前span切出去的内存块对象全部还回来了,此时central cache就可以将这个span再还给page cache。
每个桶当中的span是以双链表的形式组织起来的,当我们需要将某个span归还给page cache时,就可以很方便的将该span从双链表结构中移出。如果用单链表结构的话就比较麻烦了,因为单链表在删除时,需要知道当前结点的前一个结点。
- 双链表结构
根据上面的描述,central cache的每个哈希桶里面存储的都是一个双链表结构,对于该双链表结构我们可以对其进行封装。
//带头双向循环链表
class SpanList
{
public:SpanList(){_head = new Span;_head->_next = _head;_head->_prev = _head;}void Insert(Span* pos, Span* newSpan){assert(pos);assert(newSpan);Span* prev = pos->_prev;prev->_next = newSpan;newSpan->_prev = prev;newSpan->_next = pos;pos->_prev = newSpan;}void Erase(Span* pos){assert(pos);assert(pos != _head); //不能删除哨兵位的头结点Span* prev = pos->_prev;Span* next = pos->_next;prev->_next = next;next->_prev = prev;}
private:Span* _head;
public:std::mutex _mtx; //桶锁
};
需要注意的是,从双链表删除的span会还给下一层的page cache,相当于只是把这个span从双链表中移除,因此不需要对删除的span进行delete操作。
- central cache的结构
central cache的映射规则和thread cache是一样的,因此central cache里面哈希桶的个数也是208,但central cache每个哈希桶中存储就是我们上面定义的双链表结构。
class CentralCache
{
public://...
private:SpanList _spanLists[NFREELISTS];
};
central cache和thread cache的映射规则一样,有一个好处就是,当thread cache的某个桶没有内存了,就可以直接去central cache对应的哈希桶进行申请就行了。
🌷1.3 centralcache核心实现
- central cache的实现方式
每个线程都有一个属于自己的thread cache,我们是用TLS来实现每个线程无锁的访问属于自己的thread cache的。而central cache和page cache在整个进程中只有一个,对于这种只能创建一个对象的类,我们可以将其设置为单例模式。
单例模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。单例模式又分为饿汉模式和懒汉模式,懒汉模式相对较复杂,我们这里使用饿汉模式就足够了。
//单例模式
class CentralCache
{
public://提供一个全局访问点static CentralCache* GetInstance(){return &_sInst;}
private:SpanList _spanLists[NFREELISTS];
private:CentralCache() //构造函数私有{}CentralCache(const CentralCache&) = delete; //防拷贝static CentralCache _sInst;
};
为了保证CentralCache类只能创建一个对象,我们需要将central cache的构造函数和拷贝构造函数设置为私有,或者在C++11中也可以在函数声明的后面加上=delete进行修饰。
CentralCache类当中还需要有一个CentralCache类型的静态的成员变量,当程序运行起来后我们就立马创建该对象,在此后的程序中就只有这一个单例了。
CentralCache CentralCache::_sInst;
最后central cache还需要提供一个公有的成员函数,用于获取该对象,此时在整个进程中就只会有一个central cache对象了。
- 慢开始反馈调节算法
当thread cache向central cache申请内存时,central cache应该给出多少个对象呢?这是一个值得思考的问题,如果central cache给的太少,那么thread cache在短时间内用完了又会来申请;但如果一次性给的太多了,可能thread cache用不完也就浪费了。
我们这里采用了一个慢开始反馈调节算法。当thread cache向central cache申请内存时,如果申请的是较小的对象,那么可以多给一点,但如果申请的是较大的对象,就可以少给一点。
我们就可以根据所需申请的对象的大小计算出具体给出的对象个数,并且可以将给出的对象个数控制到2~512个之间。也就是说,就算thread cache要申请的对象再小,我最多一次性给出512个对象;就算thread cache要申请的对象再大,我至少一次性给出2个对象
//管理对齐和映射等关系
class SizeClass
{
public://thread cache一次从central cache获取对象的上限static size_t NumMoveSize(size_t size){assert(size > 0);//对象越小,计算出的上限越高//对象越大,计算出的上限越低int num = MAX_BYTES / size;if (num < 2)num = 2;if (num > 512)num = 512;return num;}
};
但就算申请的是小对象,一次性给出512个也是比较多的,基于这个原因,我们可以在FreeList结构中增加一个叫做_maxSize的成员变量,该变量的初始值设置为1,并且提供一个公有成员函数用于获取这个变量。也就是说,现在thread cache中的每个自由链表都会有一个自己的_maxSize。
//管理切分好的小对象的自由链表
class FreeList
{
public:size_t& MaxSize(){return _maxSize;}private:void* _freeList = nullptr; //自由链表size_t _maxSize = 1;
};
此时当thread cache申请对象时,我们会比较_maxSize和计算得出的值,取出其中的较小值作为本次申请对象的个数。此外,如果本次采用的是_maxSize的值,那么还会将thread cache中该自由链表的_maxSize的值进行加一。
因此,thread cache第一次向central cache申请某大小的对象时,申请到的都是一个,但下一次thread cache再向central cache申请同样大小的对象时,因为该自由链表中的_maxSize增加了,最终就会申请到两个。直到该自由链表中_maxSize的值,增长到超过计算出的值后就不会继续增长了,此后申请到的对象个数就是计算出的个数。(这有点像网络中拥塞控制的机制)
- 从中心缓存获取对象
每次thread cache向central cache申请对象时,我们先通过慢开始反馈调节算法计算出本次应该申请的对象的个数,然后再向central cache进行申请。
如果thread cache最终申请到对象的个数就是一个,那么直接将该对象返回即可。为什么需要返回一个申请到的对象呢?因为thread cache要向central cache申请对象,其实由于某个线程向thread cache申请对象但thread cache当中没有,这才导致thread cache要向central cache申请对象。因此central cache将对象返回给thread cache后,thread cache会再将该对象返回给申请对象的线程。
但如果thread cache最终申请到的是多个对象,那么除了将第一个对象返回之外,还需要将剩下的对象挂到thread cache对应的哈希桶当中。
//从中心缓存获取对象
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{//慢开始反馈调节算法//1、最开始不会一次向central cache一次批量要太多,因为要太多了可能用不完//2、如果你不断有size大小的内存需求,那么batchNum就会不断增长,直到上限size_t batchNum = std::min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));if (batchNum == _freeLists[index].MaxSize()){_freeLists[index].MaxSize() += 1;}void* start = nullptr;void* end = nullptr;size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);assert(actualNum >= 1); //至少有一个if (actualNum == 1) //申请到对象的个数是一个,则直接将这一个对象返回即可{assert(start == end);return start;}else //申请到对象的个数是多个,还需要将剩下的对象挂到thread cache中对应的哈希桶中{_freeLists[index].PushRange(NextObj(start), end);return start;}
}
- 从中心缓存获取一定数量的对象
这里我们要从central cache获取n个指定大小的对象,这些对象肯定都是从central cache对应哈希桶的某个span中取出来的,因此取出来的这n个对象是链接在一起的,我们只需要得到这段链表的头和尾即可,这里可以采用输出型参数进行获取。
//从central cache获取一定数量的对象给thread cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{size_t index = SizeClass::Index(size);_spanLists[index]._mtx.lock();//加锁//在对应哈希桶中获取一个非空的spanSpan* span = GetOneSpan(_spanLists[index], size);assert(span); //span不为空assert(span->_freeList); //span当中的自由链表也不为空//从span中获取n个对象//如果不够n个,有多少拿多少start = span->_freeList;end = start;size_t i = 0;size_t actualNum = 1;while ( i < batchNum - 1 && NextObj(end) != nullptr){end = NextObj(end);++actualNum;++i;}span->_freeList = NextObj(end); //取完后剩下的对象继续放到自由链表NextObj(end) = nullptr; //取出的一段链表的表尾置空
// span->_useCount += actualNum; //更新被分配给thread cache的计数_spanLists[index]._mtx.unlock(); //解锁return actualNum;}
由于central cache是所有线程共享的,所以我们在访问central cache中的哈希桶时,需要先给对应的哈希桶加上桶锁,在获取到对象后再将桶锁解掉。
在向central cache获取对象时,先是在central cache对应的哈希桶中获取到一个非空的span,然后从这个span的自由链表中取出n个对象即可,但可能这个非空的span的自由链表当中对象的个数不足n个,这时该自由链表当中有多少个对象就给多少就行了。
也就是说,thread cache实际从central cache获得的对象的个数可能与我们传入的n值是不一样的,因此我们需要统计本次申请过程中,实际thread cache获取到的对象个数,然后根据该值及时更新这个span中的小对象被分配给thread cache的计数。
需要注意的是,虽然我们实际申请到对象的个数可能比n要小,但这并不会产生任何影响。因为thread cache的本意就是向central cache申请一个对象,我们之所以要一次多申请一些对象,是因为这样一来下次线程再申请相同大小的对象时就可以直接在thread cache里面获取了,而不用再向central cache申请对象。
- 插入一段范围的对象到自由链表
此外,如果thread cache最终从central cache获取到的对象个数是大于一的,那么我们还需要将剩下的对象插入到thread cache中对应的哈希桶中,为了能让自由链表支持插入一段范围的对象,我们还需要在FreeList类中增加一个对应的成员函数。
//管理切分好的小对象的自由链表
class FreeList
{
public://插入一段范围的对象到自由链表void PushRange(void* start, void* end){assert(start);assert(end);//头插NextObj(end) = _freeList;_freeList = start;}
private:void* _freeList = nullptr; //自由链表size_t _maxSize = 1;
};
相关文章:
【项目设计】高并发内存池(三)[CentralCache的实现]
🎇C学习历程:入门 博客主页:一起去看日落吗持续分享博主的C学习历程博主的能力有限,出现错误希望大家不吝赐教分享给大家一句我很喜欢的话: 也许你现在做的事情,暂时看不到成果,但不要忘记&…...
2023年,35岁测试工程师只能被“优化裁员”吗?肯定不是····
国内的互联网行业发展较快,所以造成了技术研发类员工工作强度比较大,同时技术的快速更新又需要员工不断的学习新的技术。因此淘汰率也比较高,超过35岁的基层研发类员工,往往因为家庭原因、身体原因,比较难以跟得上工作…...
gitlab部署使用,jenkins部署使用
gitlab部署使用,jenkins部署使用在线安装gitlab下载gitlab安装gitlab使用gitlab设置中文修改管理员密码创建组,创建项目,创建用户jenkins下载jenkins安装jenkin使用jenkins更改管理员密码配置拉取代码配置登录gitlab拉取代码的账号密码配置项目配置gitlab仓库配置构…...
从零开始的机械臂yolov5抓取gazebo仿真(环境搭建篇下)
sunday功能包使用介绍以及开源 sunday我给自己机械臂的命名,原型是innfos的gluon机械臂。通过sw模型文件转urdf。Sunday项目主要由六个功能包sunday_description、sunday_gazebo、sunday_moveit_config、yolov5_ros、vacuum_plugin、realsense_ros_gazebo组成&…...
GCC编译器 MinGW的下载安装使用教程
哎 总所周知 gcc可以用来编译C 和C。在linux广泛应用,那么window怎么使用gcc呢。就要用到gcc的window工具----MInGW,安装好之后,直接可以在windows的dos界面编译。下面讲解安装使用过程。1.官网下载MinGW - Minimalist GNU for Windows downl…...
【项目实战】SpringMVC配置全局属性,是实现WebMvcConfigurer接口,还是直接继承WebMvcConfigurationSupport类?
一、说明 官方推荐以下两种方式来配置全局的SpringMVC的相关属性 方式一:实现WebMvcConfigurer接口(推荐)方式二:直接继承WebMvcConfigurationSupport类。介绍一下两者区别吧。 二、 WebMvcConfigurer介绍 WebMvcConfigurer是一个接口,用于配置全局的SpringMVC的相关属…...
房产营销、地产中介如何高效低成本获客?
数字化对企业而言,机遇和挑战并存。房产企业可借助数字化加强日益扩大的业务规模和业务领域管理,以提升管理效率,降低管理难度;基于数字化技术加强客户的服务体验,进而收集多业态客户和场景数据,拓展创新业…...
Kotlin-作用域函数
在对象的上下文中执行代码块。当您在提供lambda表达式的对象上调用这样的函数时,它会形成一个临时作用域。在此范围内,可以不带名称地访问对象。这样的函数称为作用域函数。 let run with apply also 作用域函数不会引入任何新的技术功能,但它…...
QNX7.1 交叉编译开源库
1.下载QNX7.1 SDK并解压 ITL:~/work/tiqnx710$ ls -l 总用量 16 drwxrwxr-x 4 xxx4096 1月 28 13:38 host -rwxrwxr-x 1 xxx 972 1月 28 13:38 qnxsdp-env.bat -rwxrwxr-x 1 xxx 1676 1月 28 13:38 qnxsdp-env.sh drwxrwxr-x 3 xxx 4096 1月 28 13:38 target xxxITL:~/work/ti…...
论文投稿指南——中文核心期刊推荐(外国语言)
【前言】 🚀 想发论文怎么办?手把手教你论文如何投稿!那么,首先要搞懂投稿目标——论文期刊 🎄 在期刊论文的分布中,存在一种普遍现象:即对于某一特定的学科或专业来说,少数期刊所含…...
Fabric系列 - 链码-内部链码的特性
(1)Fabric repo下的案例 Chaincode(1.4的目录结构) fabric/examples/chaincode/go ├── example02 #一个简单的转账合约 ├── eventsender #发送事件通知 ├── passthru #调用其他链码(或者其他channel的链码)example02 (转账) 一个简单的转账合约。该链码简单实…...
NetApp SnapCenter 备份管理 ——借助应用程序一致的数据备份管理,简化混合云操作
NetApp SnapCenter 简单、可扩展、赋权:跨 Data Fabric 的企业级数据保护和克隆管理 主要优势 • 利用与应用程序集成的工作流和预定义策略简化备份、恢复和克隆管理。 • 借助基于存储的数据管理功能提高性能和可用性,并缩短测试和开发用时。 • 提供基…...
Java内置队列和高性能队列Disruptor
一、队列简介 队列是一种特殊的线性表,遵循先入先出、后入后出(FIFO)的基本原则,一般来说,它只允许在表的前端进行删除操作,而在表的后端进行插入操作,但是java的某些队列运行在任何地方插入删…...
比特数据结构与算法(第四章_下)二叉树的遍历
本章将会详细讲解二叉树遍历的四种方式,分别为前序遍历、中序遍历、后续遍历和层序遍历。在学习遍历之前,会先带大家回顾一下二叉树的基本概念。学习二叉树的基本操作前,需要先创建一颗二叉树,然后才能学习其相关的基本操作&#…...
chatGPT是什么
2022年11月,人工智能公司OpenAI推出了一款聊天机器人:ChatGPT。它能够通过学习和理解人类语言来进行对话,还能与聊天对象进行有逻辑的互动。除了聊天,ChatGPT还能够根据聊天对象提出的要求,进行文字翻译、文案撰写、代…...
jenkins漏洞集合
目录 CVE-2015-8103 反序列化远程代码执行 CVE-2016-0788 Jenkins CI和LTS 远程代码执行漏洞 CVE-2016-0792 低权限用户命令执行 CVE-2016-9299 代码执行 CVE-2017-1000353 Jenkins-CI 远程代码执行 CVE-2018-1000110 用户枚举 CVE-2018-1000861 远程命令执行 CVE-2018…...
用canvas画一个炫酷的粒子动画倒计时
前言 😆 这是一篇踩在活动尾声的文章,主要是之前在摸鱼社群里有人发了个粒子动画的特效视频,想着研究研究写一篇文章出来看看,结果这一下子就研究了半个多月。 😂 下面就把研究成果通过文字的形式展现出来吧…...
Java技术学习——Maven相关知识
一、什么是Maven? Maven是Apache软件基金会组织维护的一款专门为Java项目提供构建和依赖管理支持的工具。 1.1 构建 构建过程包含的主要环节如下: 清理:删除上一次构建的结果,为下一次构建做好准备编译:Java源程序…...
C++ 认识和了解C++
1.在使用C语言写代码的时候开头要用到的是: #include<iostream> using namespace std;不可以写成这样: #include iostream.h(1)iostream是输入输出流类, istream输入流类 cin >> ostream输出流类 cout &…...
u盘误删的文件怎么找回
u盘误删的文件怎么找回?u盘的特点之一就是极其便携,可以容纳各种格式的数据和文件,需要时可以直接使用。每次使用都会或多或少的存放一些文件,但有使用就会有删除,为了不影响使用性,清理存储空间是必要的。清理中如果…...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...
智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...
CVPR 2025 MIMO: 支持视觉指代和像素grounding 的医学视觉语言模型
CVPR 2025 | MIMO:支持视觉指代和像素对齐的医学视觉语言模型 论文信息 标题:MIMO: A medical vision language model with visual referring multimodal input and pixel grounding multimodal output作者:Yanyuan Chen, Dexuan Xu, Yu Hu…...
MODBUS TCP转CANopen 技术赋能高效协同作业
在现代工业自动化领域,MODBUS TCP和CANopen两种通讯协议因其稳定性和高效性被广泛应用于各种设备和系统中。而随着科技的不断进步,这两种通讯协议也正在被逐步融合,形成了一种新型的通讯方式——开疆智能MODBUS TCP转CANopen网关KJ-TCPC-CANP…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
Spring Security 认证流程——补充
一、认证流程概述 Spring Security 的认证流程基于 过滤器链(Filter Chain),核心组件包括 UsernamePasswordAuthenticationFilter、AuthenticationManager、UserDetailsService 等。整个流程可分为以下步骤: 用户提交登录请求拦…...
QT开发技术【ffmpeg + QAudioOutput】音乐播放器
一、 介绍 使用ffmpeg 4.2.2 在数字化浪潮席卷全球的当下,音视频内容犹如璀璨繁星,点亮了人们的生活与工作。从短视频平台上令人捧腹的搞笑视频,到在线课堂中知识渊博的专家授课,再到影视平台上扣人心弦的高清大片,音…...
数据分析六部曲?
引言 上一章我们说到了数据分析六部曲,何谓六部曲呢? 其实啊,数据分析没那么难,只要掌握了下面这六个步骤,也就是数据分析六部曲,就算你是个啥都不懂的小白,也能慢慢上手做数据分析啦。 第一…...
Pandas 可视化集成:数据科学家的高效绘图指南
为什么选择 Pandas 进行数据可视化? 在数据科学和分析领域,可视化是理解数据、发现模式和传达见解的关键步骤。Python 生态系统提供了多种可视化工具,如 Matplotlib、Seaborn、Plotly 等,但 Pandas 内置的可视化功能因其与数据结…...
Spring事务传播机制有哪些?
导语: Spring事务传播机制是后端面试中的必考知识点,特别容易出现在“项目细节挖掘”阶段。面试官通过它来判断你是否真正理解事务控制的本质与异常传播机制。本文将从实战与源码角度出发,全面剖析Spring事务传播机制,帮助你答得有…...

