Postgresql源码(136)syscache/relcache 缓存及失效机制

相关

《Postgresql源码(45)SysCache内存结构与搜索流程分析》

0 总结速查

syscache:缓存系统表的行。通用数据结构,可以缓存一切数据(hash + dlist)。可以分别缓存单行和多行查询。

  1. syscache使用CatCache数组,定义了一些常用查询的结果集缓存,数据放到CatCache里面的dlist中存放。
  2. syscache查询接口
    • SearchSysCache系列接口时,key须按照cacheinfo的定义来查询
      • pg_class支持where relname = ? and relnamespace = ?的查询:SearchSysCache2(RELNAMENSP,k1,k2)
      • pg_class支持where oid = ?的查询:SearchSysCache1(RELOID,k1)
    • SearchSysCacheList系列接口时,可以使用少于定义的key去查询,例如
      • SearchSysCacheList1(AMPROCNUM, ObjectIdGetDatum(opfamilyoid));
      • SearchSysCacheExists4(AMPROCNUM, ObjectIdGetDatum(opfamily), ObjectIdGetDatum(opcintype), ObjectIdGetDatum(opcintype), Int16GetDatum(procno))
  3. syscache的查询条件(1个或多个健)组合成key,key经过hash后落到某一个dlist上,在用key按顺序遍历dlist确定哪个是想要的,dlist自带lru机制,访问到的会调整到前面。

relcache:缓存RelationData。

  1. relcache就是一张hash表保存RelationIdCache结构。
  2. RelationIdCache结构在进程初始化时分三阶段初始化:创建RelationIdCache hash表、从pg_filenode.map文件导入oid→relfilenode、从pg_internal.init文件导入RelationData(包括RelationDataRelationData->rd_relRelationData->rd_attr)。

失效机制

  1. 进程本地,维护了数组存放失效消息,在事务提交时决定写共享内存或只失效自己。
  2. 进程本地,每一层子事务都会维护一个Group结构(InvalidationMsgsGroup),指向消息数组中的几条属于自己的失效消息。
  3. 进程本地,每一个Group结构中,都会维护一个当前query的group(CurrentCmdInvalidMsgs)、之前消息的group(PriorCmdInvalidMsgs),在事务提交、回滚时,可以分别处理:
    • 事务提交:当前的和之前的都需要发送共享内存,被其他进程消费。
    • 事务回滚:当前的不管了;之前的需要失效本地缓存,不发送到共享内存。
  4. 失效时机:子事务/事务提交/回滚AtEOXact_Inval、AtEOSubXact_Inval、CommandCounterIncrement。

在这里插入图片描述

1 系统表

系统表记录的元数据用来组织整库的数据结构。

例如:create table t1(a int, b int)

  • 在pg_class中记录表名、表文件、行统计信息等等信息:说明表名存在,如何找到表文件等。
  • 在pg_attribute中记录列名、列类型等信息:说明表有哪些列、列类型等。
  • 在pg_type中增加一条和表名同名的复合类型:声明一个新的复合类型(a int, b int),类型名同表名。

2 系统表缓存

系统表是需要被高频访问的,所以PG为系统表设计了两种进程级缓存:

  1. syscache:缓存系统表tuple → 缓存行数据。
  2. relcache:缓存系统表RelationData(表模式信息) → 缓存表结构。

两种缓存保存的都是高频访问数据,可以充分利用cpu的cache,进一步减少访问延迟。

缓存为什么要放到进程本地?因为每个进程执行的业务可能完全不同,缓存的数据也会有差异,并且进程天然隔离,做到本地简单、高效。如果放到共享内存中,并发读写需要有非常精细的控制,肯定要引入锁、atomic等同步机制,得不偿失。

3 syscache(catalog cache)

syscache 以一个数组的形式存放在内存中,每一个数组位置存放一个CatCache,每一个CatCache直观上可以看做一个固定SQL的结果集,具体的数据结构参考这里:

《Postgresql源码(45)SysCache内存结构与搜索流程分析》
在这里插入图片描述

cacheinfo数组中保存着上面提到的这些“SQL”例如:


static const struct cachedesc cacheinfo[] = {.........[RELNAMENSP] = {RelationRelationId,ClassNameNspIndexId,KEY(Anum_pg_class_relname, Anum_pg_class_relnamespace),128},[RELOID] = {RelationRelationId,ClassOidIndexId,KEY(Anum_pg_class_oid),128},.........

功能上可以看做:

  • RELNAMENSP
    • 等价为:select * from pg_class where relname = ? and relnamespace = ?
    • 走索引:ClassNameNspIndexId
  • RELOID
    • 等价为:select * from pg_class where oid = ?
    • 走索引:ClassOidIndexId

查询出来的结果(tuple)存放在CatCache的dlist中,CatCache还支持一批数据缓存,具体在上面文章中介绍,不再展开。

初始化流程:

void
InitCatalogCache(void)
{int			cacheId;SysCacheRelationOidSize = SysCacheSupportingRelOidSize = 0;for (cacheId = 0; cacheId < SysCacheSize; cacheId++){SysCache[cacheId] = InitCatCache(cacheId,cacheinfo[cacheId].reloid,cacheinfo[cacheId].indoid,cacheinfo[cacheId].nkeys,cacheinfo[cacheId].key,cacheinfo[cacheId].nbuckets);SysCacheRelationOid[SysCacheRelationOidSize++] =cacheinfo[cacheId].reloid;SysCacheSupportingRelOid[SysCacheSupportingRelOidSize++] =cacheinfo[cacheId].reloid;SysCacheSupportingRelOid[SysCacheSupportingRelOidSize++] =cacheinfo[cacheId].indoid;}qsort(SysCacheRelationOid, SysCacheRelationOidSize,sizeof(Oid), oid_compare);SysCacheRelationOidSize =qunique(SysCacheRelationOid, SysCacheRelationOidSize, sizeof(Oid),oid_compare);qsort(SysCacheSupportingRelOid, SysCacheSupportingRelOidSize,sizeof(Oid), oid_compare);SysCacheSupportingRelOidSize =qunique(SysCacheSupportingRelOid, SysCacheSupportingRelOidSize,sizeof(Oid), oid_compare);CacheInitialized = true;
}

4 relcache

hash表缓存最常用的数据结构RelationData:

typedef struct RelationData
{RelFileLocator rd_locator;	/* relation physical identifier */SMgrRelation rd_smgr;		/* cached file handle, or NULL */int			rd_refcnt;		/* reference count */ProcNumber	rd_backend;		/* owning backend's proc number, if temp rel */bool		rd_islocaltemp; /* rel is a temp rel of this session */bool		rd_isnailed;	/* rel is nailed in cache */bool		rd_isvalid;		/* relcache entry is valid */bool		rd_indexvalid;	/* is rd_indexlist valid? (also rd_pkindex and* rd_replidindex) */bool		rd_statvalid;	/* is rd_statlist valid? */
...
...Form_pg_class rd_rel;		/* RELATION tuple */TupleDesc	rd_att;			/* tuple descriptor */Oid			rd_id;			/* relation's object id */LockInfoData rd_lockInfo;	/* lock mgr's info for locking relation */
...
...
} RelationData;

4.1 重要数据文件

pg_filenode.map

问题:在backend进程启动过程中,需要使用一张系统表,代码中是知道系统表具体oid的,oid对应磁盘上哪个文件,正常需要在pg_class中查询relfilenode,但是pg_class表还没加载。所以现在需要提供一个系统表oid → relfilenode的映射关系,可以找到一些最基础的系统表。

解法:pg_filenode.map提供了表oid到relfilenode的映射关系。

pg_relation_filenode函数可以查询表对应的relfilenode
在这里插入图片描述

pg_internal.init

问题:要构造一个RelationData需要访问pg_class、pg_arrtibute、pg_type等等系统表的数据,才能构造出来。但进程启动阶段,一些基础系统表的RelationData 如果每次扫描表再去构造效率会很差。

解法:pg_internal.init提供了预先计算好的系统表的 RelationData 结构。

4.2 初始化一阶段:RelationCacheInitialize

创建hash表RelationIdCache

RelationCacheInitializectl.keysize = sizeof(Oid);ctl.entrysize = sizeof(RelIdCacheEnt);RelationIdCache = hash_create("Relcache by OID", INITRELCACHESIZE,&ctl, HASH_ELEM | HASH_BLOBS);RelationMapInitialize();shared_map.magic = 0;		/* mark it not loaded */local_map.magic = 0;shared_map.num_mappings = 0;local_map.num_mappings = 0;active_shared_updates.num_mappings = 0;active_local_updates.num_mappings = 0;pending_shared_updates.num_mappings = 0;pending_local_updates.num_mappings = 0;

4.3 初始化二阶段:RelationCacheInitializePhase2

  • 读共享库的pg_filenode.map
  • 读共享库的pg_internal.init
void
RelationMapInitializePhase2(void)
{load_relmap_file(true, false);......if (!load_relcache_init_file(true)){// 失败了要兜底!formrdesc("pg_database", DatabaseRelation_Rowtype_Id, true,Natts_pg_database, Desc_pg_database);formrdesc("pg_authid", AuthIdRelation_Rowtype_Id, true,Natts_pg_authid, Desc_pg_authid);formrdesc("pg_auth_members", AuthMemRelation_Rowtype_Id, true,Natts_pg_auth_members, Desc_pg_auth_members);formrdesc("pg_shseclabel", SharedSecLabelRelation_Rowtype_Id, true,Natts_pg_shseclabel, Desc_pg_shseclabel);formrdesc("pg_subscription", SubscriptionRelation_Rowtype_Id, true,Natts_pg_subscription, Desc_pg_subscription);#define NUM_CRITICAL_SHARED_RELS	5	/* fix if you change list above */}
}

load_relmap_file加载pg_filenode.map

数据

typedef struct RelMapFile
{int32		magic;			/* always RELMAPPER_FILEMAGIC */int32		num_mappings;	/* number of valid RelMapping entries */RelMapping	mappings[MAX_MAPPINGS];pg_crc32c	crc;			/* CRC of all above */
} RelMapFile;(gdb) p shared_map
$1 = {magic = 5842711, num_mappings = 50, mappings = {{mapoid = 1262, mapfilenumber = 1262}, {mapoid = 2964, mapfilenumber = 2964}, {mapoid = 1213, mapfilenumber = 1213}, ......{mapoid = 1260, mapfilenumber = 1260},  {mapoid = 6115, mapfilenumber = 6115}, {mapoid = 0, mapfilenumber = 0}}, crc = 1938758537}

load_relcache_init_file加载pg_internal.init

在这里插入图片描述

4.4 初始化三阶段:RelationCacheInitializePhase3

  • 读非共享库的pg_filenode.map
  • 读非共享库的pg_internal.init
void
RelationMapInitializePhase3(void)
{load_relmap_file(false, false);if (IsBootstrapProcessingMode() ||!load_relcache_init_file(false)){// 失败了兜底!needNewCacheFile = true;formrdesc("pg_class", RelationRelation_Rowtype_Id, false,Natts_pg_class, Desc_pg_class);formrdesc("pg_attribute", AttributeRelation_Rowtype_Id, false,Natts_pg_attribute, Desc_pg_attribute);formrdesc("pg_proc", ProcedureRelation_Rowtype_Id, false,Natts_pg_proc, Desc_pg_proc);formrdesc("pg_type", TypeRelation_Rowtype_Id, false,Natts_pg_type, Desc_pg_type);#define NUM_CRITICAL_LOCAL_RELS 4	/* fix if you change list above */}
}

数据

typedef struct RelMapFile
{int32		magic;			/* always RELMAPPER_FILEMAGIC */int32		num_mappings;	/* number of valid RelMapping entries */RelMapping	mappings[MAX_MAPPINGS];pg_crc32c	crc;			/* CRC of all above */
} RelMapFile;(gdb) p local_map
{magic = 5842711, num_mappings = 17, mappings = {{mapoid = 1259, mapfilenumber = 1259}, {mapoid = 1249, mapfilenumber = 1249}, {mapoid = 1255, mapfilenumber = 1255}, ......{mapoid = 3455, mapfilenumber = 3455}, {mapoid = 0, mapfilenumber = 0}}, crc = 3752523506}

5 缓存同步

失效消息处理是通过共享内存和轮询的机制实现的。

5.1 进程本地失效消息记录

本地的操作在事务操作之前,不应该通知任何其他进程,所以机制上会先把需要失效的信息记录到进程本地InvalMessageArrays数组中,等事务提交时在做统一处理,这里先看下本地进程如何保存失效消息的。

例如relcache失效入口之一:

  • CacheInvalidateRelcache
    • PrepareInvalidationState
      • 构造TransInvalidationInfo结构,与子事务绑定
      • TransInvalidationInfo中记录了当前的InvalidationMsgsGroup和上一个InvalidationMsgsGroup。
      • InvalidationMsgsGroup里面记录了数组的起始位置和结束位置。
    • RegisterRelcacheInvalidation
      • AddRelcacheInvalidationMessage
        • 检查InvalMessageArrays数组中没有这一条
        • AddInvalidationMessage
          • 插入InvalMessageArrays数组中,并更新InvalidationMsgsGroup中记录的位置。

注意:InvalidationMsgsGroup的作用就是记录InvalMessageArrays数组中的起始、终止位置。

进程本地保存失效消息数据结构:
在这里插入图片描述

(为什么交nestmsg:最后一条失效消息的下一个)

5.2 进程提交、回滚时对失效消息的处理

见注释:

void
AtEOXact_Inval(bool isCommit)
{...if (isCommit){if (transInvalInfo->RelcacheInitFileInval)RelationCacheInitFilePreInvalidate();// 把当前的失效消息追加到prior中AppendInvalidationMessages(&transInvalInfo->PriorCmdInvalidMsgs,&transInvalInfo->CurrentCmdInvalidMsgs);// 顶层事务提交时:共享内存发送失效消息ProcessInvalidationMessagesMulti(&transInvalInfo->PriorCmdInvalidMsgs,SendSharedInvalidMessages);if (transInvalInfo->RelcacheInitFileInval)RelationCacheInitFilePostInvalidate();}else{// 顶层事务回滚时:只需要把自己的失效掉,不需要发送出去ProcessInvalidationMessages(&transInvalInfo->PriorCmdInvalidMsgs,LocalExecuteInvalidationMessage);}...
}

注意,当进程回滚时,为什么要把自己本地的失效掉?因为事务内的一些写、读操作,可能已经cache了一些会被回滚调的数据,cache没有mvcc机制,这里必须把回滚调(不可见)的数据失效掉,否则后面在读到这些数据就是脏读了。

5.3 CommandCounterIncrement触发本地失效

一个事务中执行了多个命令,但直到事务最终提交之前,这些更改都是暂时的。意味着在事务提交之前,肯定不会将失效消息发送到共享队列。但是,即使事务最终回滚,每个命令执行后的本地缓存仍需要反映这些暂时的更改,保证事物内的后续查询能拿到正确的结果。

CommandCounterIncrementAtCCI_LocalCacheCommandEndInvalidationMessages// 先把当前query造成的失效消息做 到 本地ProcessInvalidationMessages(&transInvalInfo->CurrentCmdInvalidMsgs, LocalExecuteInvalidationMessage)// 把当前的失效消息 追加到 历史消息中 PriorCmdInvalidMsgsAppendInvalidationMessages(&transInvalInfo->PriorCmdInvalidMsgs,&transInvalInfo->CurrentCmdInvalidMsgs);

5.4 为什么TransInvalidationInfo有两个Group?

InvalidationMsgsGroup记录消息队列中的起止位置,这几个消息是当前Group管理的。

TransInvalidationInfo中记录了两个Group?当前CurrentCmdInvalidMsgs、历史PriorCmdInvalidMsgs。

  • 当前的失效消息需要再每个命令执行后,应用到本地,保证事物内的后续SQL能查到正确的缓存数据。
  • 当前的失效消息在事务回滚时,不需要处理,只需要把历史PriorCmdInvalidMsgs做到本地即可。

typedef struct TransInvalidationInfo
{/* Back link to parent transaction's info */struct TransInvalidationInfo *parent;/* Subtransaction nesting depth */int			my_level;/* Events emitted by current command */InvalidationMsgsGroup CurrentCmdInvalidMsgs;/* Events emitted by previous commands of this (sub)transaction */InvalidationMsgsGroup PriorCmdInvalidMsgs;/* init file must be invalidated? */bool		RelcacheInitFileInval;
} TransInvalidationInfo;

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

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

相关文章

SpringBoot3.X配置OAuth

背景 之前在学习OAuth2时&#xff0c;我就有一个疑惑&#xff0c;OAuth2中有太多的配置、服务类都标注了Deprecated&#xff0c;如下&#xff1a; 显然这些写法已经过时了&#xff0c;那么官方推荐的最新写法是什么样的呢&#xff1f;当时我没有深究这些&#xff0c;我以为我放…

Ansible-template模块动态生成特定文件

文章目录 一、Jinja2介绍什么是主要特性安装基本用法进阶特性总结 Jinja2与Ansible关系1. 模板引擎2. Ansible 的依赖3. 变量和模板4. 动态生成配置5. 社区和生态系统总结 二、Ansible如何使用Jinja2使用template模块Jinja2文件中使用判断和循环Jinja2文件中使用判断语法 Jinja…

onload_tcpdump命令抓包报错Onload stack [7,] already has tcpdump process

最近碰到Onload 不支持同时运行多个 tcpdump 进程的报错&#xff0c;实际上使用了ps查询当时系统中并没有tcpdump相关进程存在。需要重启服务器本机使用onload加速的相关进程后才能使用onload_tcpdump正常抓包&#xff0c;很奇怪&#xff0c;之前确实没遇到这样的问题&#xff…

C++友元和运算符重载

目录 一. 友元 friend 1.1 概念 1.2 友元函数 1.3 友元类 1.4 友元成员函数 二. 运算符重载 2.1 概念 2.2成员函数运算符重载 2.3 成员函数运算符重载 2.4 特殊运算符重载 2.4.1 赋值运算符重载 2.4.2 类型转换运算符重载 2.5 注意事项 三、std::string 字符串类…

MyBatis-Plus分页查询

在实际开发中&#xff0c;对于大量数据的查询&#xff0c;可以通过分页查询的方式来减少查询量和提高查询效率。在 MyBatis-Plus 中&#xff0c;分页查询可以通过使用 Page 对象和 IService 接口提供的分页方法来实现。MyBatis-Plus 的分页插件 PaginationInnerInterceptor 提供…

基于Spring框架的分层解耦详解

博客主页&#xff1a;誓则盟约系列专栏&#xff1a;Java Web关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Java Web 三层架构&#xff1a; Java Web可以大致被分为三层架构&#xff1a;…

Win11 安装 PostgreSQL 数据库,两种方式详细步骤

文章目录 一、exe文件安装 &#xff08;推荐&#xff09;下载安装包1. 选择操作系统2. 跳转到EDB&#xff08;PostgreSQL 的安装包托管在 EDB上&#xff09;3. 选择版本点击下载按钮 安装1. 管理员打开安装包2. 选择安装目录3. 勾选安装项4. 设置数据存储目录5. 设置管理员密码…

【C++报错已解决】std::ios_base::floatfield

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

YOLOv8改进,YOLOv8主干网络替换为GhostNetV3(2024年华为提出的轻量化架构,全网首发),助力涨点

摘要 GhostNetV3 是由华为诺亚方舟实验室的团队发布的,于2024年4月发布。 摘要:紧凑型神经网络专为边缘设备上的应用设计,具备更快的推理速度,但性能相对适中。然而,紧凑型模型的训练策略目前借鉴自传统模型,这忽略了它们在模型容量上的差异,可能阻碍紧凑型模型的性能…

如何用ChatGPT制作一款手机游戏应用

有没有想过自己做一款手机游戏&#xff0c;并生成apk手机应用呢&#xff1f;有了人工智能&#xff0c;这一切就成为可能。今天&#xff0c;我们就使用ChatGPT来创建一个简单的井字棋游戏&#xff08;Tic-Tac-Toe&#xff09;&#xff0c;其实这个过程非常轻松且高效。 通过Cha…

从哪里下载高清解压视频素材?推荐五个优质素材资源网站

想制作吸引人的抖音小说推文&#xff0c;但不知道从哪里获取高清解压视频素材&#xff1f;今天就为大家推荐五个优秀的网站&#xff0c;帮助你轻松找到所需的素材&#xff0c;提升你的创作质量。 首先是蛙学网 作为国内顶级的短视频素材网站&#xff0c;蛙学网提供了丰富的4K高…

浅谈java异常[Exception]

一&#xff0e; 异常的定义 在《java编程思想》中这样定义 异常&#xff1a;阻止当前方法或作用域继续执行的问题。虽然java中有异常处理机制&#xff0c;但是要明确一点&#xff0c;决不应该用"正常"的态度来看待异常。绝对一点说异常就是某种意义上的错误&#xf…

SpringBoot使用validation进行自参数校验

一&#xff1a;介绍 在 SpringBoot 项目开发中&#xff0c;很多与数据库交互的参数需要校验数据正确性。很多小伙伴会把参数判断写进代码里&#xff0c;但是这种写法往往会有低可读性以及多处使用的时候&#xff0c;需要变更验证规则时&#xff0c;不易于维护等缺点。今天给大家…

Java之多态

文章目录 1. 多态1.1 多态的概念 2. 方法的重写3. 向上转型3.13.2 发生向上转型的时机 4. 动态绑定和静态绑定5. 什么是多态5.15.2 多态的优缺点 6. 避免在构造方法中调用重写的方法7. 向下转型![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/fd1fa83140d94f37ab3b88…

HTML+CSS 水滴登录页

文章目录 一、效果演示二、Code1.HTML2.CSS 三、实现思路拆分 一、效果演示 实现了一个水滴登录页的效果。页面包含一个水滴形状的登录框和两个按钮&#xff0c;登录框包括用户名、密码和登录按钮&#xff0c;按钮分别为忘记密码和注册。整个页面的设计非常有创意&#xff0c;采…

一些超好用的 GitHub 插件和技巧

聊聊我平时使用 GitHub 时学到的一些插件、技巧。 ‍ ‍ 浏览器插件 在我的另一篇博客 浏览器插件推荐 里提到过跟 GitHub 相关的一些插件&#xff0c;这里重复下&#xff1a; Sourcegraph&#xff1a;在线打开项目&#xff0c;方便阅读&#xff0c;将 GitHub 变得和 IDE …

【AI创作组】Matlab中进行符号计算

提示:代码一定要自己运行过才算数…… 1. 符号计算工具箱介绍 1.1 工具箱功能 MATLAB的符号计算工具箱,即Symbolic Math Toolbox,是一套强大的数学软件工具,它使得MATLAB具备了符号运算的能力。该工具箱提供了一系列函数,用于求解、绘制和操作符号数学方程。用户可以直接…

【Linux】修改用户名用户家目录

0、锁定旧用户登录 如果旧用户olduser正在运行中是无法操作的&#xff0c;需要先禁用用户登录&#xff0c;然后杀掉所有此用户的进程。 1. 使用 usermod 命令禁用用户 这将锁定用户账户&#xff0c;使其无法登录&#xff1a; sudo usermod -L olduser2. 停止用户的进程 如…

Woocommerce怎么分类显示产品?如何将Shopify的产品导入到Woocommerce?

WooCommerce作为WordPress的一个电子商务插件&#xff0c;功能强大、使用简洁&#xff0c;能够轻松集成到WordPress网站中&#xff0c;为用户提供了一个完整的在线商店解决方案&#xff0c;在国外还是挺受欢迎的。 Woocommerce怎么分类显示产品&#xff1f; 在Woocommerce中&a…

【微服务】springboot 实现动态修改接口返回值

目录 一、前言 二、动态修改接口返回结果实现方案总结 2.1 使用反射动态修改返回结果参数 2.1.1 认识反射 2.1.2 反射的作用 2.1.3 反射相关的类 2.1.4 反射实现接口参数动态修改实现思路 2.2 使用ControllerAdvice 注解动态修改返回结果参数​​​​​​​ 2.2.1 注解…