上集节要:
UNDO在ORACLE 里面是非常重要的组件,也是ORACLE能竞争过MSSQL,IBM等数据库的重要优势.后来MYSQL数据库的 innodb引撑也加入了UNDO特色.
UNDO是什么? 业界的叫法很多,中文名词也很多,然后对于理解它反而造成了混乱. 什么前滚,撤销,回滚.
实际上UNDO 就是UNDO .它只是暂时保存下过去的东西而已, 一个表空间.
好比我们要做某些事情需要临时空间来操作,也许叫工作空间. 当然我们的ORACLE也有个临时表空间. 不过我们的数据库里的数据很重要,干活之前得把原来的数据放在另外个地方保存起来.等活干好了,干完了,得到领导肯定的好评.那就可以把新数据存进数据库里面,原来的数据就可以废弃了!
所以UNDO实际修改数据临时把原来的数据存放的表空间, 太长了,简短说是,旧数据表空间!
那好了 我们有了旧数据表空间,保留旧数据可以干什么呢? 其实我们想到干什么就能干什么.
目前能干下面的事
1 我们活干失败了,领导不满意, 就要把数据恢复过来,或者叫还原回来
2 我们干活当中,是有个时间长度的,最小也要毫秒的时间.这个时候,别人来查数据,如果没有旧数据,那他该查什么呢? 等你活干完了,看新的数据吗? 可新数据不是他想要的.
好了 我们把上面1和2 拿数据库术语来说是
1 事务回滚 ROLLBACK,实列恢复. 实列恢复不仅需要REDO日志,也需要UNDO来参与
2 这叫读一致性,不能读脏,不能读已提交, CR块.ACID
一个事务的开始需要从UNDO里面要到东西才能干活. 得先获得UNDO表空间里的块.
事务叫做一系列的操作,可以通俗理解工作内容,工作步骤,工作流程. 翻译成IT术语就是一些列的SQL.
如果你写过存储过程的话,可以理解一个存储过程就一个事务单位.
所以UNDO和事务是挂勾的, 事务需要在UNDO里存放旧数据. 涉及到事务的地方有两个,一个是UNDO里面,另外一个是数据块里面的ITL槽. 叫做事务槽.
我们给事务分配个ID, 这个ID不是自增量,也不是时间量,而是跟UNDO有关的.
我们执行个事务如下:
update zfk set object_name='zfk' where object_id=9;
不提交它,然后查询事务表V$TRANSATCION; 该表的其他字段可以参考官方文档.
select ADDR,XIDUSN,XIDSLOT,XIDSQN,UBAFIL,UBABLK,UBASQN,UBAREC,STATUS,XID,FLAG,START_SCN from v$transaction
XID 05001B00DEAF000 是十六进制的事务ID
它是由 XIDUSN.XIDSLOT.XIDSQN组成的. 把值转换成16进制 5=>05; 27=>1B;45022=>AFDE . 然这个值是反的.
这三个是什么呢? XIDUSN是UNDO段编号, XIDSLOT是段里面的事务槽编号,XIDSQN是该事务槽中第几次被重用. 也就是重用次数.
从上面XID的编码可以推算出,同时活跃的XID多少? 取决于UNDO表空间里的段多少,以及每个段里面的事务槽是多少.
在10G以后,UNDO采用自动管理模式,下图是系统自动生成的10个段.另外一个是SYSTME的
来我们看看UNDO段,段头块里面的信息
这个图只是让大家在大脑里有个映像而已,重点看事务控制区,事务表和空闲块.
这篇不详细讲事务的事情! 我们主题是UNDO嘛!!
UNDO段很多人把它做成圆圈形式,我觉得容易误解人,还有这个前值映像.
UNDO 里面的段,段中的块会反复地重用! 不仅是重用而且 要覆盖旧数据.
这就容易导致 ORA-01555 快照太旧. 结合上面的图好像是时钟样,转一圈,不管三七二十一就把前面的块给覆盖掉了.不是这样的感觉的.
从UNDO段头的块里面看到有个空闲块池! 那么这就涉及两个问题, 问题一就是事务槽的事务被重用,另外个是UNDO块的重用.是两个不同对象的重用.
ORACLE 对UNDO 块的重用是有条件的, 优先使用空闲块,否则就扩展,再者重用!
应像这图样在大脑里形成映像, 灰色的代表已经使用的UNDO块,绿色代表是未使用的块,叫空闲块.
因此UNDO表空间中的数据在不同时段就会有不同的状态显示,在dba_undo_extents 数据字典里记录了UNDO中每个区段的状态.
SQL> SELECT tablespace_name, status, SUM (bytes) 1024 1024 as "Bytes(M)"
FROM dba_undo_extents
GROUP BY tablespace_name, status;
TABLESPACE_NAME STATUS Bytes(M)
---------------- --------- ----------
UNDOTBS2 EXPIRED 1.4375
UNDOTBS2 UNEXPIRED .375
UNDOTBS2 ACTIVE .125
UNDOTBS1 EXPIRED 22
其中:
ACTIVE :未提交的事务所占用的UNDO块,该块所关联的事务并未提交,用于实现读一致性,所以该数据不能被其它事务的数据所覆盖 。
UNEXPIRED:已经提交但未过期的UNDO块,该UNDO块关联的事务已经提交,但是仍受到undo retention参数保持时间的影响,当undo表空间中没有可用的数据块时,这些数据块会直接被覆盖而进行重用。
EXPIRED: 事务已经提交,而且UNDO块保存时间已经超过undo retention参数指定的时间,属于已经过期的数据.可以被随时重用。
需要说明的是,当设置表空间启动Guarantee特性时,UNEXPIRED类型的块就必须要等到undo_retention 指定时间过期后才能被重用。
这个UNDO段没有空闲块了,那只好重用某些块了! 那到底重用哪些UNDO块呢?
仔细阅读上面的文字可以理清下.
实际上我们通过红绿灯来表示
1 优先重用绿色的 EXPIRED
2 接着重用 黄色的 RETENTION 保护期900秒中的块. 当然你可以制定规则,不可以重用黄色的.你可以使用ALTER TABLESPACE undotbs1 RETENTION GUARANTEE 修改UNDO表空间的属性,强制黄色的不能重用
3 从其他的UNDO段里偷空闲的UNDO块用
另外说下 11G有个讨厌的参数如下
--设置UNDO自动调优,它会使的UNDO长期得不到释放
alter system set "_undo_autotune" = flase scope=spfile;
一般来说UNDO表空间给一个数据文件,在LINUX下能扩展到32GB,基本上能满足所有的业务.
然并卵的事情,32GB UNDO也要使用掉,ORACLE很贪吃的, 按照上面的原则基本上先扩展后重用,也就是说32GB过不鸟几个月就到达了.UNDO空间使用率100% 其实这个没啥关系,除非你的磁盘空间真心的少,不够用的话,只要新建个UNDO表空间,再进行空间切换,释放旧的UNDO空间的磁盘大小.
再说下 一个事务申请一个UNDO块来使用, 这个时候是以独占模式霸占整个块. 也就是说该UNDO块只要它一个事务可以写,其他事务不可以写. 只有等它写完了,还有空闲的空间,别的事务就可以写.
另外一个事务,只能在一个UNDO段里面工作,原则上不可以跨段,使用别的段里的UNDO块.
这就是说事务最多使用的UNDO块数量,取决于UNDO段. 超过了就容易报
ORA-1628 回滚段达到32765最大值
也就是说我们要控制事务的大小,不能让它产生太多旧数据了,方法是分批修改数据.
这里再谈下前值映像. 很早以前,我一致认为是整个数据块,保存在UNDO里面,否则UNDO使用那么多呢? 这个前值映像真的不好理解,它实际上就是 该列的原值. 不是块,也不是一整行.
ORA-01555 快照太旧 这个错误是怎么回事呢? 其实这个错误在很早以前大约10年前吧,还挺流行的. 意思是说我要的旧数据找不到了,被重用了!
大家一看到是被重用了,就是手UNDO段要么太小了,只有不够用,不能扩展才开始重用.要么是事务太频繁了. 这个是什么意思呢? 也就是说事务非常多,在不能扩展的情况下,事务需要重用块.很不幸你需要的块被中招重用了.
这要从多方面优化, 1 是增加UNDO表空间大小.2优化你自己的查询语句或者其他DML语句.让其查询时间变短; 3 是优化大事务,让其使用更少的块.
当中了解到 事务必须先到UNDO表空间里搞定个事务ID 才能开始干活.
拿到了事务ID编号,同时拿到了一块UNDO块来保留旧数据,就可以向目标数据块ITL槽申请位置. 这样我们有一些对像来协调工作了.它们就是
1 UNDO 段块中的事务表
2 UNDO 块里面,旧数据记录
3 数据块 上面的ITL
4 数据块的 ROW
先看第一个,段头块回顾图
事务表的段信息如下
index:标识事务表中的行
state:表示事务状态 9代表非活跃,10代表活跃;
cflags:表示事务状态:0x0无事务;0x10死事务;0x80活动事务;0x90回滚事务;
wrap#:事务槽使用次数
uel:当事务活动时,指向下个可用的事务槽(不重要)
scn:事务开始SCN,或者提交SCN
dba: 事务最后使用的undo块地址
nub:使用多少个UNDO块
cmt:提交时间(不重要)
从这些字段可以大概了解下,有些什么功能,无非是为了回滚准备着. SCN+DBA+NUB 这三个字段就能获得回滚开始地方和结束的地方.
事务拿到事务XID后,这个XID前面说过由UNDO段号,+Index,+wrap# 这三个组成.
事务拿到了XID后, 它执行个UPDATE TABLE SET NAME='ZFK' WHERE ID=9;这样的工作, 假如这语句 要更新10笔记录,当这10笔记录分布在5个块中,假设每个块,这里是指数据块,包含15笔记录.目标10笔记录很均匀分布下去,其中有两笔需要更新的.
事务不是一口气把5个块一起修改的,虽然做为人类看是这样的,实际上它先要拿到第一个数据块来修改,修改好了,依次逐渐去修改其他块中的数据.然后整体提交.
那我们看第三个对象 数据块
下面是数据块的逻辑图
数据块信息
这个图有三部分构成
1 是ITL 事务槽
2 是行记录
3 行结构和数据
当事务拿到了事务XID,就要向数据块上的ITL申请,申请到了就把XID放进对应的位置上.
数据块的ITL字段信息
ITL:事务槽号;
XID:事务ID;
UBA: undo块里面的undo记录地址
FLAG:事务当前状态 ----活动;U 快速提交; C 提交并清除;B T 不重要状态;
LCK 锁定行数
SCN/FSC SCN 提交的SCN; FSC 空闲空间审计(不重要)
2 行记录 当中的 pri[0] offset=0x1f85 表示行号和偏移量, 这些值可以在下面的行结构和值中找到
1 行结构和数据
tab 0 ,row 2,@0x1f85
tl:9, fb:--H-FL-- Ib:0x1 cc:2
col 0: [2] c1 04
col 1: [2] c1 64
其中 row 2 @0x1f85 对应行记录中的数据; COL 0 表示第一个字段 [2] 表示该字段实际长度; 后面的 C1 04 表示实际的数据.
最重要的是 lb:0x1 表示指向数据块中的事务槽号.
当事务申请到了事务槽号后,通过行记录(目录)(索引项) 找到了目标行,更改LB指针,指向自己的事务槽号.
然后把行当中的某列的值和当前事务槽原来的事务信息. 因为事务槽也会被重用,一起打包保留到UNDO块中.
最后该轮到主角上场啦!! 压轴戏 先看逻辑图
它同样有三个区域
1控制区
2 记录索引区
3 记录区
下面是块的内容: 重点不是要看懂
第一部分 控制区 UNDO BLK:
第二部分: REC OFFSET
第三部分: *---------------------------------
第一部分 XID 就知道该块归哪个事务拥有
第二部分 讲的是UNDO记录和偏移量
第三部分才是重点 又细分三个部分
第1小部分 从*--- 到*----- 算主要是讲事务槽和上个记录
stl 表示该记录是哪个UNDO事务槽号的. 这跟数据块中的行指针 LB 一样的.
opc 表示操作类型
rci 表示上个记录的本块行号
rdba 表示 上个记录的UNDO块地址 外加 irb信息
第2小部分 从*---- 到col 这里主要记录数块中的ITL信息
itli:数据块事务槽号
uba:数据块事务槽号中的uba
第3小部分 就是剩下的 COL 旧数据啦
这样我们就把旧数据安全保存在UNDO块中了.
假设我们已经把5个块,10个目标行修改完后,发出COMMIT命令,这个事务就结束了.那么这5个块上的事务标志FLAGS 都表示C; 另外有个情况是前3个快被DBWR写入了磁盘,为了性能,不能从磁盘再读取出来,做个事务清除工作.留到日后别的事务或者是查询再读取的时候,顺便做一下.
假如 我们修改了前3个块,人工发出ROLLBACK,或者断电进行实列恢复,或者回话被KILL掉了. 我们就要撤销该事务的更新,也就是做回滚操作.
回滚操作:
1 通过UNDO段头上的事务表信息,找到死的事务
2 找到该事务的最后一个块DBA字段 UNDO 记录 跟 数据块UBA一样.
3 到了UNDO块里,读取该记录 恢复 记录当中的旧数据和旧的ITL信息到数据块中
4 然后读取UNDO记录当中的 RCI RDBA IRB等指针信息 找到事务上一条UNDO记录.
这样就完成了回滚链条 可以想象 这是个反向更新操作, 同样资源的消耗,修改数据块,写日志.
读一致性:
1 找到目标行,发现行中的IB 指向的ITL 中的SCN 比自己的大
2 把当前块复制到内存其他区域中
3 对所有提交的事务做清除工,反向更改未提交的事务. (其实就是做块恢复操作)
4 通过行指针指向的ITL中的UBA 找到UNDO块中的记录信息
5 把UNDO块中的记录 旧数据和旧的ITL还原回来
6 再对比还原回来的旧ITL中的SCN,还大的话 继续读取UBA的指针,又去读UNDO块中的记录.重复循环,直到恢复所需要的SCN跟自己一样的
这样就恢复一个独特的数据块出来,属于该回话私有的. 这就是CR块!
这样 UNDO块中的旧记录 通过两个指针完成 回滚和读一致性!
ORA-01555 快照过旧 意思说 读一致性的循环查找 UBA 所指向的UNDO块记录 已经不存在了.被覆盖了! 好比说 你的一个查询 从早上9点中查到下午5点钟,跑了8个小时.当最后这个块在这8个小时中被修改了1千次. 就产生 ITL->UBA->ITL->UBA 这样的1000个链条.
可惜的是这个链条前面最初10点的UNDO块已经超过了900秒,并且UNDO段已经没有空闲UNDO块了,这样它不可避免的被其他事务所征用了. 自然你的查询,当查到该块的时候,已经无法恢复到9点的数据了.
从前面文章得知,当事务提交后会在数据块中的ITL该XID 标帜字段FLAG 打上C,并在SCN/FCN字段上打上自己提交的SCN.
如果说当一个事务更新了1万个块,更新时间比如说30分钟.
这个时候会因为DBWR把被更新的数据块写回磁盘中.
假如提交的时候已经有8千个块写入了磁盘.那么做提交命令的时候,需要把块上的XID信息修改下,如上面的动作一样,还有解除行上的锁字节. 那么是否要把写进磁盘的8000个块再次读入到内存中进行事务修改呢?
ORACLE 为了性能 做了下面三个步骤完成提交 修改事务的动作
一 如果块比较少,都在内存中未被写入磁盘,那么做个完整的事务提交修改工作.
二 部分或者大部分写入磁盘的话,先做还在内存中的块事务提交修改工作,在FLAG上打上U标帜,并写入SCN. 并设置会话内存中的10%为保留值,保留多少修改的块. 实际上是个列表.
三 对已经写入磁盘的块,做延迟块清除(延迟事务提交工作). 就是说等下个回话读取该块的时候做.
这里要牢记一点就是 数据块里的ITL,UNDO段里的ITL,以及UNDO块 都会被重用,被覆盖掉.
当从磁盘读取块的时候,发现有些事务没有提交,那么拿着事务XID去UNDO里面找,如果找到了就把该事务的SCN写入数据块ITL对应的信息中.
如果找不到呢? 这个时候表示UNDO段的ITL被重用了,同样事务被重用之前,新事务会把ITL信息登记在自己UNDO块中记录里面,这个记录在事务的首条记录里面.
不过UNDO段的ITL里面没有跟数据块UBA字段. 这个字段被移植到事务控制区中. 事务控制区有关ITL槽号被重用的信息.
信息字段如下
seq: 0x08f5
chd:0x00b
ctl:0x0017
inc:0x000000
nfb:0x0001
mgc:0xb000
xts:0x0068
flg:0x0001
opt:2147483646(0x7ffffffe)
uba:0x180120f.08f5.24
scn:0x0000.018bc75e
其他字段不重要,重要是UBA和SCN. SCN是最近重用时被覆盖的前事务的提交的SCN. UBA指向前前的链条指针.也就是事务开始的首条UNDO记录.
uba:0x0180120f.8f5.21 ctl max scn:0x0000.018bc704
prv tx scn:0x0000.018bc75e
txn start scn :scn: 0x0000.018bcd3e logon user:86
prev brb:25170445 prev bcl:0
prv tx scn:0x0000.018bc75e <=表示被覆盖的提交SCN
txn start scn :scn: 0x0000.018bcd3e <=表示事务开始的SCN
prev brb:25170445 <=十六进制是0x0180120D 事务最后的UNDO块地址
这里DUMP出来的结构信息好像少了个WRAP#值. 没关系我们理解原理. 通过事务的首条unod记录里面的uba 地址 可以追溯更早被覆盖的Undo段的ITL信息.
疑惑是 读一致性: 当一个查询找到该块,如何通过行来发现该行的是否被修改提交过. 怎么样找到ITL里面 哪个事务槽影响了该行?
实际上它不需要通过行来判断是哪个事务ITL, 而是把整个块中ITL里面已提交的SCN 来对比,凡是大于查询回话开始的SCN 就要回退. 也就是说ITL 大于的 都要回退 通过UBA来. 这样就构造出 CR块
鱼尾问题是 做延迟块清除的时候,如何判断当前的SCN 是自己要的SCN 呢? 如何在控制区里面找到属于自己事务槽的呢?
经过仔细研究 推到出是 通过恢复UNDO首条记录里面的内容,每恢复一次就减掉当前的WRAP#值,直到减到跟自己的WRAP#值一样为止.
如何在控制区里面找到属于自己事务槽的呢? 目前发现只有一条事务控制区记录. 或许每次延迟块清楚 都要把UNDO段头块做恢复.也跟读一致性一样 单独复制到内存区里去做回滚.
当然这样性能比较低下, 如果事务控制区记录了每条事务槽的重用信息的话,这样性能会更高
这个是事务控制区的内容 通过
alter system dump undo header '_SYSSMU2_2232571081$';
TRN CTL:: seq: 0x268a chd: 0x001d ctl: 0x000a inc: 0x00000000 nfb: 0x0000
mgc: 0xb000 xts: 0x0068 flg: 0x0001 opt: 2147483646 (0x7ffffffe)
uba: 0x00c001ef.2682.28 scn: 0x0000.16c326e5
通过UBA提供的UNDO块地址 DUMP出UNDO内容,先计算出文件和块号
select dbms_utility.data_block_address_file(to_number('00c001ef',
'xxxxxxxxxxxx')) file_id,
dbms_utility.data_block_address_block(to_number('00c001ef',
'xxxxxxxxxxxx')) block_id
from dual;
然后DUMP:
alter system dump datafile 3 block 495;
内容如下 从蓝色字体看起
*** 2017-02-28 15:41:26.505
Start dump data blocks tsn: 2 file#:3 minblk 495 maxblk 495
Block dump from cache:
Dump of buffer cache at level 4 for tsn=2 rdba=12583407
BH (0x20af88f30) file#: 3 rdba: 0x00c001ef (3/495) class: 20 ba: 0x20a3cc000
set: 9 pool: 3 bsz: 8192 bsi: 0 sflg: 1 pwc: 240,28
dbwrid: 0 obj: -1 objn: 0 tsn: 2 afn: 3 hint: f
hash: [0x2593c5220,0x1d8fc0110] lru: [0x1f5f8d598,0x1c4f6da98]
ckptq: [NULL] fileq: [NULL] objq: [0x1f3febd00,0x1c4f6dac0] objaq: [0x1f5f8d5d0,0x1c4f6dad0]
st: XCURRENT md: NULL fpin: 'ktuwh03: ktugnb' tch: 13
flags: block_written_once redo_since_read
LRBA: [0x0.0.0] LSCN: [0x0.0] HSCN: [0xffff.ffffffff] HSUB: [388]
Block dump from disk:
buffer tsn: 2 rdba: 0x00c001ef (3/495)
scn: 0x0000.16c329b0 seq: 0x1e flg: 0x04 tail: 0x29b0021e
frmt: 0x02 chkval: 0x3af8 type: 0x02=KTU UNDO BLOCK
Hex dump of block: st=0, typ_found=1
Dump of memory from 0x00007F4606E8CA00 to 0x00007F4606E8EA00
7F4606E8CA00 0000A202 00C001EF 16C329B0 041E0000 [.........)......]
7F4606E8CA10 00003AF8 00070002 0000B03B 28462682 [.:......;....&F(]
********************************************************************************
UNDO BLK:
xid: 0x0002.007.0000b03b seq: 0x2682 cnt: 0x46 irb: 0x28 icl: 0x0 flg: 0x0000
Rec Offset Rec Offset Rec Offset Rec Offset Rec Offset
---------------------------------------------------------------------------
0x01 0x1f60 0x02 0x1e48 0x03 0x1dd4 0x04 0x1d50 0x05 0x1ce4
0x06 0x1c80 0x07 0x1c2c 0x08 0x1b9c 0x09 0x1b14 0x0a 0x1a8c
0x0b 0x19fc 0x0c 0x1974 0x0d 0x18ec 0x0e 0x185c 0x0f 0x17e4
0x10 0x175c 0x11 0x16cc 0x12 0x1654 0x13 0x15cc 0x14 0x153c
0x15 0x14c4 0x16 0x143c 0x17 0x1328 0x18 0x12c0 0x19 0x1268
0x1a 0x1214 0x1b 0x11b8 0x1c 0x1144 0x1d 0x10e8 0x1e 0x1094
0x1f 0x100c 0x20 0x0f80 0x21 0x0f18 0x22 0x0e30 0x23 0x0dc8
0x24 0x0d70 0x25 0x0ce8 0x26 0x0c88 0x27 0x0bfc 0x28 0x0b74
0x29 0x0b18 0x2a 0x0abc 0x2b 0x0a60 0x2c 0x0a04 0x2d 0x09a8
0x2e 0x094c 0x2f 0x08f0 0x30 0x0894 0x31 0x0838 0x32 0x07dc
0x33 0x0780 0x34 0x0724 0x35 0x06c8 0x36 0x066c 0x37 0x0610
0x38 0x05b4 0x39 0x0558 0x3a 0x04fc 0x3b 0x04a0 0x3c 0x0444
0x3d 0x03e8 0x3e 0x038c 0x3f 0x0330 0x40 0x02d4 0x41 0x0278
0x42 0x021c 0x43 0x01c0 0x44 0x0164 0x45 0x0108 0x46 0x00ac
*-----------------------------
* Rec #0x1 slt: 0x1e objn: 66563(0x00010403) objd: 66563 tblspc: 1(0x00000001)
* Layer: 11 (Row) opc: 1 rci 0x00
Undo type: Regular undo Begin trans Last buffer split: No
Temp Object: No
Tablespace Undo: No
rdba: 0x00000000Ext idx: 0
flg2: 0
*-----------------------------
uba: 0x00c001ee.2682.32 ctl max scn: 0x0000.16c3268f prv tx scn: 0x0000.16c32695
txn start scn: scn: 0x0000.16c3294b logon user: 0
prev brb: 12583404 prev bcl: 0
KDO undo record:
KTB Redo
op: 0x04 ver: 0x01
compat bit: 4 (post-11) padding: 1
op: L itl: xid: 0x0004.00d.0000b28c uba: 0x00c019c4.2439.1b
flg: C--- lkc: 0 scn: 0x0000.16c32942
KDO Op code: LKR row dependencies Disabled
xtype: XA flags: 0x00000000 bdba: 0x00807955 hdba: 0x00807952
itli: 2 ispac: 0 maxfr: 4858
tabn: 0 slot: 7 to: 0
*-----------------------------
* Rec #0x2 slt: 0x1e objn: 66563(0x00010403) objd: 66563 tblspc: 1(0x00000001)
* Layer: 11 (Row) opc: 1 rci 0x01
Undo type: Regular undo Last buffer split: No
Temp Object: No
Tablespace Undo: No
rdba: 0x00000000
*-----------------------------
总结图
前一篇 :ASM中的几个概念
课文外:反对道德恐怖主义
本号文对你有价值可以打赏下,金额多少无所谓!
理科精华
文史经典
财经经典




