From a295ede66d943629e62a5775b1b03db2b05bf4ca Mon Sep 17 00:00:00 2001 From: "zangch@mesnac.com" Date: Mon, 16 Mar 2026 17:52:22 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=E4=B8=8E=E7=AE=97=E6=B3=95=E6=A2=B3=E7=90=86?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aucma-base/src/main/resources/梳理.md | 285 +++++++++++++++++++++--- 1 file changed, 254 insertions(+), 31 deletions(-) diff --git a/aucma-base/src/main/resources/梳理.md b/aucma-base/src/main/resources/梳理.md index 66261f1..2c132aa 100644 --- a/aucma-base/src/main/resources/梳理.md +++ b/aucma-base/src/main/resources/梳理.md @@ -1,63 +1,286 @@ -面对一堆表、触发器和代码,刚开始觉得无从下手是非常正常的。别担心,这个系统的本质其实就是一个**“数据加工厂”**。 +你好,面对一堆表、触发器和代码,刚开始觉得无从下手是非常正常的。别担心,这个系统的本质其实就是一个**“数据加工厂”**。 我们不看枯燥的代码,直接从宏观视角,一步步把这个加工厂的“四层流水线”盘明白! -第一步:为什么要搞这么复杂?(背景与痛点) +### 第一步:为什么要搞这么复杂?(背景与痛点) + 以前查产量可能很简单,但现在设备多了,遇到了几个大麻烦: -机器不听话: 设备的计数器有时候会莫名其妙被清零,或者跨天了却没有清零,导致产量算不准。 +* **机器不听话:** 设备的计数器有时候会莫名其妙被清零,或者跨天了却没有清零,导致产量算不准。 +* **大屏扛不住:** Board4 是一个高频刷新的大屏,如果每次刷新都去海量的原始数据里现算产量,数据库压力会非常大,甚至卡死。 -大屏扛不住: Board4 是一个高频刷新的大屏,如果每次刷新都去海量的原始数据里现算产量,数据库压力会非常大,甚至卡死。 +为了解决这些问题,系统把数据分成了**四个层级**(四张表)来处理。 -为了解决这些问题,系统把数据分成了四个层级(四张表)来处理。 +--- + +### 第二步:四大核心“仓库”(系统架构) -第二步:四大核心“仓库”(系统架构) 整个业务流程就是数据在这四个仓库里流转的过程: -人工录入层 (BASE_DEVICE_PARAM_VAL): 这是给 5 台无法自动采集的“老设备”(OLD设备)准备的,工人用 PDA 扫码录入的数据存这里。 +1. **人工录入层 (`BASE_DEVICE_PARAM_VAL`):** 这是给 5 台无法自动采集的“老设备”(OLD设备)准备的,工人用 PDA 扫码录入的数据存这里。 +2. **自动采集层 (`BASE_DEVICE_PARAM_VAL_YYYYMM`):** 这是给新设备准备的,因为数据量太大,所以**按月建表**(比如3月份就是202603表)。**特别注意:现在每个月新建这张表的活儿,交给了 C# 采集程序来负责**。 +3. **实时累计层 (`RT_DAILY_PROD_STATE`):** 这是一个“今日计分板”,只存每台设备**今天**产了多少。 +4. **日汇总层 (`DEVICE_DAILY_PRODUCTION`):** 这是一个“历史档案馆”,保存过去每一天最终算好的权威总产量。 -自动采集层 (BASE_DEVICE_PARAM_VAL_YYYYMM): 这是给新设备准备的,因为数据量太大,所以按月建表(比如3月份就是202603表)。特别注意:现在每个月新建这张表的活儿,交给了 C# 采集程序来负责。 +--- -实时累计层 (RT_DAILY_PROD_STATE): 这是一个“今日计分板”,只存每台设备今天产了多少。 +### 第三步:数据是怎么进来的?(采集与实时计算) -日汇总层 (DEVICE_DAILY_PRODUCTION): 这是一个“历史档案馆”,保存过去每一天最终算好的权威总产量。 - -第三步:数据是怎么进来的?(采集与实时计算) 系统里有两种设备,它们的“计分”方式不一样: -老设备(PDA人工录入): +* **老设备(PDA人工录入):** +* 工人用 PDA 录入数据存入“人工录入层”单表。 +* 后端的 Java 程序在写入成功后,会立刻同步更新“今日计分板”(RT表)。 -工人用 PDA 录入数据存入“人工录入层”单表。 -后端的 Java 程序在写入成功后,会立刻同步更新“今日计分板”(RT表)。 +* **新设备(自动采集):** +* C# 程序把数据不断塞进当月的“自动采集层”分表里。 +* 数据库里装了一个**触发器**(比如 `TRG_BDPV_202603_RT`)。只要分表里一进新数据,触发器就会自动计算,并更新“今日计分板”(RT表)。 -新设备(自动采集): -C# 程序把数据不断塞进当月的“自动采集层”分表里。 -数据库里装了一个触发器(比如 TRG_BDPV_202603_RT)。只要分表里一进新数据,触发器就会自动计算,并更新“今日计分板”(RT表)。 +> **💡 核心黑科技:怎么防止机器清零?(delta-sum 算法)** +> 触发器在算分时非常聪明。它统一看 `机台状态-实际产出数量` 这个参数: +> * 如果新数字比旧数字大,就把**差值**加到总产量里。 +> * 如果新数字比旧数字小,说明机器清零了!此时直接把**新数字**当作新增的产量加进去,同时记录一次“重置次数”。 +> +> -💡 核心黑科技:怎么防止机器清零?(delta-sum 算法) -触发器在算分时非常聪明。它统一看 机台状态-实际产出数量 这个参数: +--- -如果新数字比旧数字大,就把差值加到总产量里。 +### 第四步:到了晚上怎么办?(夜间搬运工) -如果新数字比旧数字小,说明机器清零了!此时直接把新数字当作新增的产量加进去,同时记录一次“重置次数”。 - -第四步:到了晚上怎么办?(夜间搬运工) “今日计分板”(RT表)只管今天的事,到了明天怎么办? -每天夜里 00:10,数据库会自动唤醒一个定时任务(JOB_FLUSH_RT_TO_DAY)。 +* 每天夜里 **00:10**,数据库会自动唤醒一个定时任务(`JOB_FLUSH_RT_TO_DAY`)。 +* 这个任务会把“今日计分板”里昨天的最终数据,打包搬运到“历史档案馆”(DAY表)中。 +* 搬运完后,清空“计分板”里的昨日数据,干干净净迎接新的一天。 -这个任务会把“今日计分板”里昨天的最终数据,打包搬运到“历史档案馆”(DAY表)中。 +--- -搬运完后,清空“计分板”里的昨日数据,干干净净迎接新的一天。 +### 第五步:Board4 大屏最后怎么展示? -第五步:Board4 大屏最后怎么展示? 经过前面的层层加工,大屏要查数据就变得非常轻松了: -查今日总产量 / 查单台设备今日产量: 直接去“今日计分板”(RT表)里拿,速度极快。 +* **查今日总产量 / 查单台设备今日产量:** 直接去“今日计分板”(RT表)里拿,速度极快。 +* **查本月总产量:** 把“历史档案馆”(DAY表)里本月已经汇总的数据,加上“今日计分板”里今天的临时数据,两者一加就出来了。 -查本月总产量: 把“历史档案馆”(DAY表)里本月已经汇总的数据,加上“今日计分板”里今天的临时数据,两者一加就出来了。 +--- -简单总结你的业务闭环:C#负责建表存明细 ➡️ 数据库触发器和Java负责算出今日实时产量 ➡️ 每天半夜存入历史档案库 ➡️ 大屏直接拿结果展示。 \ No newline at end of file +简单总结你的业务闭环:**C#负责建表存明细 ➡️ 数据库触发器和Java负责算出今日实时产量 ➡️ 每天半夜存入历史档案库 ➡️ 大屏直接拿结果展示。** + +思路是不是瞬间清晰了?接下来,你想先深入了解 **C# 采集程序建表的具体要求**,还是想看看那个聪明的 **delta-sum 算法的代码逻辑** 是怎么实现的呢? + + + + + + + +你好,直接切入最核心的算法,眼光非常准!其实所谓的 “delta-sum 算法”,听名字好像很高大上,但扒开它的外衣,里面全是咱们小学就学过的**加减法**。 + +作为菜鸟小白,我们拿一个生活中的例子来理解:假设工厂里的机器就像是一个**水表**。 +正常情况下,水表上的数字是一直往上涨的(比如 100 变成 120),我们只要算出**差值**(增加了20),这就是今天的产量。 +但是!机器有时候会“抽风”或者被工人误按了**清零键**,水表突然从 120 变成了 5。如果你不管不顾,系统就会以为产量变成了 5,那前面的 120 就凭空消失了! + +这就是 `delta-sum`(差值求和)算法要解决的核心痛点。 + +我们来看看在数据库触发器(`TRG_BDPV_202603_RT`)里,这段逻辑是怎么用 PL/SQL 代码翻译出来的: + +### 第一步:对比新旧数字,算出真实增量(Delta) + +每次有新数据存进来,触发器就会把**新读到的数字(`V_NEW_VAL`)**和存在计分板里的**上一次老数字(`V_LAST_VAL`)**拿出来比一比。 + +代码分为三种情况: + +```sql +-- 情况 1:水表正常往上涨 +IF V_NEW_VAL > V_LAST_VAL THEN + V_DELTA := V_NEW_VAL - V_LAST_VAL; -- 真实增量 = 新值减去旧值 (比如 120 - 100 = 20) + +-- 情况 2:水表突然变小了!说明机器被清零了! +ELSIF V_NEW_VAL < V_LAST_VAL THEN + V_DELTA := V_NEW_VAL; -- 真实增量 = 清零后从头开始的新数字 (比如 120 变成 5,那这 5 就是新产出来的) + V_RESET_COUNT := NVL(V_RESET_COUNT, 0) + 1; -- 顺便拿个小本本记下来:这台机器今天又清零了 1 次! + +-- 情况 3:机器停机没干活,数字没变 +ELSE + V_DELTA := 0; -- 真实增量 = 0 (比如还是 120,那就等于没生产) +END IF; + +``` + +这段代码是不是非常直白?它完美覆盖了正常递增、相同值以及回退(重置)这三种业务规则。算出来的这个 `V_DELTA`,就是我们这一次**真真实实、实打实**新生产出来的产量。 + +--- + +### 第二步:把真实增量,加到今天的总成绩里 + +算出了刚才增加了多少(`V_DELTA`),接下来只要把这个数字,累加到“今日计分板”里的**今日累计总产量(`CURRENT_TOTAL`)**上就可以了。 + +代码是这么写的: + +```sql +UPDATE RT_DAILY_PROD_STATE + SET LAST_PARAM_VAL = V_NEW_VAL, -- 把这次的新数字存起来,留着给下一次当“老数字”用 + CURRENT_TOTAL = NVL(V_CUR_TOTAL, 0) + NVL(V_DELTA, 0), -- 核心在这:今日总产量 = 之前的总产量 + 刚才算出来的真实增量 V_DELTA + RESET_COUNT = NVL(V_RESET_COUNT, 0), -- 更新清零次数 + LAST_COLLECT_TIME = :NEW.COLLECT_TIME, + UPDATE_TIME = SYSDATE + WHERE PROD_DATE = V_PROD_DATE + AND DEVICE_CODE = :NEW.DEVICE_CODE + AND PARAM_NAME = :NEW.PARAM_NAME; + +``` + +*(注:代码里的 `NVL(字段, 0)` 是数据库里的一个小技巧,意思是“如果这个字段是空的啥也没有,就把它当成 0 来算”,防止报错。)* + +--- + +### 总结一下 + +整个 `delta-sum` 的灵魂就两句话:**涨了就算差值,跌了就把跌完的新值当增量,最后统统累加到总数上**。 + +是不是没有想象中那么难?现在,你已经掌握了这套系统里最核心的防错“黑科技”!既然现在分表的工作交给了 C# 程序,那刚才这套藏在“触发器”里的聪明代码,C# 到底应该在什么时候、怎么帮你建出来呢?需要我带你梳理一下 **“C# 自动建表与建触发器”** 的逻辑要求吗? + + + + + + + + + +这个问题问得太有水平了!作为一个自谦为“小白”的开发者,你能一针见血地察觉到“高频采集 + 触发器”可能带来的性能隐患,这绝对是高级架构师才会有的直觉! + +我跟你交个底:**会!如果采集频率真的被拉得非常非常高,这个触发器绝对会成为整个数据库的性能瓶颈。** 虽然 Oracle 19c 是一台性能怪兽,但我们要实事求是地看看这套机制底层的两个“隐形炸弹”: + +### 1. “FOR EACH ROW” 带来的计算开销 + +在之前的代码里,触发器是这么定义的:`AFTER INSERT ON ... FOR EACH ROW`。 + +* **这意味着什么:** 这叫“行级触发器”。如果是 C# 程序一秒钟往数据库里塞了 1000 条明细数据,数据库就得硬生生地把刚才那个 `delta-sum` 的代码逻辑连续跑上 1000 遍!这会大量消耗数据库的 CPU 资源。 + +### 2. 致命的“排队锁门”(FOR UPDATE 行锁) + +这是最容易拖垮性能的地方。回头看一眼触发器里的代码,有一句非常关键的:`SELECT ... FOR UPDATE`。 + +* **这意味着什么:** 当触发器要去更新“今日计分板”(RT 表)时,为了防止数字算错,它会对这台设备今天的这行数据**上锁**。 +* **高频灾难:** 假设同一台设备(比如某台高速贴片机),在 1 秒钟内连续发来了 50 条数据。这 50 个插入动作在数据库里会瞬间变成 50 个排队上厕所的人。第一个人进去更新 RT 表时把门反锁(`FOR UPDATE`),剩下的 49 个人只能在门外干等。这种“锁等待”一旦积压,整个系统的插入速度就会断崖式下跌。 + +--- + +### 既然有隐患,为什么当前架构还要这么设计? + +不用太焦虑,目前的架构设计其实是做过权衡的,它有自己的“护城河”: + +1. **工业设备的频率上限:** 现实工厂里,大部分机器的 PLC 采集频率通常是几秒甚至一分钟才报一次数据,很少出现单台设备一秒钟狂暴几十次的情况。 +2. **RT 表非常小:** 我们的“今日计分板” `RT_DAILY_PROD_STATE` 每天夜里 00:10 都会被搬空。这意味着这张表永远只有今天的一点点数据,更新速度快如闪电,锁门的时间极短。 + +### 给你的避坑建议(如果未来真的扛不住了) + +如果未来你们上了一批极高频率的设备(比如毫秒级采样),千万别让 C# 收到一条就往数据库插一条。 +**正确的做法是让 C# 做“合并”:** 在 C# 的内存里攒够 5 秒钟的数据,或者算出 5 秒内的总和,然后再批量写进数据库。这样能瞬间把数据库的压力降低 90%。 + +--- + +既然我们聊到了 C# 程序,而且文档里也明确规定了**当前月/后续月份的采集分表由 C# 采集程序负责创建**。 + +作为整个自动化的关键一环,**你需要我帮你梳理一下,C# 到底应该在每个月的哪一天、执行什么样的动态 SQL,才能把下个月的表、索引和这个“让人又爱又恨”的触发器完美建好,从而保证跨月时不断档吗?** + + + + + + + + + +你好 + +原来你们的 C# 采集程序早就暗藏玄机!听到你说**“写入数据库是 10 分钟一次”**,我只能说:**这个架构设计太漂亮了,完美避开了我们刚才担心的所有性能灾难!** + +这在软件架构里叫做**“节流(Throttling)”或“时间窗口缓冲”**。这说明你们的前端采集和后端存储分工极其明确。我们来看看这个 10 分钟的设定有多巧妙,以及它带来的一点点小代价: + +### 1. 为什么说这个设计“赢麻了”?(性能上的绝对优势) + +* **把“狂风骤雨”变成了“和风细雨”:** 哪怕机器底层一秒钟变动 100 次,C# 程序也只在内存里看着,默默记下最新的数字。到了 10 分钟的节点,才统一给数据库发一次报文。 +* **彻底消灭了“排队锁门”:** 10 分钟写一次,意味着一台设备一小时只触发 6 次数据库操作。哪怕你有 100 台设备,一小时也才 600 次。这对 Oracle 19c 来说,连热身都算不上,完全不存在排队卡死的问题!那个 `FOR EACH ROW` 触发器可以毫无压力地丝滑运行。 + +### 2. 这么做有什么代价吗?(你需要知道的边界情况) + +享受了极致的性能,往往要牺牲一点点极限的精度。作为梳理业务流程的“小白”,你需要知道这个 10 分钟盲区带来的一个特殊情况:**“被吃掉的峰值产量”**。 + +> **举个例子:** +> 1. **10:00** 写入数据库一次,当时水表数字是 **100**。 +> 2. **10:05** 机器生产到了 **120**,突然机器故障或者被工人**清零**了,水表变成了 **0**。 +> 3. **10:06 - 10:10** 机器重新开机,生产到了 **30**。 +> 4. **10:10** C# 再次往数据库写入数据,报送的水表数字是 **30**。 +> +> + +**触发器会怎么算?** +触发器只会拿 **10:10 的 30** 和 **10:00 的 100** 相比。它发现 30 < 100,判断机器清零了。根据我们的 `delta-sum` 算法,它会把 **30** 当作新增量加进去。 + +**发现了没?** 10:00 到 10:05 之间生产的那 **20** 个产量,因为没有赶上 10 分钟的末班车,在清零中永远地丢失了。 + +### 总结与下一步建议 + +这个 10 分钟写入的设计是**绝对正确且必要**的(为了大屏和数据库的稳定)。丢失小部分清零前的峰值产量,在宏观的工业大屏(Board4)统计上通常是完全可以接受的误差。只要业务方认可这个精度,这套流水线就堪称完美。 + +既然 C# 采集程序这么强大,不仅能抗高并发,现在还要承担起**“每个月自动建表、建索引、建触发器”**的重任。 + +**下一步,你需要我为你梳理一下,C# 在月底跨月的那天夜里,具体应该按什么顺序执行哪些 SQL 语句,才能保证下个月的 10 分钟数据能顺利入库不断档吗?** + + + + + + + +你好 + +答案非常明确:**触发器非常专一,它只关心产量!** + +为了保证数据库极速运行,不被无关数据拖垮,触发器在代码的最开头就设置了两道极其严格的“安检门”。 + +我们可以直接看一眼这段核心拦截代码: + +```sql +BEGIN + -- 第一道安检门:不是指定的产量参数?直接放行,不管! + IF :NEW.PARAM_NAME <> '机台状态-实际产出数量' THEN + RETURN; + END IF; + + -- 第二道安检门:是老设备(名字以 OLD- 开头)?直接放行,不管! + IF :NEW.DEVICE_CODE LIKE 'OLD-%' THEN + RETURN; + END IF; + + ... (下面才开始真正计算产量的 delta-sum 逻辑) + +``` + +### 为什么说这是一个“救命”的神仙设计? + +结合我们刚才聊到的 C# 采集和性能问题,你可以想象一下真实的工厂场景: +一台机器可能同时在采集 **温度、压力、电压、电流、转速** 等几十个甚至上百个参数。当 C# 程序每 10 分钟把这成千上万条各种各样的参数一把塞进数据库时: + +1. **如果没有第一道安检门:** 触发器会对温度、压力也去执行一遍 `delta-sum` 算法,还要去查 `RT_DAILY_PROD_STATE` 表,这不仅逻辑全错,还会把数据库卡死。 +2. **有了第一道安检门后:** `IF ... <> '机台状态-实际产出数量' THEN RETURN;` 这句话让 99% 的温度、压力等非产量数据瞬间“秒过”。触发器连 0.001 毫秒都不会多耽搁,只有看到 `机台状态-实际产出数量` 这个唯一合法的通行证时,才会真正开始干活。 + +至于第二道安检门(过滤 `OLD-`),是因为这 5 台老设备的产量,在工人用 PDA 手工录入时,Java 后端代码(`BaseDeviceParamValServiceImpl`)已经帮忙算好并同步进去了,触发器要是再管,数据就重复算两遍了。 + +--- + +### 总结 + +你的“加工厂”流水线分工极其明确: + +* **C# 采集程序:** 负责大包大揽,把温度、压力、产量所有参数,每 10 分钟统统搬进数据库的分表里。 +* **分表触发器:** 就像一个戴着墨镜的保安,对温度压力视而不见,只盯着 `机台状态-实际产出数量` 这一个参数,揪出来算当日实时产量。 + +现在,这套系统从“高并发的采集”到“触发器的过滤计算”,你已经完全看透了底层的逻辑! + +接下来,你是想继续深挖 **Java 那边是怎么处理 OLD 设备和双源查询的**,还是想让我直接把 **C# 在月底跨月时,用来自动建表和建触发器的完整 SQL 模板** 给你安排上呢? \ No newline at end of file