动态

2026

发布了 说说 #传记

兰利掉进波托马克河,九天后自行车匠上了天

小故事

1903 年 10 月 7 日,波托马克河上漂着一间大木屋,屋顶架着导轨,导轨上趴着一架机器。坐在上面的是史密森尼学会掌门人塞缪尔·兰利,快七十岁了,天文学界的老前辈。据常见说法,他为了这架“大飞行器”前后从陆军部拿到五万美元,学会另有拨款,团队、场地、人手一样不缺。他是有底气的:1896 年,他的蒸汽模型无人驾驶飞过波托马克河上空近千米。

那天他请了记者。弹射装置一发力,机器没飞起来,扭了一下,栽进河里,助手曼利被人从水里捞出来。12 月 8 日他又试了一次,还是摔。之后他收手了。

九天后的 12 月 17 日,北卡罗来纳基蒂霍克的沙丘上,两个从俄亥俄代顿来的自行车店老板蹲在机器旁边。冷风里,机器滑行一段就离了地,飞了十二秒,一百多米;同一天又飞了几次,最远将近一分钟。围着看的有五个人,其中几位是附近救生站的渔民。他们叫奥维尔和威尔伯·莱特。

这俩人不是闭门造车。他们读遍了能找到的资料,写信向史密森尼学会要文献,跟滑翔机老手沙努特通了好几年信。1900 到 1902 年,他们一趟趟往沙丘跑,摔了修,修了再摔。1901 年那架滑翔机让他们很窝火:实测升力跟当时通用的李林塔尔表格对不上。他们没有接着猜,在自行车店后院搭了个木箱风洞,先后测了两百多种翼型,最后认定那些老表格本身就有错。要造发动机时,市面上的汽油机都太重,他们自己造了一台。据常见统计,这场竞赛里他们的花费不到一千美元。

一句话说清

1903 年,拿着政府五万美元、请了记者的兰利两次试飞都栽进波托马克河;九天后,花了不到一千美元的自行车匠莱特兄弟在沙丘上第一次飞了起来。

别信谣言

  • “兰利什么都不懂,纯属白花钱。” 他的无人蒸汽模型 1896 年确实飞过近千米;1914 年有人把“大飞行器”修好重飞成功,但那台机器被改动了几十处。问题不在他懂不懂飞行,在他把成败押在一次弹射上。
  • “莱特兄弟是没学历的野路子,全靠灵感。” 他们找齐了当时的公开材料,还跟最懂滑翔机的人长期通信。缺的是钱和身份,不缺信息。

带走什么

  • 钱和名声能买来场地与团队,买不来“再试一次”。兰利只有两次机会,莱特有一整个冬天。
  • 手里的尺子不对,先修尺子。数据对不上,就去造风洞,而不是继续争论。
  • 循环转得快,比准备得完好更划算:摔、修、再摔。
  • 他们账面上花得少,但那三年在沙丘上挨的风和冷,是用别的东西付的。

换我身上

  • 我手上的事,是攒一场漂亮的发布,还是每周都能摔一次、改一次?
  • 本周可试(30 分钟内可验收):挑一件正在憋的大方案,切出一个“九天内能飞一次”的最小版本——写一句这几天就能做完的动作,再写一句验收标准(谁看、看到什么算过),并把时间放进日程。验收:两句都写完,日程里真有一个带时间的安排。

#传记

发布了 说说 #育儿

宝宝怕吸尘器吹风机,不是胆小是记住了

什么情况下

白天老人带娃,你下班刚进门,奶奶一句“今天吸尘器一响,孩子吓得直哭,抱都抱不住”。九到十二个月的宝宝,正是陆续开始怕某些声音和东西的阶段。这篇写给家里刚出现这种哭的爸妈,也写给帮忙带娃的老人。

今天要知道的

  • 这个月龄开始怕声音、怕某些物件,是他在学着分辨熟悉和不熟悉。以前什么响动都不当回事,现在记得住,也记得住上次被吓到的那一下。
  • 怕不是胆小,也说明不了以后性格怎样。多数宝宝过一段就淡下去,真正影响长短的是大人怎么接这一次。
  • 他的反应常常发生在会说话之前:整个人僵住、往你身上扑、扭头躲、大哭。这时候先给身体上的安全感,比讲道理快得多。
  • 让他自己看见声音从哪来,比在背后突然响要好得多。他看着你按下开关,心里就有准备,不太会把这件事当成突然袭击。

别人常搞错

  • 觉得孩子胆小,硬抱着他去听、去摸,想着多见几次就习惯了。这个月龄硬来,往往把怕的时间拉得更长。
  • 一家人围着笑他“你看他吓的”。他读得懂被笑,容易把这件害怕的事和被人围着看连在一起。

为啥有用

他现在会怕,是因为记住了上一次的经验,不是矫情。一次害怕的事里有人稳稳地陪着他,他下次遇到新的响动、新的地方会更有底。反过来,被硬拉过去、被笑过几次,他可能把这份怕拖得更久。

照着做

  • 今晚挑一个他会怕的响动试一次:吸尘器、吹风机、榨汁机都行。抱着他,先说一句“要响了”,再开机,两秒就关。不哭就夸一句,哭了就抱着退到门外,全程别超过一两分钟。本周每天来一次。
  • 别把他的手按到会怕的东西上,也别从背后突然开机。
  • 跟家里老人定一句统一的话:他哭的时候第一句不说“别怕”,改成“奶奶在,我们离远一点看”。说好一次,全家照这个来。
  • 如果他连着几周对大部分新声音都僵住,或者熟悉的人抱着也一直放松不下来,下次体检主动跟医生提一句,别自己在网上对症状。

#育儿

发布了 说说 #概念#思维

反脆弱

山坳里有两片葡萄地,一片架了密实的挡风棚,一片敞着。棚里那片头两年长得最齐整,藤蔓顺、果串大,主人常拿它去比另一片,觉得另一片是被风吹野了。

第三年秋天,一场从侧后刮来的大风把棚先掀了起来。藤缠在架子上互相扯断,断口齐了半亩;敞着的那片枝干矮壮、根扎得深,风过之后只落了些叶子。主人这才明白,风不是白吹的——棚挡掉了风,也挡掉了藤长筋骨的那几年;可要是风大到连根都拔起来,那片地也什么都留不下。

可以学到什么

反脆弱说的是:有些东西挨过一次冲击,不是回到原样,而是比之前更强;另一些东西,冲击来一次就碎。拿波动当尺子,能分成三类:瓷器一类,摔一次就完了;玻璃杯一类,摔了还是那个杯子,不增不减;骨头、肌肉、免疫系统一类,受一次扛得住的力,就长一点承受力。前两类都在躲波动,只有第三类靠波动长大。

它常被读成“抗压”“乐观”“心态好”,这些说的都是人怎么面对困难,落点还在扛住;反脆弱问的是结构——这件事里头有没有“受力—反馈—调整”这条回路。有回路,冲击就是一次校准的信息;回路断了,或者力偏偏落在你调不了的那一环上,冲击就只是损耗。判起来也就一句话:这次波动,给我留下了什么下次能用的东西?

两头都要防。一头是拿它当借口去找苦吃:压力有阈值,连轴撞墙超过恢复能力,剩下的只有损伤,不是成长。另一头是把什么都当训练:安全带、刹车、备份、保险天生就是用来削掉波动的,它们该做的是稳,不该拿“多摔几次就强了”去练。分界在于这段波动你是能接住并从中调整,还是只能硬受着。

和我有什么关系

孩子搭了一半的积木塔塌了,你的手比他的脑子快,先替他扶住,顺手把最底下那块摆正——他确实没哭,可这一塌本来是他练“自己找出哪块没放稳”的机会。这样的小麻烦一天里会出现好几回,每一回被提前扫掉,他攒下的就不是“我试过并且试成了”的记录,而是“出了事有人接”。要留的其实不多:只要他哭完还能回来接着搭,就让他自己收。你只在两个点上伸手——这一次会伤到他,或者他明确开口求你。

自己给自己排的学习路径,通常会把最卡的那一段悄悄绕过去:先听讲解,听着顺才动手,一遇到推不动的地方,就换一门更友好的入门课从头再来。进度条一直好看,力气却一直没长——真正的进步,多半发生在“想不通但还是坐在那儿想”的那几十分钟里。你当然可以继续避开会受伤的搞法,不熬夜、不硬啃太超纲的东西,但别把为难的那一段整个外包给讲解和答案,那等于给野地里的藤架上挡风棚。

如何实践

从今天(9月18日)起,每次手要伸出去之前先问一句:这一次的麻烦,他自己收得了吗?本周做两件事:一,9月18日到9月22日,每天留一回不插手的机会——积木塌了、鞋带系不上、拼图拼错了,只在他可能受伤或他开口求助时才出手,其余时候你坐在旁边不动,晚上写一句“他自己收掉了什么”。9月22日(周二)晚把五句话连起来读,数一数有几回真的自己收住了,有几回卡到哭、还得你进去。二,新学的那门东西,9月18日到9月22日每天留二十分钟硬想时间:关掉讲解和答案,只对着一个想不通的点写草稿,到点还没动,就记下卡在哪一步、明天从哪一步接着想;9月23日(周三)回看这一周记下的卡点,数清几处是自己推出来的、几处最后是查出来的。

风不来的时候,挡风棚和露地看不出分别。

反脆弱提醒你:先分清哪一处该减震、哪一处该留一点风。能被冲击校准的地方,别把最后那点波动也一并抹平。

#概念 #思维

发布了 说说 #面试

秒杀的分层过滤:库存扣减的正确性不在锁上,在扣减位置与失败回补

面试官常怎么问

  • “秒杀系统怎么设计?怎么保证不超卖?”
  • “Redis 扣库存和数据库扣库存,谁说了算?两边对不上怎么查?”
  • “为什么不用一把分布式锁把商品锁住?”

原理

一句话总括:秒杀要解决的不是“并发怎么写锁”,而是“瞬时流量远大于系统容量时,怎么让请求逐层递减,并让库存扣减这一步具备原子性、让失败的那一步具备补偿能力”。

难点有三个来源。第一是流量形态:下单请求集中在开抢后的极短窗口,峰值 QPS 通常是稳态的几十上百倍,任何一层按峰值容量建设都不经济。第二是库存的守恒性:库存是有限且需要账实相符的资源,超卖是资损,少卖是客诉,两者都得在方案里正面回答。第三是失败窗口:扣减、落库、发消息分布在不同组件上,任意一步失败都会留下钱货不一致的中间态。

由此推出设计主线。把请求做成漏斗,客户端、网关、应用、Redis、消息队列、数据库各自只承担自己能处理的数量,越往后量越小;把“判断库存是否充足”和“扣减库存”压成一次原子操作,从根上消除 check-then-act 的竞态;把裁决层放在 Redis、把账本放在数据库,两者之间用异步链路连接,并配幂等、回补与对账。

为什么这样设计而不是全程强一致:一条链路上同时要满足高并发、强一致和低延迟,通常做不到,只能选择在哪一环放宽。库存的强一致放在数据库上的代价是行锁排队与写入热点,于是把并发压力前移到 Redis 的原子操作上,用最终一致加对账来收敛账本。

前提与边界:Redis 的脚本执行语义、集群模式下脚本对 key 的要求、消息队列是否支持事务消息、MySQL 的行锁与唯一键行为,都随版本与部署形态变化,落地前需对照目标版本文档核实。

细节

  1. 过滤顺序与量级递减。客户端按钮置灰只是体验,不构成防线;真正的过滤从接入层开始,按用户、设备、IP 维度限流,把单用户高频请求挡在应用之外。应用层可以再用本地计数(进程内 AtomicInteger 或本地缓存)按真实库存的若干倍粗筛,例如库存 100 件时放行几百个请求进入 Redis 扣减,让绝大多数请求在几乎没有网络开销的情况下被拒。
  2. 为什么用 Lua 做扣减。Redis 执行命令是单线程的,一段 Lua 脚本在执行期间不被其他命令插入,于是“读库存、判断、扣减、记录用户”这几步可以合成一个原子单元,中间没有可被插入的窗口。代价是脚本会占住主线程,脚本必须短,脚本里放 KEYS 扫描或大范围 HGETALL 会连带拖慢同实例上的其他业务。
  3. 库存预热与变更通道。开抢前把数据库库存加载到 Redis,加载动作需要通过单实例任务或分布式锁保证只执行一次;活动期间的补货、调整库存要走独立通道同步到 Redis 和数据库,否则会出现“Redis 显示 100 件、数据库只有 80 件”的错位。
  4. 一人一单的表达。用 Redis 的 set 结构记录已下单用户,或直接用数据库唯一键(用户与活动的组合)表达。它的价值不只是防重复,而是把“是否重复”变成一次幂等命中,为后面的重试与消息重投留出空间。
  5. 失败窗口。Redis 扣减成功后,发送消息、写数据库、返回回执都可能失败。此时库存已经少了而订单没有生成,就是少卖。处理方式是把扣减与消息发送绑定成一次可重放的单元:本地消息表加定时补偿,或使用消息队列的事务消息能力,同时让消费端幂等。
  6. 超时释放必须幂等。常见规则是订单 15 分钟未支付则释放库存。释放动作要基于状态机判断,例如“从未支付流转到已取消”只允许成功一次;如果只用“删除记录再加回库存”这种写法,重复执行会把同一份库存加回两次,超卖就此产生。
  7. 超卖与少卖的成因不同,监控要分开。超卖通常来自扣减不原子、回补重复、或绕过 Redis 直接写数据库;少卖通常来自扣减成功而后续链路失败且无人回补。前者要审计每一次回补,后者要盯“扣减成功但落库失败”的差值。
  8. 热 key 与集群约束。单个商品的库存 key 集中在同一个 Redis 节点上,扣减请求也会集中到同一分片。常见缓解手法是把库存按份数拆成多个 key 分桶,代价是桶间不均衡会留下少量卖不掉的余量。若使用集群模式,Lua 脚本涉及的多个 key 需落在同一槽,分桶键设计要一并考虑。

机制图或链路

flowchart TD
    A["用户点击下单"] --> B["接入层限流: 用户/设备/活动维度"]
    B --> C["应用层本地计数粗筛"]
    C --> D["Redis Lua: 原子判断库存并记录用户"]
    D -->|库存不足或重复下单| E["快速返回失败"]
    D -->|扣减成功| F["发送消息到 MQ"]
    F --> G["消费端按唯一键幂等落库"]
    G --> H["数据库账本"]
    F --> I["发送失败则由本地消息表重投"]
    H --> J["对账任务: Redis 与 DB 库存差值比对"]

一句话点题:这张图的价值在于每往下一层,流量都要再少一个量级;真正需要严格正确的只有扣减那一步和它的补偿链路。

正确写法

最小 Lua 脚本(仅演示核心逻辑,未覆盖脚本超时配置、集群槽位约束、指标埋点与异常兜底):

-- KEYS[1] 库存 key, KEYS[2] 已下单用户集合
-- ARGV[1] 用户标识, ARGV[2] 本次扣减数量
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
  return -2                                -- 重复下单
end
local stock = tonumber(redis.call('GET', KEYS[1]) or '-1')
if stock < tonumber(ARGV[2]) then
  return -1                                -- 库存不足
end
redis.call('DECRBY', KEYS[1], ARGV[2])
redis.call('SADD', KEYS[2], ARGV[1])
return stock - tonumber(ARGV[2])           -- 剩余库存

返回码要区分库存不足、重复下单与系统异常,否则前端只能给出一句含糊提示,用户会持续重试。

危险写法一:先 GET 判断,再单独 DECR 扣减。两个命令之间存在可被其他请求插入的窗口,多个请求可以同时读到同一份库存并各自扣减,超卖由此产生;之后加多少重试都补不回被破坏的守恒性。

危险写法二:用一把商品级分布式锁把整个扣减包住。锁的粒度覆盖了所有用户,吞吐被串行化;锁过期与续期又引入新的边界,锁失效的瞬间并发重新涌入,超卖仍然可能出现。

危险写法三:把 Redis 当成账本的全部依据,数据库只写订单不记库存流水。Redis 重启、淘汰、误操作都会让账实不符,事后也缺少可推导的基准。

延伸一层

  1. 与限流的关系。限流管入口,秒杀的分层过滤可以看作限流在活动场景下的一种组织方式;入口拦不住的量,靠后面的原子扣减与异步链路兜住,两者是分工而不是互替。
  2. 与幂等的关系。回补、超时释放、消息重投三处都依赖幂等,常见落地是唯一键、去重表与状态机流转。秒杀是把幂等设计用在高并发写路径上的典型场景。
  3. 与缓存一致性的关系。库存可以看作一种带业务规则的缓存写,但它比普通缓存严格:普通缓存允许短暂陈旧,库存不允许凭空多出来。所以它需要账本、对账与回补,而不只是读写顺序上的技巧。
  4. 面试官常接着问“队列里堆积了几百万条消息怎么办”。回答要点是先判断是消费能力不足还是消息被反复重投,再决定扩分区、扩消费者、旁路降级或丢弃可丢的消息类型;消费端幂等是扩容的前提,否则扩容只会放大重复副作用。

架构师视角

  1. 角色划分要写清:Redis 是裁决层,数据库是账本,消息队列是缓冲与重放通道。三层各有明确的失效模式,方案文档里应写清任意一层不可用时活动的降级姿态。
  2. 对账是必需项而不是优化项。定时比对 Redis 与数据库的库存差值、扣减成功与落库成功的水位,差异超过阈值告警,日终校准。没有对账,任何补偿都只是猜测。
  3. 风险归属要落到人。回补由谁执行、差异由谁确认、活动期间谁值班,应在方案评审阶段明确,否则库存不一致会成为长期无人认领的工单。
  4. 变更治理。库存数量、活动规则、限流阈值的修改走配置与审计,不允许直接改 Redis 数据;高风险阈值变更要有压测数据与回滚方案。
  5. 容量评估按放大后的量做。压测要对齐开抢瞬间的入口 QPS,并演练 Redis 故障、消息堆积、数据库写入变慢三类场景,活动预案才有依据。

面试怎么答

30 秒精华

秒杀的核心是把请求做成逐层递减的漏斗,并把判断库存与扣减库存压成一次原子操作;正确性靠 Redis 原子扣减加数据库账本、幂等回补与对账,而不是把商品整体锁起来。回答时先讲流量形态与守恒性这两个约束,再讲扣减位置的选择,最后补失败窗口与补偿。

答法骨架

  1. 先给结论:这是流量与一致性的组合问题,不是加锁问题。
  2. 讲分层:接入层限流、应用层本地粗筛、Redis 原子扣减、消息削峰、数据库结算,每层只放行下一层能处理的数量。
  3. 讲原子性:Redis 单线程加 Lua 把多步合成一步,说明为什么 GET 加 DECR 会超卖。
  4. 补失败与兜底:扣减与消息绑定、消费幂等、超时释放幂等、对账与回补,并说明超卖与少卖的成因不同。

可能被追问

  • “Redis 扣了、数据库没落成怎么办?”应答要点:这属于失败窗口,用本地消息表或事务消息保证扣减与消息同生共死,消费端按唯一键幂等;短期靠重投放量,长期靠对账发现差异并回补。
  • “为什么不直接用分布式锁?”应答要点:锁提供互斥,不提供吞吐,也不解决失败后的补偿;热点商品上锁会把并发串行化,且锁过期与续期本身又引入新的边界。原子扣减天然互斥,代价更低。

高阶追问

  • “集群模式下 Lua 脚本怎么写?”应答要点:脚本涉及的 key 需要落在同一槽,通常用 hashtag 把同一商品的库存键与用户集合键绑定到同一分片;具体限制需对照所用 Redis 版本与客户端文档核实。
  • “对账以哪边为准,怎么重放?”应答要点:以数据库流水作为账本推导应收敛值,Redis 只作裁决层;重放要基于流水与幂等规则,避免把已经回补过的差异再补一次。

容易翻车

  1. 常见错误说法:“加一把分布式锁就没有超卖。”正确边界:锁只提供互斥,锁过期、续期失败、以及跨组件操作都在锁的保护范围之外,超卖仍可能发生;真正消除竞态的是把判断与扣减合成原子操作。
  2. 常见错误说法:“Redis 扣减成功就等于下单成功。”正确边界:扣减只是裁决,消息与落库仍可能失败,需要幂等重投、补偿与对账来收敛,否则表现为有库存扣减却没有订单的少卖。
  3. 常见错误说法:“库存放 Redis 就够了,数据库只是留档。”正确边界:Redis 会重启、会淘汰、会被误操作,账本必须在数据库;只有裁决层加账本加对账,账实相符才有可验证的依据。

复习卡片

  • 一句话:秒杀是把瞬时流量做成逐层递减的漏斗,并把库存判断与扣减压成一次原子操作,再用幂等与对账收敛失败窗口。
  • 主链路:接入层限流,应用层本地粗筛,Redis Lua 原子扣减并记录用户,扣减成功后发消息到队列,消费端按唯一键幂等落库,发送失败走本地消息表重投,定时对账比对库存差值。
  • 关键边界:GET 与 DECR 分离会超卖;脚本要短;集群下多 key 需同槽;超时释放与消息重投都要幂等;Redis 是裁决层,数据库才是账本。
  • 选型与适用场景:库存有限、瞬时流量远大于稳态的活动型业务适合这套结构;对一致性要求更高而流量平稳的场景,直接在数据库用行锁或唯一键结算更省心。
  • 易混点:超卖与少卖成因不同;限流与降级分工不同(入口拦截与故障兜底);缓存允许短暂陈旧,库存不允许。

自测

  1. 为什么“先 GET 判断、再 DECR 扣减”的写法会出现超卖?换成 Lua 之后消除的是什么?
  2. 库存扣减成功但消息发送失败时,系统处于什么状态?有哪两种处理方向,各自的代价是什么?
  3. 口述:对照你参与过的一次高并发写入场景(不虚构公司名与业务数据),说明流量在哪一层被拦住、压力落在哪个组件的哪一步,以及当时最担心的是哪种不一致。

参考答案

  1. 两个命令之间存在可被其他请求插入的时间窗口,多个请求可以同时读到同一份库存并各自扣减,扣减总量超过初始库存。Lua 消除的是这个窗口:脚本执行期间其他命令被排队,判断与扣减作为整体生效,也就是把 check-then-act 变成一次原子动作。
  2. 处于库存已扣减、订单未生成的中间态,即少卖。方向一是让扣减与消息发送绑定成可重放单元(本地消息表加定时补偿,或事务消息),代价是要维护额外的表与补偿任务;方向二是先落库再以数据库为裁决,代价是并发能力下降、写入成为新的热点。
  3. 回答骨架:先讲流量形态与约束(峰值与稳态的倍差、库存守恒),再讲拦在哪一层(接入层限流、应用层粗筛),然后讲压力落在哪一步(Redis 原子扣减与消息写入),最后讲兜底(幂等、补偿、对账、降级预案)以及当时最担心的不一致类型。

本周练一次

20 分钟内:写下你手上一个“高并发写加有限资源”的接口,画出它的分层过滤链路,标注每一层放行的量级与拒绝的原因;再写出这条链上三处必须幂等的动作,以及各自失效时会出现什么现象。最后闭眼口述 2 分钟,用“漏斗、原子扣减、失败窗口、对账”四个词讲完整条设计。

#面试

发布了 说说 #github

kestra-io/kestra

一句话:把“定时跑的”和“事件触发的”自动化收进一份 YAML 的编排平台,服务端全是 Java。它值得读的地方不在“又一个调度器”,而在一个 23 模块的 Java 大仓怎么把队列、存储、插件加载都做成可替换件,还能一条 docker 命令单机跑起来。

仓库卡片

  • 链接:https://github.com/kestra-io/kestra
  • 类型:成品应用(可自部署的编排平台,Apache-2.0;后端 Java 25 + Micronaut,界面 Vue 3,Gradle 9.7.1 多模块)
  • 维护印象:活跃。最新 Release v2.0.2(2026-09-15),默认分支 develop 末次提交 2026-09-17;约 2.81 万 star、2998 fork、665 个 open issue,数据于 2026-09-18 读 GitHub API 核对

亮点在哪

  1. 队列不依赖 Kafka,自己实现了数据库队列(queue-jdbc/)。JdbcQueueClient.subscribeDispatch 在一个事务里做完三件事:按 typerouting_key 取一页,用 jOOQ 的 .forUpdate().skipLocked() 锁住(生成 FOR UPDATE SKIP LOCKED),在事务内把消息交给消费者,再按 offset 删除。多实例抢同一张表不互相等待,也不会出现“删了但没处理”。
  2. 轮询间隔分档退避,不是写死一个 sleep。queue/.../poller/QueuePollerConfiguration.java 是 record,字段含 minPollIntervalmaxPollIntervalpollSwitchIntervalpollSizeswitchStepsimmediateRepollcomputeSteps() 从最大间隔起每次除以二直到触到最小间隔。默认值在 cli/src/main/resources/application.yml:25ms、500ms、60s。空闲时几乎不压库,来活了取满一页就立即重查。
  3. 插件是运行时从 Maven 仓下载再挂载的。core/.../plugins/MavenPluginDownloader.java 用 Apache Maven Resolver 做版本范围解析,落到名为 kestra-plugins-m2-repository 的本地仓,默认仓库是 Maven Central。加载器 PluginClassLoader.java(161 行)是 child-first 的 URLClassLoader,并用一条正则 EXCLUDESjavajakartaio.kestra.core、slf4j、logback、jackson 各包、reactor、netty、opentelemetry 等整体排除在隔离之外,避免类身份不一致引发 LinkageErrorgetResourcegetResources 被覆写成只查本地,防止插件资源盖住主程序。
  4. 自动装插件时先判断线程身份(PluginAutoInstallService.installMissingPlugins)。它先解析 flow YAML 里缺失的任务类型 FQCN,再去插件目录找对应坐标;装之前检查 Thread.currentThread() instanceof FastThreadLocalThread,若是 Netty 事件循环线程就丢给 Thread.startVirtualThread 后台做,注释写明原因:目录查询用的 HTTP 客户端本身要用事件循环,同步等会死锁到读超时。同处还有一句安全注释:只有能在插件目录 manifest 里匹配到 groupId 与 artifactId 的坐标才允许安装,调用方无法让服务端加载任意 Maven 坐标。
  5. 消息被当成有大小预算的东西。queue/.../QueueService.serialize 序列化后若超过 kestra.queue.message-protection.limit(配置文件里是 1MB),除“已终止的执行”放行外一律抛 MessageTooBigException,同时打一个按类名分组的 metric;异常文案直接给对策:调大 limit,或把任务输出挪进内部存储(kestra.task.outputs.limit)。放过终止态执行,是不放就永远报告不出去。

使用价值

  • 想给“一堆有依赖关系的批任务”找编排层:namespace 隔离、subflow 复用、retries 与 timeout、backfill 补跑、quotas 限流、checks 前置校验,这些是调度器与编排器的分水岭,Kestra 把它们都做成了 YAML 字段。
  • 想学数据库队列怎么写:一趟事务内的“锁页、消费、删除”,加一条分档退避的轮询曲线,再加反序列化失败的兜底(Postgres 的 JSONB 拒绝 \u0000 与孤立 UTF-16 代理对时,把 DataException 转成可恢复的队列异常),这套组合能搬到自己维护的任务表上。
  • 想学 Java 插件体系:类加载隔离、资源隔离、父加载器排除清单、下载器与本地仓、安装入口白名单,这是一个完整闭环的参考实现,比只讲双亲委派的理论材料具体。
  • 想学大仓怎么分模块:控制面与数据面分开,queuequeue-jdbcrepository-memoryjdbc-* 各成模块,同一接口多种落地方案并存,单机与集群共用一套代码。

和我有什么关系

  • 多点的日报、对账、库存同步这类任务常见形态是“多步骤 + 有依赖 + 失败要能重跑”。上面的 retriestimeoutbackfill 可以作为选型或自研时的对照面;映射到你们的实际约束还需核对。(落地场景)
  • vault 里已有 200-java/topic/java-class-loading/ 专题。PluginClassLoaderPluginScanner 可作为该专题的生产级案例补进去:为什么 child-first、排除清单里为什么必须列 Jackson 与 Reactor、registerAsParallelCapable 解决什么,都是能直接写进笔记的素材。
  • second-brain 的日推靠 Hermes cron 定时跑,改 prompt 立即生效,改错了下一次就照错跑。Kestra 2.0 的 draft revision 是另一种做法:改动先存草稿,触发器继续跑上一版已发布的 revision,看准了再切。这条思路可以用来设计自己 cron 的“改稿与生效分离”。
  • 通用工程经验:分档退避的轮询、SKIP LOCKED 抢任务、按消息大小设预算、事件循环上不做阻塞等待,这四条与业务无关,任何自研调度都用得上,属长期储备。

别踩坑

  • 门槛在工具链上:build.gradle 三处 toolchain 都写死 JavaLanguageVersion.of(25),wrapper 是 Gradle 9.7.1。仓库自己的 AGENTS.md 明说整仓 ./gradlew build 要几十分钟,只想读代码别一上来跑全量构建。
  • 插件不在这个仓里:plugins/ 目录是空的,各插件(含随之发布的 Vue UI 组件)在 kestra-io/plugin-* 独立仓库;2.0 里插件 schema bundle 打进主 JAR 的 /plugins-schema.jsonkestra.plugins.schema-bundle-url-template 默认为空,别照老文档去配 URL。
  • 2.0 有破坏性变更:ForEachForEachItem 合并为 Loop,trigger 的 conditions 改成 when,worker 通信由 JDBC 队列换成 gRPC worker-controller(默认控制器端口 50051)。按 1.x 教程写的 flow 会报错,迁移走官方 v2.0 指南。

怎么验证

  1. 读两个文件:queue-jdbc/src/main/java/io/kestra/queue/jdbc/client/JdbcQueueClient.javasubscribeDispatch,与 queue/src/main/java/io/kestra/queue/poller/QueuePollerConfiguration.javacomputeSteps();再对照 cli/src/main/resources/application.ymlkestra.jdbc.queues 段的三个默认值。
  2. core/src/main/java/io/kestra/core/plugins/PluginClassLoader.javaEXCLUDES 正则与 loadClassgetResources 两个覆写,再扫一眼同目录 MavenPluginDownloader.java 顶部的 Aether 依赖与本地仓前缀,即可确认“插件从 Maven 仓下载后被隔离加载”这条链路。
  3. 想看它跑起来,README 给的单机命令是 docker run --pull=always --rm -it -p 8080:8080 --user=root -v kestra_data:/app/storage -v kestra_db:/app/data -v /var/run/docker.sock:/var/run/docker.sock -v /tmp:/tmp -e KESTRA_PLUGINS_AUTO_INSTALL_ENABLED=true kestra/kestra:latest-slim server local。我未实际执行,标注待核对。

本周可试

30 分钟:读 JdbcQueueClient.subscribeDispatchjdbc/src/main/java/io/kestra/jdbc/AbstractJdbcRepository.java 第 97 行那段关于 FOR UPDATE 与调用方事务的注释,写三句话:消费在事务内意味着 worker 处理到一半挂掉时消息会怎样、SKIP LOCKED 让多个 worker 抢同一页时各自看到什么、把删除放在同一事务里避免的是哪一种故障。验收标准是能答出“至少一次投递”以及它的代价(重复消费要靠下游幂等)。

#github

发布了 说说 #关系

冷战里先开口的那个,不丢人

什么情况下

育儿期伴侣——因为带娃分工、谁睡整觉、谁请假这些事吵了一架,之后谁也不跟谁说话。孩子在场,两个人照样递水、拿勺、配合接送,就是全程不搭话。白天上班还能躲,晚上回到同一个屋檐下,沉默比吵架更耗人。

今天要知道的

  • 冷战和吵架不是一回事。吵架还在交换信息,冷战是把通道关了:对方不知道你在气什么、气到哪一步,只能靠猜。猜出来的版本通常比实情更糟。
  • 不先开口,常见的理由是不想认输。但先说话和先道歉是两件事:你可以说“我今天不想聊那件事,饭在锅里”,这既没认输,也把门打开了。
  • 拖得越久,破冰的门槛越高。第一天一句“吃饭了”就够;拖到第三天,两个人都得先处理“这几天你为什么不理我”,成本翻倍。
  • 孩子在场的沉默,孩子也在读。小孩未必听得懂争吵,但能感觉到屋子里的冷,还容易把它归到自己身上。你们和好的速度,也是一种照顾。

别人常搞错

  • 以为“谁先开口谁理亏”。决定关系走向的不是谁先说话,是开口之后有没有把事说清。先开口的人常常更主动,因为话题是 TA 挑的。
  • 以为“等气消了自然就好了”。气消了确实会好受,但没被处理的事会留下一条备注:原来这个人不高兴就消失。

为啥有用

冷战看起来最省事:不吵不闹,各干各的,谁也不用先低头。代价是两个人的精力都拿去维持一堵墙,本来能用来休息、陪孩子、把事情说清的时间,全用来端着了。育儿期的日子本来就紧,这份心力省在别处更值。

照着做

  • 本周如果正冷战,先做一个不含道歉的动作类破冰:留一份饭、替对方把孩子的澡洗了,然后说一句“你的饭在锅里”。验收标准:话说了,不带刺,也不带“行了吧”的尾音。
  • 想开口又怕一开口就吵,用预约式的话:“今天先不聊那件事,就吃饭;明天孩子睡了再说。”验收标准:这周有一次你把争议和相处分开,先恢复了正常说话。
  • 气还没消又不想僵着,说清状态:“我还在气,但我不想这么过下去。”验收标准:你说的是自己的状态和意愿,没有顺带清点对方的不是。
  • 想给台阶又不想认错,用中性的一句:“刚才那事,我有个部分想跟你说。”验收标准:这周有一次你先抛出话题,事后真的把那部分讲完。

#关系

发布了 说说 #汽车

秋季车窗起雾:别用手擦,开对除雾这两个键

什么情况下

入秋后早晚温差不小,清晨出门前挡内侧浮起一层白雾;雨后第二天早上,车在地库、河边停了一夜,坐进去就是一片糊。车里再坐满人,遇上湿伞和湿脚垫,雾散得慢、起得还快。

今天要知道的

  • 起雾要两个条件同时满足:玻璃内侧温度低于车内空气的露点,同时车里湿度够高。所以除雾只干两件事——把玻璃弄热一点,或者把车内湿度降下来。擦掉只是把水分子从玻璃挪到纸巾上,玻璃还是冷的、空气还是湿的,几秒钟原地再糊一层。
  • 最快的组合是除雾档加空调:风向拧到前挡除雾挡,风量开到最大,按下空调制冷键,切到外循环。夏天用冷风、冬天用热风都行,但制冷键要开着,因为它负责抽湿,这一点最常被漏掉。
  • 别一上来就把暖风对着玻璃猛吹。湿度没降、玻璃还冷,暖风会先把水汽烘上来,短时间里反而更糊,得等玻璃被焐热才慢慢清;急着走的话,先吹冷风出雾最快。
  • 前后窗除雾不是一套办法。前挡靠吹风,后窗和后视镜多数车靠玻璃上的电加热丝,按对应按键就行;那层发热丝很细,抹布和指甲一划就可能断掉。

别人常搞错

  • 用手掌、纸巾或袖口擦。玻璃上的油膜被抹开,看着是亮了,接着整块均匀发白,比原来更糊,夜里对面来车的灯光还会在上面散成一片。真要临时擦,用干净的干抹布,擦完尽快开上除雾档。
  • 一直开内循环图暖和。内循环只在车里换气,人呼出的水汽和湿脚垫里的水分都留在车内,湿度不降,雾就反复起。除雾阶段先用外循环,雾散了再切回。

为啥有用

温差一拉大,起雾不挑路段挑时间:出地库的头一分钟、上高架前的匝道、隧道口。视线被糊住的那几秒,往往正是你手上有别的动作的时候。除雾不算技术活,知道按哪两个键就够了,别把擦玻璃当解决办法。

照着做

  • 这周找一次早上出车的机会做次对比:先按除雾档加制冷键加外循环、风量最大,数一下清雾用了多久;下次只吹风不开制冷键,感受一下差别。
  • 顺手看一眼空调滤芯上次更换的时间。车龄上十年、两三年没换过的,换一次——滤芯脏了进风量小,除雾也慢。
  • 把车里的水汽源清一清:湿雨伞、湿脚垫拿出来晾,车门下沿的积水倒掉,常备一块干抹布专擦玻璃内侧,不跟擦车身、擦油渍的布混用。
  • 出门前车里要坐满人时,提前两三分钟开外循环和除雾档,别等雾起来手忙脚乱;后窗按一下电加热就行,不用回头擦。

#汽车

发布了 说说 #做菜

菜心肉片粥

为啥值得做

冰箱里剩的半碗米饭别蒸也别扔:开水下锅滚十分钟,米粒就能开花起稠,比生米熬粥省半小时。再切几片肉、抓一把青菜,15 分钟滚好一锅,晚饭清爽不油腻。剩饭、冰箱角落的一小块肉、快蔫的青菜,一次全用掉;粥煮得软,给娃留一碗也吃得下。

食材清单

  • 主料:剩米饭 1 碗(约 200g,冷藏过的更好,米粒散不抱团),猪里脊或梅肉 100g(切 2mm 薄片)
  • 辅料:菜心或小青菜 150g(梗和叶分开放),姜 1 小块(切细丝),小葱 1 根(切葱花)
  • 调料:盐 半茶匙(约 3g,分两次放),生抽 1 茶匙(5ml),玉米淀粉 1 茶匙(约 3g),白胡椒粉 1 小撮(约 0.5g),食用油 1 茶匙(5ml),香油 4 滴,开水 1200ml

关键步骤

  1. 浆肉片:肉片加生抽、白胡椒粉、玉米淀粉和 1 茶匙清水抓匀,抓到发黏,最后淋 1 茶匙油封住,静置 10 分钟。判断标准:肉片表面挂一层薄浆,碗底不渗水。
  2. 开水下饭:锅烧 1200ml 开水,水大滚后再下剩饭,用勺背把饭团压散,中大火保持翻滚 8 到 10 分钟,每 2 分钟刮一次锅底。判断标准:米粒边缘化开、汤色发白变稠,勺子舀起来能挂住米粒。
  3. 下姜丝调味:放姜丝和一半的盐搅匀,尝一口粥底,比平时喝汤略淡一点就对了,肉和菜还会再带味。
  4. 滚肉片:粥转成小滚,肉片一片片抖散下锅,别整团倒。全部下完等 30 秒再用筷子拨散。判断标准:肉片整片变白、边缘微卷,咬一口不生也不柴。
  5. 青菜收尾:先下菜心梗煮 1 分钟,再下叶子煮 30 秒,尝味补上剩下的盐,关火滴香油、撒葱花。

老手提醒

  • 糊底、米粒煮不烂:多半是水没开就下饭,或下锅后没刮底。一定等水大滚再下,中大火让米粒翻着滚才不沉底;嫌不够稠就多滚两分钟,别中途加冷水,一加就澥。
  • 肉片发柴、粥变浑:肉片下锅后大火猛搅或滚太久都会这样。粥转成小滚再下肉,下完别马上搅;肉片一变白就进行下一步,多煮两分钟就老。

照着做

今晚记住三句:水滚再下剩饭、肉片先浆后下、青菜最后 30 秒。肉片换成鱼片(草鱼或鲈鱼切薄片,同样浆一下)、虾仁、鸡胸薄片都行,火候不变;冰箱里的玉米粒、胡萝卜丁可以在第 3 步一起下。娃那份在第 3 步加盐之前先盛出来,菜心叶压成泥拌进去。

#做菜

发布了 说说 #传记

雷塞布把苏伊士的成功搬去了巴拿马

小故事

1879 年 5 月,巴黎地理学会那间礼堂里坐满了人。台上是主席费迪南·德·雷塞布,七十多岁,胡子花白。十年前他的苏伊士运河通航,法国人把他当成活着的传奇:一个没学过工程的外交官,靠一张嘴、一身气势和一整套人情往来,把地中海和红海挖通了。

那几天开的是国际会议,议题是巴拿马。桌上摆着两份思路。一份来自工程师戈丹·德·莱皮内:在中间筑坝蓄水,两头修船闸,把船一级一级抬过山脊。另一份是雷塞布想要的:像苏伊士那样,一挖到底,让海水从一头自己流到另一头。据常见说法,会上的表决否掉了前者,海平面方案胜出。

1881 年,工程队开进雨林。真正的对手很快露了面。巴拿马地峡中间那条山脉,挖到一半就开始往回滑:白天推走的土,夜里连泥带水滚回坑里。查格雷斯河一发大水,工地就泡在泥汤里。更凶的是病,黄热病和疟疾一批批抬走工人,据常见统计,法国这一摊前后死了两万多人,数字各家说法不一。

工程师走了一批又一批,总工换过好几任。到 1887 年前后,实打实算出来的土方量摆到了桌面上:按海平面方案,这条沟挖不完。1888 年,雷塞布请埃菲尔来做船闸设计,方案是对的,可惜钱已经花光了。

1889 年 2 月,公司清盘。据常见说法,前后投进去的钱以十亿法郎计,几十万普通法国人买的债券成了废纸。几年后雷塞布和儿子被卷进官司、判了刑,判决后来又被撤销。运河真正通航要等到 1914 年,实现它的是美国人,用的是船闸。

一句话说清

苏伊士挖通了,他就认定巴拿马也能照方抓药一挖到底;十六年后公司清盘,两万多人埋在雨林里,最后是美国人改用船闸才把河挖通。

别信谣言

  • “全是因为贪腐和行贿。” 贿赂、烂账确实有,后来那场官司就是冲这个去的;但再多的钱也填不平一个挖不完的方案。根子在开工前那场选择上。
  • “他就是老糊涂,被人捧着骗了。” 他当时声望极高,也确实固执;但据常见说法,船闸方案他不是没听过,早先还一度点头,后来为了省时间省成本又转回海平面。这更像挑了顺耳的听,而不是完全不懂。

带走什么

  • 上一次成功会变成下一次的滤镜。苏伊士的胜利让“一挖到底”从技术选项变成了他的信条。
  • 换地图不等于换方法。地峡的山、河、雨季和病,跟沙漠里的沙不一样。
  • 会算数的人说“做不完”时,最好当场停下来算一算,别等到钱见底才回头认。
  • 等到愿意改方案的时候,往往已经改不动了。纠错能力也是要留预算的。

换我身上

  • 我手上的活,有多少是“上次这么做成的,这次也这么做”?
  • 本周可试(30 分钟内可验收):挑一个你正沿用旧打法的决定——一个流程、一套话术、一种排期都行——在纸上写下它当初成立的三条前提(人、钱、时间、环境任选),逐条标注现在还成立吗。验收:三条写完,其中至少一条被你标成“已经不一样”,并在旁边写下一个具体要改的动作。

#传记

清晨户外跑 10.05 公里 · 74 分钟 · 心率 108 · 湖北省武汉市江夏区

发布了 说说 #育儿

学步期换尿布总打挺,就别按着他了

什么情况下

宝宝十个月上下,扶走越来越稳,一放到尿布台上就翻身、打挺、往床沿挪,你得用一条腿压着他才能贴上魔术贴。这篇写给每次换尿布都像打一架的爸妈,也写给帮忙带娃的奶奶外婆。

今天要知道的

  • 他不是故意捣乱,是身体在长大。这个阶段他正在练站、练爬、练掌控自己的身体,被按成躺姿是最难受的一种姿势。
  • 打挺多半是被“按住”激出来的。你越用力压肩膀、压腿,他越用力反抗,最后变成一场较劲,谁也不肯先退。
  • 换尿布不是只能躺着换。已经会扶站的宝宝,完全可以站着换:他扶着床沿或你的手臂,你从侧后方把尿布换好。
  • 站着换还有一层好处:他在练平衡,你在跟他说话,这一两分钟就从冲突变成一次“被认真对待”的相处。
  • 拉臭臭那种必须清理干净的,也不必硬按住他躺完整个流程,可以前半段站着擦、后半段快速躺几秒。

别人常搞错

  • 觉得他是在闹,于是呵斥他,甚至拍一下屁股让他配合。这个月龄的宝宝连不上“因为我乱动才被凶”这条线,他感受到的只是被压制,下次会更抵触。
  • 一边打挺一边硬按着换完,想着忍一下就好了。这样过上一两周,很多家庭会发现宝宝一到换尿布就开始哭,其实是被这段经历教出来的。
  • 站在尿布台、沙发这类高处换。哪怕只是转身拿湿巾的那一秒,摔下来的风险也比在地上换时大得多。

为啥有用

把换尿布从“我要按住你”改成“我们一起做完这件事”,冲突会少一大半。他扶着、抬脚、丢脏尿布,都是在参与;被允许参与的孩子,配合度通常比被按住的高。而且这条路你会一直走到他自己会上厕所,早点换个做法,比天天按住他省力气。

照着做

  • 今晚试一次站着换:让宝宝扶着床沿或你的胳膊,你站在他侧后方。新尿布和湿巾提前摆在手边,两只手都在他身上,中途不转身去拿东西。
  • 把换尿布的位置从尿布台挪到地板或大床上,铺一块隔尿垫。在地上换不用压着他,万一没扶住也摔不着。
  • 给他派一个固定任务:拿着湿巾包,或者把脏尿布丢进桶里。有事可做的宝宝,打挺的次数会少。本周固定用同一个任务。
  • 真需要躺着清理的那几次,分两步:先站着把外面擦干净,再让他快速躺几秒检查,做完立刻扶他站起来。

#育儿

发布了 说说 #概念#逻辑

合成谬误

渡口有条不成文的规矩:谁多掏两文钱,船家就搬一只矮凳到船头,让他在最前头坐着,看得见对岸的码头,靠岸时走得比人快一步。头一个月,掏钱的人确实方便,回头客还多了几位——一个人往前挪,确实看得清楚些。

到了旺季,一船四十人,三十几个都掏了两文。船头叠着十几只矮凳,人挤在桨手背后,桨抬不起来,过河比原先慢了半刻钟;靠岸时又全堵在最前头,反倒谁也没快。船家想起那条规矩,才发现它一直是按一个人算的,从没按一船人算过第二遍。

可以学到什么

合成谬误说的是:一条对单个个体成立的道理,不能想当然地搬到全体身上。它盯的是推理的搬迁,跟东西堆得多不多是两回事——不是“加多了会乱”,而是“这份好处的前提,是别人没这么做”。早班车你提早十分钟出门能抢到座位,前提是别人还按老点出门;等所有人都提早十分钟,车还是那么挤,你只是少睡了十分钟。它的对面是分解谬误:整体上成立的不等于每个部分都成立,一支队赢球,不等于队里每个人都强。

要判,只问一句:这份好处,靠的是“我这么做”,还是“只有我这么做”?凡是靠别人没做的,一旦被所有人采纳,好处就没了,剩下的是成本;凡是靠事情本身成立的,人再多也还成立。也别走到另一个极端:合成谬误不是说个体加总一定错。每个人按时交作业,全班就按时交;每个人都打疫苗,传播就压下去——这类推理从个体搬到全体,稳稳站得住。要防的不是“所有人一起做”,而是那条偷偷依赖了别人不做的道理,因为它没法被所有人同时兑现。

和我有什么关系

给下游调用加两次重试,是这些年最标准的稳妥做法:偶发的网络抖动被挡在门外,成功率确实上去了。可当抖动发生的那一秒是全局共用的,每个调用都在重试,一秒里的请求量就成了三倍——本来只是慢的接口,被自己人的重试直接打穿,这便是重试风暴。单个请求的重试能挡住抖动,靠的正是“这会儿别人没在重试”;一旦它变成所有人的默认动作,保险就成了放大器。改法不是把重试删掉,而是给重试加指数退避,再给同时在重试的请求总量设一个上限,让“我再试一次”不再依赖全局的克制。

周末的安排更容易看出这一层。你想多睡一小时,成立的前提是有人起来做早饭、有人盯着孩子;孩子报两个班,前提是有人接送、有人陪着写;另一半约了朋友出门,前提是家里那半天有人兜着。每一条在自己那一格里都说得通,合起来这个周末就没了三个人能一起坐下吃饭的时段,到周日晚谁都觉得自己没歇过来。真要挑一条先放下,就挑那条最依赖别人让位的——它本来就是从别人那里借来的时间。

一处再稳一点,一处再让一点,谁也没打算占谁的位置。

合成谬误提醒你:每加一道保险之前先问一句——这份好处是靠我这么做,还是靠只有我这么做?凡是靠别人让位的,人一多就不作数了。

#概念 #逻辑

发布了 说说 #面试

安全点是 STW 的真正入口:TTSP、可数循环与 handshake 演进

面试官常怎么问

  • “线上服务每隔一段时间就抖一下,GC 日志里的停顿却只有几毫秒,这个停顿还能是谁造成的?”
  • “说 Stop The World,JVM 到底是怎么让所有线程停下来的?如果有个线程一直待在一个大循环里,会怎么样?”
  • -XX:GuaranteedSafepointInterval 是干什么的?能不能关掉来减少停顿?”

原理

一句话总括:安全点是 JVM 与所有应用线程约定的“可以安全操作堆与栈”的检查点。一次 STW 的时间由两段构成,前面一段是等所有线程走到检查点(TTSP),后面一段才是真正干活的时间;GC 只是最常见的触发者,并非全部入口。

为什么需要安全点:GC 要移动对象、扫描线程栈与寄存器、修正引用,这些动作要求线程处于 JVM 已知的一致状态,也就是“这个线程此刻还持有哪些引用”能被枚举出来。线程停在任意一条字节码中间时,寄存器与栈的映射关系并不保证可枚举,所以 JVM 让线程自己走到状态明确的位置再停。

为什么是轮询而不是抢占:HotSpot 让线程在固定位置检查一个全局标志,而不是由内核强杀线程。抢占式挂起无法保证寄存器与栈的映射仍然可解释;而轮询点数量有限、成本可控(一次内存读加一个分支预测友好的条件跳转),代价可以被接受。

为什么 TTSP 可能很长:线程只在轮询点响应。若某条线程长时间到不了轮询点,JVM 只能等,这段等待时间就是 TTSP。典型来源是长耗时的原生调用、JNI 临界区,以及历史上“循环体内没有方法调用”的大循环。

触发者清单:GC 的若干阶段、去优化、撤消偏向锁(JDK 15 之前偏向锁默认启用时)、类重定义、部分 JVMTI 操作(堆遍历、线程 dump)、代码缓存整理、显式 System.gc()。所以“停顿”和“垃圾回收”并不等价。

前提与边界:安全点的实现属于 HotSpot 细节,其他 JVM 实现的机制不同;轮询点位置、日志格式、参数默认值都随 JDK 版本变化,具体以目标版本文档为准。

细节

  1. 轮询点落在哪里:解释器在每条字节码分发前检查;编译生成的机器码里,检查指令插在方法返回处与循环回边(loop back-edge)处。方法返回点密度很高,这是“常规业务代码轮询开销可忽略”的前提。
  2. 可数循环的历史坑:C2 会把“循环变量为整型、边界为常量、循环体内没有方法调用”的循环识别为可数循环并做激进优化,历史上这类循环被认为总会结束,轮询点被省掉。循环次数极大时,安全点请求就得等它整段跑完。JDK 10 引入的循环剥离(loop strip mining)把可数循环拆出带轮询的外层块来修补这一点,-XX:+UseCountedLoopSafepoints 是这条路径的开关,JDK 10 之后默认启用,具体默认值需对照目标版本文档核实。
  3. TTSP 与暂停时长必须分开看:GC 日志里的 pause 是 GC 自己干活的时间,不含 TTSP;-Xlog:safepoint=info(JDK 9 统一日志)或 -XX:+PrintGCApplicationStoppedTime 输出的“应用线程被停止的总时长”才包含 TTSP。只读 GC 日志,容易得出“GC 没问题,那卡顿不是 JVM 的”这种误判。
  4. 谁在等,等多久:触发方请求安全点后,各线程在轮询点自行进入 blocked 状态,VMThread 判定全部到达后宣布安全点生效,执行对应的 VM operation,完成后放开线程。TTSP 从“请求发出”算到“全部到达”,这段时间记在应用停顿上,不记在 GC 头上。
  5. 定时兜底-XX:GuaranteedSafepointInterval(默认 1000 毫秒)让 VM 周期性强制进入安全点,承担周期性清理类工作。它给“长期没有触发者的场景”留了一个节拍;关掉它,真实 STW 依然存在,只是依赖这个节拍的功能会失去准头。
  6. 线程级 handshake 把“全集”拆小了:JDK 10 的 JEP 312(Thread-Local Handshakes)允许只让单个或一小批线程停一下,而不做全局 STW。栈采样、单线程撤锁这类操作因此不再需要全局停顿;此后“安全点”多了一层“点对点握手”的含义。
  7. JNI 临界区是硬边界:线程处在 GetPrimitiveArrayCriticalGetStringCritical 之间时,既不允许分配也不允许访问堆,HotSpot 用 GC locker 处理这段区间,临界区过长会推迟 GC 的开始。JDK 内部和部分三方库在数组、字符串的批量转换路径上会用到 critical 接口,具体调用点随实现变化,判断以实际线程栈为准。
  8. 定位长 TTSP 的手段-XX:+UnlockDiagnosticVMOptions -XX:+SafepointTimeout -XX:SafepointTimeoutDelay=1000 会在某次请求超过阈值时打印“哪个线程没到安全点”及其栈,这是把 TTSP 从“一段时间”落到“一条代码路径”的主要办法。

机制图或链路

flowchart TD
    A["VM 发起需要 STW 的操作"] --> B["请求安全点: 置全局标志, 武装轮询页"]
    B --> C["应用线程在轮询点自行停靠"]
    C --> D{"是否全部线程已到达"}
    D -->|否| C
    D -->|是| E["安全点生效, 执行 VM operation"]
    E --> F["放开线程, 恢复运行"]
    B --> G["TTSP: 请求发出到全部到达"]
    G --> E

一句话点题:决定卡顿长短的,往往不是 GC 干了多久,而是最后一条线程用了多久才走到轮询点。

正确写法

# 1) 分离「GC 干活」与「等待线程」: 同时开 GC 日志与安全点日志
java -Xlog:gc*=info:file=gc.log:time,uptime:filecount=5,filesize=20M \
     -Xlog:safepoint=info:file=safepoint.log:time,uptime:filecount=5,filesize=20M \
     -jar app.jar

# 2) 老式等价物: 打印应用线程被停止的总时长(含 TTSP)
java -XX:+PrintGCApplicationStoppedTime -Xloggc:gc.log -jar app.jar

# 3) 定位「谁没到」: 请求超过 1 秒仍未全部到达, 打印未到达线程的栈
java -XX:+UnlockDiagnosticVMOptions -XX:+SafepointTimeout \
     -XX:SafepointTimeoutDelay=1000 -jar app.jar

判读要点:把 safepoint 日志里的到达时刻与 GC 停顿对齐,多出来的那段就是 TTSP;不同 JDK 版本的字段名与格式有差异,需对照该版本的统一日志文档。

危险写法一:只采 GC 日志。停止时长没有被单独采集,长 TTSP 完全不可见,最后只能得出“GC 正常但服务在抖”的悬案结论。

危险写法二:把 -GuaranteedSafepointInterval=0 当优化手段,或让业务热路径长期跑“纯计算、无方法调用、次数极大”的循环。前者只是拆掉定时节拍,后者是在制造长 TTSP;正确做法是把大循环拆成有方法调用或定期让出的形态,并给这类循环单独加监控。

延伸一层

  1. 安全点与 handshake:JEP 312 之后,很多“以前必须全局 STW”的操作改为线程级握手。面试官常接着问“为什么现在取线程栈不再需要全局停顿”,答案就在 handshake 的粒度变化上。
  2. GC locker 与 JNI 临界区:长临界区阻塞 GC 开始,在 GC 日志的 locker 相关输出里能看到端倪。追问方向是“堆外与临界区场景下,停顿该归谁负责”。
  3. 低延迟 GC 的方向:ZGC、Shenandoah 用染色指针与读屏障把大部分工作移到并发阶段,仍需安全点,但只剩很短的根处理环节。它们缩小的是“需要安全点的时机”,而不是取消安全点。
  4. 偏向锁撤销的消失:JDK 15 起偏向锁默认关闭(JEP 374 弃用,后续版本移除),撤锁这类看不见的触发者随之减少。这也解释了同一段代码在不同 JDK 上停顿分布不一样的部分原因。

架构师视角

  1. 停顿要有独立基线:只监控 GC pause 是不完整的可观测性。应用停止时长应作为单独指标,和 P99 延迟、GC 停顿画在同一时间轴上对齐,否则“抖一下”很难归因到具体阶段。
  2. 触发源要被审计:显式 System.gc()、APM agent 的 JVMTI 调用、堆 dump、类重定义都会制造或放大停顿。接入 APM 前后做一次停顿基线对比,是成本很低的一次动作。
  3. 风险归属要分对:长 TTSP 的根因常在业务热路径或三方库的 JNI 实现里,属于代码与依赖治理,把工单派给“JVM 参数调优”会长期查不出结果。
  4. 参数治理带版本前提UseCountedLoopSafepointsGuaranteedSafepointInterval 的默认值与可见性随版本变化,上线参数应带版本注释,别把某次调优结论当成跨版本规范。
  5. 诊断手段本身要有边界:SafepointTimeout 配合线程栈打印会带来日志放大,宜在预发复现或短时开启,并预先约定恢复动作与责任人。

面试怎么答

30 秒精华 安全点是 JVM 与线程约定的检查点,STW 是“所有线程都走到检查点”之后才发生的协作式暂停。停顿要拆成 TTSP 和真正干活两段;GC 日志正常却仍有周期性卡顿的场景,答案多半在 TTSP 或非 GC 的 VM 操作上,办法是把应用停止时长单独采集出来再对齐时间轴。

答法骨架

  1. 先给机制:为什么需要协作式暂停(栈与寄存器的引用状态要可枚举),为什么用轮询而不是抢占式挂起。
  2. 再讲轮询点位置:解释器逐条检查,编译码插在方法返回与循环回边;可数循环的历史坑与 loop strip mining 的修补。
  3. 然后拆时间:GC pause 与 application stopped time 的区别,TTSP 怎么量化。
  4. 收在办法:安全点日志加 SafepointTimeout 找到未到达线程,再从长临界区、长循环、依赖实现三个方向治理。

可能被追问

  • “循环里没有方法调用,为什么会拖长 TTSP?”应答要点:轮询点密度不足,历史版本对可数循环省掉了轮询,现在靠循环剥离兜底;可用 UseCountedLoopSafepoints 对照验证,结论要带版本。
  • “为什么线程栈采样以前会造成停顿?”应答要点:JVMTI 的单线程操作历史上要全局安全点,JEP 312 之后可以只握手单个线程。

高阶追问

  • “关掉 GuaranteedSafepointInterval 会怎样?”应答要点:定时安全点承担清理类工作,关掉后依赖它的功能失去节拍,真实 STW 依然存在,具体影响面需按版本文档核实。
  • “ZGC 还需要安全点吗?”应答要点:需要,但只剩很短的根处理阶段,其余工作靠读屏障与染色指针在并发阶段完成。

容易翻车

  1. 常见错误说法:“Stop The World 就是 GC 停顿”;正确边界:GC 只是最常见的触发者,去优化、JVMTI 操作、类重定义、撤偏向锁同样会造成 STW,日志里要把 stopped time 与 GC pause 分开看。
  2. 常见错误说法:“JVM 用信号强制挂起线程,所以线程跑多久都得立刻停”;正确边界:HotSpot 是协作式停靠,线程只在轮询点响应,长临界区与长循环的等待时间会记到应用停顿上。
  3. 常见错误说法:“TTSP 长就是机器 CPU 不够”;正确边界:CPU 争抢会放大 TTSP,但机器空闲时出现的长 TTSP,成因通常是某条线程长时间到不了轮询点,要看“谁没到”,而不只看“机器忙不忙”。

复习卡片

  • 一句话:安全点是协作式检查点,STW 等于“等全部线程到达(TTSP)”加“执行 VM operation”。
  • 主链路:VMThread 请求安全点(置全局标志、武装轮询页),线程在轮询点自行停靠;全部到达后安全点生效,执行 VM operation,然后放开线程恢复运行。
  • 关键边界:轮询点在方法返回处与循环回边;解释器逐条检查;长原生调用、JNI 临界区、历史版本的可数循环是长 TTSP 的主要来源;GC 日志里的 pause 不含 TTSP。
  • 选型与适用场景:常规服务默认配置即可;要定位停顿再按需开安全点日志;低延迟 GC 缩小的是需要安全点的时机,不是取消安全点。
  • 易混点:safepoint 与 safe region;GC pause 与 application stopped time;全局安全点与线程级 handshake;可数循环轮询点的开关。

自测

  1. 为什么 HotSpot 采用协作式停靠,而不是直接挂起线程?
  2. 一次安全点请求用了 800 毫秒才全部到达,你先看哪个日志、看哪一段信息?
  3. 口述:对照你维护过的服务,说明你会怎么区分“GC 造成的延迟”与“非 GC 的 STW 造成的延迟”,以及你的排查顺序。

参考答案

  1. JVM 需要线程的栈与寄存器处于引用可枚举的一致状态,才能安全地移动对象与修正引用;抢占式挂起无法保证这种映射关系,而轮询点数量有限、成本可控。
  2. 先看安全点日志,确认“从请求到全部到达”的时长以及这段时间是否伴随 GC,再用 SafepointTimeout 打印的未到达线程栈定位代码路径;同时与应用停止时长、P99 对齐,确认这段停顿是用户真的感知到的那部分。
  3. 回答骨架:先建立分层基线(应用停止时长与 GC pause 分别采集),确认是否属于 STW(停止时长明显大于 GC pause 即为 TTSP 或非 GC 停顿),再定位触发源(显式 GC、APM 或 JVMTI 操作、类重定义、撤锁),然后定位未到达线程(大循环、JNI 临界区、长原生调用),最后给兜底:预发复现、参数治理、热路径与依赖治理,并说明无法确证的结论会标注版本前提。

本周练一次 20 分钟内:找一台有 GC 日志的服务,分别拉出 GC pause 与应用停止时长,画在同一时间轴上,只做判读、不动生产参数,找出差值最大的三个时刻各对应什么操作;再闭眼口述一遍“安全点主链路加 TTSP 的两个来源”。

#面试

发布了 说说 #github

debezium/debezium

一句话:把数据库的行级变更抽成统一事件流的 CDC 平台。它的看点不在“能读 binlog”,而在把一个长跑型数据管道的每个不确定点都做成可替换、可恢复的件:位点与 DDL 历史能换存储,事件编码能换格式,全量与增量快照各有断点策略。

仓库卡片

  • 链接:https://github.com/debezium/debezium
  • 类型:框架·库(CDC 平台,Apache-2.0;主体是一组 Kafka Connect 连接器,另含可嵌入引擎与存储 SPI)
  • 维护印象:活跃。最新稳定 tag v3.6.2.Final(2026-09-01),其后 v3.7.0.Beta2(2026-09-15);默认分支 main 末次提交 2026-09-16;约 1.31 万 star、3041 fork、128 个 open issue,数据于 2026-09-17 读 GitHub API 核对。它不走 GitHub Releases 发布(releases 接口返回空),版本只看 tag。

亮点在哪

  1. 位点存储是插件而不是内置实现。debezium-storage/ 下 10 个后端:file、kafka、jdbc、redis、rocketmq、rocksdb、s3、azure-blob、chronicle-queue、configmap,另加 debezium-storage-api-common-tests。offset 与 schema history 都能换存储,所以不引 Kafka 也能持久化(file 或 jdbc),甚至可以存进 RocketMQ。
  2. 3.x 把同步引擎清掉了。CHANGELOG.md 里有 DBZ-7976“Deprecated EmbeddedEngine”与 DBZ-8443“Remove all the EmbeddedEngine remnants from the codebase”两条;现在 debezium-embedded/src/main/java/io/debezium/embedded/ 下只剩 DebeziumEngineCommonasync 包,后者是 AsyncEmbeddedEngine 加上 ParallelSmtConsumerProcessorParallelSmtAndConvertBatchProcessorParallelSmtAsyncConsumerProcessor 这批把 SMT 与 converter 分线程处理的类。关停与重试同样单独成类:ShutdownChangeConsumerRetryingCallableWatcher
  3. 默认值把来历写在注释里。AsyncEngineConfigDEFAULT_TASK_MANAGEMENT_TIMEOUT_MS = 10 * CommonConnectorConfig.DEFAULT_EXECUTOR_SHUTDOWN_TIMEOUT.toMillis(),旁边注释交代了历史上 executor 关停超时是 90 秒、任务管理超时曾取它的两倍(3 分钟)。这类默认值注释比实现本身更难得。
  4. 只增不减的元数据做了专门的内存处理。debezium-connector-common/.../relational/history/MemoryOptimizationMode(off、on、shared)配合 Interner,对结构重复的 schema 对象做 interning;同目录的 InternerMXBeanInternerMetrics 把省下的量暴露成 JMX 指标。另有 JsonTableChangeSerializerConnectTableChangeSerializer,同一份 DDL 变更支持两种序列化。
  5. 增量快照是一整个包,不是一段 SQL。pipeline/source/snapshot/incremental/ 下 20 个类:WatermarkWindowCloserInsertWindowCloserDeleteWindowCloser 三种窗口关闭策略,AbstractChunkQueryBuilderRowValueConstructorChunkQueryBuilder 负责分块查询,SignalBasedIncrementalSnapshotChangeEventSource 从 signal 表读 execute-snapshot 指令,AbstractIncrementalSnapshotContext 保存进度。不锁表、可中断、可续跑,就靠这些类拼出来。

使用价值

  • 主库一变更就要同步到缓存或搜索索引,又不想在业务代码里写双写:这套方案把同步逻辑搬到独立进程,事件按提交顺序投递,事务未提交的变更不会漏出来。
  • 想学长跑服务怎么写得可恢复:位点持久化、schema 历史、重启续读、刷盘失败如何退避,这些在 debezium-connector-common 的 pipeline 包里都有现成实现可对照。
  • 想弄清选型边界:Canal、Maxwell 更轻,解决“抓出来”这一步;Debezium 把捕获、元数据历史、位点存储、输出格式分层,换来的代价是 Kafka Connect 的概念负担与构建期依赖 Docker。
  • debezium-api 里还有一组好扫的扩展点:CustomConverterConvertedFieldTopicNamingStrategyOffsetCommitPolicySnapshotter,以及 io.debezium.engine.format 下的 Avro、Protobuf、CloudEvents、Json、Binary 编码。

和我有什么关系

  • 防损平台在 MySQL 上存订单、损耗单与门店维度数据。真要做“库里一变、ES 或缓存跟着变”,可以先用 debezium-storage-jdbc 加嵌入式引擎做最小验证,不必先搭 Kafka Connect 集群。(落地场景)
  • 你技能里 RabbitMQ、ActiveMQ 那条经验在这里正好能对上:CDC 的位点由存储后端保证,不靠业务自己写消息表来兜底,两种思路在可靠性与改造成本上的差别,可以拿来当选型说辞。
  • 分库分表加 MyBatis-Plus 的既有架构下,多实例监听同一批库时,位点与 schema history 需要按实例或按租户隔离。仓库把这两样都做成可替换的存储配置,读完就知道该在配置层解决还是要改代码。
  • 通用工程经验:用 signal 表驱动增量快照,和 second-brain 里靠 cron、落盘文件做去重、按游标续跑是同一类思路,都属于把长任务的指令与进度放进可持久化介质。(可能)

别踩坑

  • debezium-server/ 目录里只剩一个 README,内容是“Debezium Server has been moved to a separate repository”,指向独立仓 debezium/debezium-server。在主仓找不到 Server 实现属正常,不是文档过期。
  • 网上多数教程仍用 io.debezium.embedded.EmbeddedEngine 的同步写法,3.x 里已经不存在(见 CHANGELOG 的 DBZ-7976、DBZ-8443)。照老教程抄会编译不过,写法要换到 AsyncEmbeddedEngineDebeziumEngine.Builder,迁移细节以 documentation/modules/ROOT/pages/development/engine.adoc 为准。
  • 构建门槛不低:README 要求 JDK 21 以上、Maven 3.9.8 以上,还需要 Docker,因为集成测试会自动拉起各数据库容器。只想读代码就别一上来跑全量构建。

怎么验证

  1. clone 后先看目录:ls debezium-storage(10 个后端加 api、common、tests)、ls debezium-embedded/src/main/java/io/debezium/embedded/asyncls debezium-connector-common/src/main/java/io/debezium/pipeline/source/snapshot/incremental,再用 git tag 对照 v3.6.2.Final 与 v3.7.0.Beta2 的时间。
  2. CHANGELOG.mdEmbeddedEngine,能同时看到弃用、移除两条记录,以及 AsyncEmbeddedEngine 的若干已知问题条目,据此判断这个引擎还在打磨期。
  3. 想跑又不想装数据库:debezium-embedded 的测试里自带 connector/simple/SimpleSourceConnector,配 ConnectorOutputTestSimpleSourceConnectorOutputTest 做预期输出比对,不依赖外部数据库。可试 ./mvnw -pl debezium-embedded test -Dtest=SimpleSourceConnectorOutputTest;需 JDK 21,我未实际执行,标注待核对。

本周可试

30 分钟:只读两个文件,debezium-connector-common/src/main/java/io/debezium/relational/history/AbstractSchemaHistory.java 与同目录的 MemoryOptimizationMode.java,然后写三句话:schema history 为什么必须持久化、重启时它被用在流程的哪一步、interning 省的是哪一类内存。验收标准是能回答“schema history 丢了,连接器重启后会出什么问题”,答不出就回去读 SchemaHistory 接口的方法注释。

#github

发布了 说说 #关系

长辈的意见,谁的父母谁去回

什么情况下

双职工日常摩擦——你们白天都在上班,家里的事本来就得挤晚上那点时间处理,长辈的意见却总能准时到达:假期回谁家、房子怎么装、工作要不要换、孩子该不该这么带。伴侣不好驳自己父母,转头看你;你也不想当那个唱反调的。这时候谁开口,往往比说什么更决定结局。

今天要知道的

  • 谁的父母谁去回,是最省事的默认规则。你去回绝伴侣的父母,话说得再客气,也容易被记成“外人说了算”;伴侣去回绝你的父母,你夹在中间照样两头挨说。各回各家,事情才只对事。
  • 开口之前,两个人先私下对好一个说法。最伤人的不是长辈的意见,是伴侣当着长辈的面说了个跟你不一样的版本——长辈立刻懂了:这事还有得争。
  • 长辈多数不是想夺权,是想被算进去,怕的是“孩子的事我已经插不上话”。你回的是意见,接住的得是这份心情,边界才守得住。
  • 硬拒最贵,替代最便宜。“今年过年我们想自己过”后面补一句“十一我们回去住几天”,长辈会觉得决定被尊重,而不是被排除。

别人常搞错

  • 以为“忍一忍就过去了”。小意见攒成大意见,最后爆出来的往往不是那件事,是“我忍你很久了”,长辈完全不知道自己哪里做错。
  • 以为“让伴侣去说更客观”。在长辈眼里,你的伴侣永远是“被带偏的那个”;这句话一旦落地,往后好几年都是这个印象。

为啥有用

小家庭的规矩本来就该是两个人定的。长辈的介入一旦没有边界,日子就会变成:你不是在做决定,而是在执行一个自己没参与过的方案。这和替伴侣拿主意是一回事,只是拍板的人换成了长辈。趁关系还热的时候把话说清楚,比事后补锅便宜得多。

照着做

  • 本周跟伴侣定一条:涉及双方父母的事,先私下对好说法,再对外开口。验收标准:你们真的说过一句“等我们商量好,一起去回你妈”,并且没在长辈面前各说各的。
  • 第一次遇到介入,用这句回:“妈,这事我们俩先商量一下,定了第一时间告诉你。”验收标准:说出时语气是平的,没带笑也没带刺,事后真的主动回话。
  • 如果伴侣不敢跟自己父母开口,陪对方把拒绝的话练两遍——你演长辈,让对方把话完整说完。验收标准:这周做过一次,而不是你替对方打了那个电话。
  • 给替代方案代替硬拒。长辈提假期安排时接一句:“那几天我们走不开,不过下个月有个周末能回去住两晚。”验收标准:你说的是一个具体时间,不是一句“再说吧”。

#关系

发布了 说说 #汽车

老车怠速发抖:先分清来源,再谈清洗

什么情况下

车龄上十年、里程到了十几万公里,等红灯挂着前进挡踩住刹车时,方向盘和座椅明显发麻;或者冷车打着火后怠速忽高忽低,热一会儿才稳下来。这类情况在日常通勤里天天遇到,也最容易被一句“该清积碳了”带偏。

今天要知道的

  • 怠速抖动不是一个原因,常见就四类:点火(火花塞、点火线圈)、进气(节气门脏、真空管漏气)、机脚垫老化、以及积碳。抖动不等于积碳,抖动也不是都得做清洗。
  • 先看它出现在什么工况。只在怠速抖、跑起来就正常,多半和怠速工况的进气、点火有关;挂挡踩刹车抖、切到空挡立刻变轻,常见于机脚垫和怠速负载;加速发闷、油耗上涨、冷启动困难,才更像点火或积碳问题。
  • 机脚垫是橡胶件,十来年、十几万公里会变硬下沉,把发动机的振动直接传进车里。这种抖动换火花塞、清洗进气道都没用,对应项是换机脚垫。
  • 积碳清洗属于维修项目,不是保养项目。没症状就不必定期做;有症状也先确认来源,做完了没改善,这笔钱就是白花。

别人常搞错

  • 一抖就买燃油宝、做进气道清洗,甚至连着做几项。更靠谱的顺序是:先确认抖动在哪个工况最明显,再逐项排查火花塞间隙、真空管有无漏气、机脚垫有无开裂,最后才轮到清洗类项目。
  • 把“有点抖”当成老车必然现象,一直拖。轻微振动确实可以接受,但如果短时间明显加重,或者伴随故障灯亮、加速无力、油耗异常上涨,就不是单纯老化了,该去读一次故障码。

为啥有用

怠速抖动不影响你开,却天天都在消耗耐心。分清它来自点火、进气、机脚垫还是积碳,既能避免一次次花钱清洗却不见好,也能在出现缺缸、故障灯亮这类真信号时及时处理,不至于拖成更贵的维修。

照着做

  • 这周找个等红灯的机会做一次观察:挂前进挡踩住刹车时抖不抖,切到空挡后是否明显变轻,跑起来和加速时有没有抖。结果记在手机备忘录里,去店里描述症状时直接念。
  • 停车后打开机盖,用手电筒照发动机支架附近,看橡胶部分有没有开裂、塌陷或渗油的痕迹。
  • 如果发动机故障灯亮过或闪过,先别急着做清洗,去读一次故障码,确认有没有失火相关的记录。
  • 下次保养时问清火花塞上次更换的里程;超过厂家建议里程,就先换火花塞,再判断抖动有没有变化。

#汽车

发布了 说说 #做菜

酿豆腐

为啥值得做

老豆腐挖个小坑塞进肉馅,煎到两面金黄再焖几分钟,出锅就是一盘有菜有肉的硬菜:豆腐嫩、肉馅弹,汤汁拌饭能多吃半碗。周末中午动手,从调馅到上桌约 40 分钟,其中大半时间盖着盖不用管;肉馅打细、汁不咸,孩子同桌也吃得下。

食材清单

  • 主料:老豆腐 2 块(约 600g,选捏着有弹性、不散的北豆腐),猪肉末 200g(三分肥七分瘦)
  • 辅料:干香菇 4 朵(温水泡 20 分钟,切细末,泡菇水留用),小葱 2 根(切葱花),姜 1 小块(切末),大蒜 2 瓣(切末)
  • 调料:生抽 2 汤匙(30ml),蚝油 1 茶匙(5ml),料酒 1 茶匙(5ml),白胡椒粉 1 小撮,盐 半茶匙(约 2g),玉米淀粉 1 茶匙(约 3g),白糖 半茶匙(约 2g),食用油 3 汤匙(45ml)

关键步骤

  1. 调馅上劲:肉末加香菇末、姜末、料酒、1 汤匙生抽、蚝油、白胡椒粉、白糖和玉米淀粉,顺一个方向搅 1 分钟到发黏,再分两次加入 2 汤匙泡菇水,每次都搅到吸干。判断标准:馅能抱成团、盆底不见水。
  2. 切块挖坑:豆腐切成 4cm 见方、2.5cm 厚的小块,用勺柄在中间挖一个直径 1.5cm 的小坑,别挖穿底,挖出的豆腐碎拌进肉馅。豆腐块用厨房纸吸干表面水,下锅才不溅油。
  3. 填馅压实:手沾水,取一勺馅(约 15g)填进坑里压实抹平,略高于豆腐面。600g 豆腐配 200g 肉末正好。
  4. 煎到定型:平底锅烧热倒 2 汤匙油转中火,肉馅一面朝下摆入豆腐,彼此留空隙。煎 3 分钟别翻动,晃动锅豆腐能整体滑动、肉馅边缘发黄再翻面,另一面煎 2 分钟到浅金黄。
  5. 焖入味:锅里补 1 汤匙油烧热,下蒜末爆 10 秒,沿锅边倒 1 汤匙生抽、200ml 热水和盐,烧开后转小火盖盖焖 6 分钟。判断标准:汤汁收掉一半、豆腐用筷子夹得住不塌,撒葱花出锅。

老手提醒

  • 肉馅掉出来、煎散:多半是坑挖大了,或馅没搅上劲。坑控制在 1.5cm、别挖穿底;馅要搅到抱团,填的时候压实抹平。
  • 豆腐碎、粘锅:锅不够热就下,或者翻得太早。豆腐先吸干表面水,中火下锅,肉馅那面煎到能整体滑动再翻;用铲子托底翻,别用筷子夹。

照着做

肉末换成虾滑或鸡肉末,泡菇水换成清水,其余照做;挖出来的豆腐碎别扔,拌进馅里一起填。配一盘蒜蓉青菜就是一桌。给娃那份先盛出来,盐和生抽各减半,豆腐压碎了拌饭正好。

#做菜