小谢在努力 - admin
type: channel
link: https://www.xxc520.cn/index.php/author/1/
description: admin
items: Java 集合与泛型的工程化边界：如何设计不会被误修改的集合 API
items: 冰箱里那半棵坏掉的白菜：我开始承认自己吃不了那么多
items: 关系走到分岔口时，如何判断该坚持还是体面放手
items: 工作久了，我不再追着学习路线跑：把职业焦虑写成一页问题笔记
items: 下班后的两个小时为什么总在消失：停止把夜晚当成第二个工作日
items: 异步任务里的 traceId 为什么消失：用 MDC 打通 Java 线程池日志上下文
items: 用可组合规则对象重构订单提交校验：告别散落的 if 与隐式顺序
items: Java 微服务调用的可靠性边界：超时、重试与幂等如何一起设计
items: 31岁，关于爱情、喜欢和自己
items: 库存扣减为什么会被覆盖：Spring Data JPA 与 MySQL 的并发控制实践

Java 集合与泛型的工程化边界：如何设计不会被误修改的集合 API
type: item
date: 2026-08-17T12:01:04+08:00
description: 集合和泛型真正棘手的地方，往往不在语法本身，而在模块之间如何传递数据。本文围绕“方法返回的集合被调用方误修改”这一常见问题，讲清泛型不变性、PECS、不可变集合、防御性拷贝，以及工程实践中的常见陷阱。在业务代码里，集合几乎无处不在：查询结果是 List，配置映射是 Map，权限集合是 Set。很多问题并不是集合不会用，而是集合越过方法、模块或线程边界之后，所有权变得模糊了。一个典型场景是：服务对象内部维护了一组状态，方法把这个 List 直接返回，调用方为了方便调用了 clear() 或 add()，结果内部状态被悄悄改变。代码没有报错，编译器也不会提醒，但后续行为已经失去控制。这篇文章集中讨论一个具体问题：如何设计集合与泛型 API，让调用方能够正常使用数据，却不能意外破坏对象内部状态。一、问题从一个直接返回开始下面的代码看起来很自然：import java.util.ArrayList; import java.util.List; public class UserGroup { private final List<String> members = new ArrayList<>(); public void addMember(String name) { members.add(name); } public List<String> getMembers() { return members; } }调用方可能这样使用：UserGroup group = new UserGroup(); group.addMember("Alice"); group.addMember("Bob"); List<String> members = group.getMembers(); members.clear(); System.out.println(group.getMembers().size()); // 0UserGroup 的内部集合本来应该由它自己管理，但 getMembers() 返回了同一个对象的引用。调用方获得的不是一份数据，而是内部数据的操作权限。这类问题通常有几个表现：某个查询方法调用后，缓存中的数据突然变少；测试之间互相污染，单独运行通过，整套运行失败；一个线程遍历集合时，另一个线程修改集合；调用方为了排序，直接对返回结果调用 sort()，导致内部顺序发生变化。核心并不只是“不要返回可变集合”，而是要明确集合的所有权：谁创建、谁负责修改、谁只能读取。二、泛型为什么不能随意替换很多开发者第一次接触通配符时，会疑惑下面的代码为什么不能编译：List<Integer> integers = new ArrayList<>(); List<Number> numbers = integers; // 编译错误Integer 是 Number 的子类型，但 List<Integer> 不是 List<Number> 的子类型。原因很简单：如果允许这样赋值，就可以通过 numbers 放入 Double，从而破坏原本只允许整数的集合。numbers.add(3.14); // 如果前面的赋值成立，就会破坏 List<Integer>Java 泛型默认是不变的，这种限制保护了类型安全。需要表达“可以读取某种父类型集合”时，应使用上界通配符：public static double sum(List<? extends Number> values) { double result = 0.0; for (Number value : values) { result += value.doubleValue(); } return result; }List<? extends Number> 可以接收 List<Integer>、List<Long> 或 List<BigDecimal>。但它主要用于读取，因为编译器无法确定其中具体是哪一种 Number：public static void readOnly(List<? extends Number> values) { Number first = values.get(0); // values.add(1); // 编译错误 }如果方法的职责是向集合写入某个类型，则应使用下界通配符：public static void addDefaults(List<? super Integer> values) { values.add(0); values.add(1); }List<? super Integer> 可以接收 List<Integer>、List<Number> 或 List<Object>。向其中写入 Integer 是安全的，但读取时…

冰箱里那半棵坏掉的白菜：我开始承认自己吃不了那么多
type: item
date: 2026-08-16T12:00:59+08:00
description: 冰箱里的食物常常不是因为不喜欢才被遗忘，而是我们高估了未来几天的精力，也低估了生活的变化。学会少买一点，是对食物负责，也是对真实生活的承认。又一次从冰箱里拿出坏掉的菜冰箱最下面的抽屉里，常常藏着一些不愿面对的东西。半棵白菜的切口已经发黑，装香菜的塑料袋内壁全是水珠，几根小葱软得弯下去，摸起来没有一点脆生气。它们刚买回来时都很好，叶片完整，颜色新鲜，被整齐地放进冰箱，像是给接下来几天的生活做了充分准备。可真正打开袋子时，我没有立刻想着怎么补救，而是先站在冰箱前回忆：这是什么时候买的？原本打算做什么？答案通常并不复杂。白菜准备炖豆腐，香菜想撒在汤上，小葱可以炒鸡蛋。买下它们的时候，每一道菜都已经在脑子里完成了，甚至连热气和味道都很清楚。只是回家以后，事情没有按照想象发展。某天临时加了点事，某天回家太晚，某天只是单纯地不想开火。等到终于想起那顿饭，食材已经失去了等待的能力。我把坏掉的部分切下来，能留下的尽量留下。实在不能吃的装进垃圾袋。袋子很轻，拿在手里却有一点让人难堪的分量。我买下的其实是理想中的生活以前我总以为，浪费食物只是因为没有规划。只要提前列好清单，安排好菜单，再控制购买数量，问题就能解决。后来才发现，清单并不总是有用。问题往往出现在更前面：我不是按照真实的生活买菜，而是按照理想中的自己买菜。理想中的我会按时下班，回家后还有耐心洗菜、切菜、刷锅；会连续几天保持稳定的胃口；不会突然和朋友吃饭，也不会因为疲惫而只想煮一碗面。站在货架前时，我很容易相信未来几天都将井然有序，于是拿下一整棵白菜、一大盒蘑菇和一把足够吃很多顿的青菜。这种购买带着一点自我安慰。冰箱被装满以后，生活仿佛也充实起来。蔬菜、水果、鸡蛋整齐地摆在那里，让人产生一种正在认真生活的感觉。但拥有食材和真正做饭是两件事。前者只需要付款，后者需要时间、体力和持续的心情。承认这一点并不轻松。它意味着我要承认自己没有想象中自律，也没有那么稳定。有时我愿意认真做一顿饭，有时只想尽快填饱肚子。这不是计划表上的意外，而是生活本来的样子。“多买更划算”未必真的划算买菜时，人很容易被数量说服。同样的东西，大份看起来单价更低；一把小葱比单买几根自然得多；摊主再添一只番茄，也会让人觉得占了便宜。可是单价便宜，不等于最终便宜。一棵白菜吃掉一半，剩下的一半坏掉，那么被丢掉的部分同样付过钱。更不用说清理腐烂菜叶、擦洗抽屉时耗费的时间，以及每次扔东西时那一点隐约的自责。我曾经不太愿意买切开的南瓜或分装的小份蔬菜，总觉得它们不够完整，算下来也不够实惠。现在反而慢慢接受了这种选择。只买一顿能吃完的量，即使单价高一点，总支出可能更少，冰箱也不会长期塞满。这并不是精打细算到每一分钱，而是把“吃完”也算进价格里。食物只有真正进入一顿饭，才完成了它被购买的意义。少买一点以后，冰箱安静了下来后来再去买菜，我开始给清单留出一点余地。不是列出一周七天的完整菜单，而是先考虑最近两三顿最可能做什么。主食和耐放的东西可以多准备一些，新鲜蔬菜则只买眼前能确定吃掉的量。我也不再为了凑出一道完整的菜，给冰箱增加太多零散食材。想吃面时，放一点现有的青菜就够了，不一定非要再买香菜、小葱和几种配菜。做饭不是拍照，也不需要每一碗都摆得齐全。还有一个很小的改变，是把容易坏的食物放到看得见的位置。以前我习惯把冰箱收拾得整整齐齐，新买的东西装进盒子，再一层层码好。结果视觉上很干净，记忆里却很模糊。现在，已经切开的蔬菜会放在最前面，剩下的一小块豆腐也不会被藏进抽屉深处。打开冰箱先看见什么，下一顿就优先处理什么。偶尔还是会有没来得及吃完的东西。生活不可能完全按照保鲜期运行。临时出门、胃口改变、计划推迟，都很正常。区别只是，我不再用下一次的大量采购来弥补这一次的浪费。过去我扔掉坏菜以后，常会产生一种补偿心理：从明天开始一定好好做饭。于是很快又买回一堆新鲜食材，像给自己一次重新开始的机会。结果新的承诺可能再次变成冰箱里的负担。现在我更愿意先空几天。把现有的鸡蛋吃掉，把角落里的面条煮完，让冰箱真正空出位置。日子不需要通过一次采购重新启动，它本来就在继续。接受生活没有那么整齐一棵坏掉的白菜当然算不上什么大事。它不会造成严重损失，也很快会被新的琐事盖过去。但这种小事会反复提醒我，我们经常高估未来的精力，并替尚未到来的自己安排太多任务。买太多菜如此，收藏太多想看的文章、给周末排满计划也是如此。人总愿意相信，未来的自己会比现在更有时间、更有耐心。可未来并不是一个精力充沛的陌生人，仍然是这个会疲惫、会犹豫、偶尔只想休息的自己。所以，少买一点并不是降低生活的标准。恰恰相反，它让我更认真地看待一顿饭真正需要什么，也让我停止用堆满的冰箱证明自己过得不错。现在看到摊位上水灵的蔬菜，我还是会有多拿一些的冲动。但我会先想一想，最近哪一天真的会做，家里还有什么没有吃完。如…

关系走到分岔口时，如何判断该坚持还是体面放手
type: item
date: 2026-08-15T12:00:42+08:00
description: 人与人的关系并不总会以争吵结束，更多时候，它是在一次次沉默、错过和不再主动里慢慢改变。真正重要的不是留住所有人，而是学会分辨值得坚持的关系，也允许自己走向新的生活。有些关系不是突然结束的人与人之间的疏远，很少有一个明确的时刻。更多时候，它发生在一些不起眼的地方：消息从及时回复变成隔很久才回，见面从提前约好变成临时起意，原本什么都能聊的人，后来只剩下几句礼貌性的问候。没有人说过“我们不要再联系了”，可是彼此都知道，那段关系已经不再处在原来的位置。年轻的时候，我们容易把关系的变化理解成背叛，觉得只要足够真诚、足够主动，就应该能够把一个人留在身边。后来才慢慢明白，关系不是一个人努力就能维持的事情。一个人可以发出邀请，可以认真倾听，可以在对方需要时出现，但没有办法替另一个人保持同样的在意。这并不一定是谁做错了。人的生活会变，方向会变，承受能力会变，想要的东西也会变。曾经亲近的人，可能只是陪我们走过了某一段路。当道路分开时，继续用力挽留，有时不是深情，而是在否认变化已经发生。判断一段关系，先看它是否允许真实存在我越来越觉得，一段值得保留的关系，不是永远热闹，也不是时时刻刻保持联系，而是双方都可以在其中做真实的人。你可以有忙不过来的时候，可以暂时不想说话，可以表达不同意见，也可以在状态不好时不必假装轻松。对方不一定每次都理解你，但至少愿意听你解释，不会把一次拒绝、一句重话，立刻上升为对你整个人的否定。相反，有些关系之所以让人疲惫，是因为它要求你持续扮演某种角色。你要永远懂事，永远及时，永远提供情绪价值，永远不能拒绝，也不能让对方失望。一旦你没有做到，关系就开始冷却，甚至让你产生强烈的愧疚感。这样的相处看起来没有冲突，却会让人一点点失去自己。你开始反复斟酌每句话，担心语气不对，担心回复慢了，担心不参加某次聚会就会被排除在外。表面上是在维护关系，实际上是在用自我压缩换取短暂的稳定。真正舒服的关系，不会要求你不断证明自己值得被留下。它有来有往，也有边界；有亲近，也容得下距离。双方都可以表达需要，也都能够接受对方不是随时有空。坚持之前，先确认是不是只有自己在坚持很多人面对关系变淡时，第一反应是加倍努力。多发几条消息，多安排几次见面，多解释自己的想法，甚至不断反省是不是自己哪里做得不够好。适度的主动当然重要。关系需要维护，沉默太久也可能让误会越来越深。但主动和单方面追赶并不是一回事。前者是向对方伸出手，后者是明知道对方没有回应，仍然不停地往前跑。可以观察几个很朴素的信号。第一，你主动靠近时，对方是否会给予回应。回应不一定及时，也不一定方式相同，但应该能让你感受到对方确实在意这段联系，而不是永远把你放在等待的位置。第二，当你遇到困难或需要倾诉时，对方是否愿意分出一点注意力。不是要求对方解决你的问题，而是希望你的难过不会被轻描淡写地推开。第三，关系中是否存在修复的可能。人都会说错话，也都会有情绪。重要的不是从不发生矛盾，而是矛盾之后，双方有没有意愿把话说清楚，愿不愿意调整相处方式。如果这些问题的答案长期都是否定的，那么继续坚持未必能换来更好的结果。你可能只是越来越习惯失望，然后把这种失望解释成自己还不够努力。放手不是否定曾经体面放手最难的地方，在于我们常常误以为，一旦离开，就等于承认过去的感情没有意义。其实不是。那些曾经拥有过的陪伴、信任和快乐，并不会因为后来走散就失效。它们在当时是真实的，也确实帮助我们成为了今天的自己。只是过去真实，不代表现在必须继续；曾经重要，也不代表未来还要用同样的方式相处。有些人适合想念，不适合继续靠近；有些关系适合保留在记忆里，不适合反复拿来验证。我们可以承认自己还会难过，也可以承认自己已经不想回到从前。两件事并不矛盾。放手也不一定要有一场郑重的告别。很多时候，它只是你不再把每个冷淡都理解成自己的责任，不再反复查看对方有没有回复，不再为了恢复旧日亲密而安排一场勉强的见面。你仍然可以祝福对方，只是不再把自己的情绪交给对方的态度决定。慢慢变好，往往从不再委屈自己开始人变得成熟，未必是越来越会处理所有关系，而是逐渐知道哪些关系不需要处理。以前我们可能把“懂事”理解成多承担一些、多退让一些、多替别人考虑一些。后来才发现，真正长久的相处不是一个人不断退到角落，而是双方都愿意为彼此留出位置。你可以善良，但不必随时牺牲；可以重感情，但不必因此失去判断；可以珍惜别人，也要记得自己的感受同样重要。慢慢变好的过程，通常没有明显的掌声。它可能只是下一次被冷落时，你没有立刻否定自己；在一段关系让你持续疲惫时，你终于敢承认它已经不适合；遇到新的朋友时，你不再急着用全部热情换取确定感，而是允许关系自然发展。你会开始把时间留给那些相处起来不必小心翼翼的人，也会把精力放回自己的生活：认真吃饭，完成手头的事情，去见真正想见的人，独自度过一些安静的晚上。生活重…

工作久了，我不再追着学习路线跑：把职业焦虑写成一页问题笔记
type: item
date: 2026-08-14T12:01:00+08:00
description: 收藏完整的学习路线，并不等于真正成长。工作几年后，我开始围绕真实问题做短笔记：记录困惑、补齐原理、验证结论，也慢慢学会不再用学习时长衡量自己。收藏夹里的上进心做程序员以后，很容易形成一种习惯：看到一篇讲架构的文章，先收藏；遇到一套源码课程，先加入列表；听说某个方向正在流行，便顺手找来一张学习路线图。那些路线通常画得很完整，从语言基础到虚拟机，从数据库到分布式系统，每个方框都像一个尚未完成的任务。刚保存下来时，人会短暂地安心，仿佛只要沿着箭头走到底，职业上的不确定就能减少一些。真正开始执行时，事情却没有那么顺利。白天处理需求、沟通进度、排查故障，注意力已经被切成很多小块。等终于能坐下来学习，面对几十个章节，往往不知道应该从哪里继续。勉强看上一段，也可能因为暂时用不到，很快忘记。我后来才明白，许多学习计划失败，并不完全是因为懒惰，而是因为计划太像一份别人的地图。它看起来面面俱到，却没有回答一个更具体的问题：我现在究竟被什么难住了？焦虑喜欢宏大的目标，成长依赖具体的问题职业焦虑出现时，人很容易给自己安排宏大的任务：系统学习并发、彻底掌握数据库、完整阅读某个框架源码。这些目标没有错，但它们边界模糊，也很难获得明确的完成感。真实的工作问题通常小得多。为什么一个请求偶尔超时？为什么同一段查询换了条件以后突然变慢？为什么线程池中的日志少了一段上下文？为什么自己能把功能做出来，却说不清方案之间的取舍？具体问题会提供学习所需的三个条件：背景、反馈和边界。背景让知识不再悬空，反馈可以检验理解是否正确，边界则提醒我们何时应该停下来。相比之下，漫无目的地追赶知识清单，很容易把学习变成一种自我审查。一天没有看书，便觉得自己退步；一个概念没听过，便担心已经落后。久而久之，学习不再是为了理解世界，而是为了证明自己仍然配得上这份工作。这种感觉并不激烈。它更像浏览技术文章时忽然出现的一点心虚，像看到别人分享成长经历后下意识地比较，也像周末打开课程，却先在目录里来回翻动。表面上是在选择内容，心里真正寻找的，可能是一种确定感。我开始只写一页问题笔记现在遇到值得追下去的问题，我会先留下一页很短的笔记，而不是立刻建立一套庞大的学习计划。一页并不是严格的篇幅限制，它只是提醒我：先把眼前的问题说清楚，不要急着把整个领域都装进来。这页笔记通常包含四部分。1. 发生了什么只记录可以观察到的现象，不急着解释原因。例如某个操作比平时慢、某项配置没有生效，或者一次沟通中自己无法说明设计依据。这一部分越具体越好。模糊地写“数据库能力不足”，只会制造压力；写成“无法解释联合索引在当前查询中的使用顺序”，才有继续行动的可能。2. 我原来怎么理解把未经验证的判断也写下来。很多时候，真正妨碍学习的不是不知道，而是把一个不完整的印象当成了结论。写下原有理解会让人有一点不自在，因为它暴露了思考中的空白。但这种不自在是有价值的。它比假装听懂更接近成长，也比继续收藏十篇文章更节省时间。3. 我做了什么验证验证可以是阅读官方文档、构造一个最小案例、查看运行信息，也可以是向同事确认业务约束。生活中的问题同样适用：如果总觉得自己没有时间，就先观察时间去了哪里，而不是立刻制定精确到半小时的日程。关键不在于使用多复杂的方法，而在于区分事实和猜测。有些困惑经过一次验证就能结束；有些问题则会暴露更深的一层。两种结果都比停留在模糊焦虑里更好。4. 下次如何识别最后写一句可以带走的话：下次遇到什么信号时，应该想到这次经验。知识真正变成经验，往往不是因为记住了完整定义，而是因为在相似场景出现时，能够认出它。问题笔记的价值也不在于积累数量，而在于让过去的思考有机会回到未来的工作中。不把每个问题都变成研究课题问题驱动的学习也有陷阱。最常见的一种，是顺着概念不断向下挖，最后忘了最初为什么出发。查一个配置项，可能一路读到框架实现；研究一次慢查询，也可能很快扩展成整套数据库原理。深入本身没有问题，但精力有成本。我会用三个问题决定是否继续：当前问题是否已经能够解释和处理？更深的知识近期是否会重复使用？继续投入，是出于好奇，还是出于害怕自己知道得不够多？如果问题已经解决，剩余内容暂时用不到，就允许笔记停在这里。承认“目前理解到这一层已经够用”，不是敷衍，而是一种边界感。另一个陷阱是过度整理。换字体、调目录、设计标签，看起来很认真，却可能替代真正的思考。笔记不必像教程，也不必保证别人一看就懂。它首先是留给未来的自己，能够还原当时的问题和判断就足够了。还有一种更隐蔽的误区：只记录成功结论，不记录走过的弯路。可真正有用的部分，常常正是“我为什么会判断错”。技术会更新，工具会变化，但误判背后的习惯可能长期存在，比如凭印象下结论、忽略前提、把偶然现象当成普遍规律。把这些写下来，比抄录一个标准答案更诚实。学习之外，也要允许生活没有产出当学习从宏…

下班后的两个小时为什么总在消失：停止把夜晚当成第二个工作日
type: item
date: 2026-08-13T12:01:02+08:00
description: 下班后的疲惫常被误解为不够自律，于是阅读、锻炼和休息都变成新的任务。本文从注意力切换与情绪恢复出发，记录一种更温和、更可执行的夜晚安排方式。那些没有真正开始的夜晚下班后最常见的场景，不是认真读完一本书，也不是痛快地看完一部电影，而是先坐一会儿。手机拿在手里，消息回了几条，短视频划过几个，顺便看了购物软件和新闻。等意识重新回到房间，时间已经过去很久。原本想做的事情一件也没开始，人却比刚到家时更加疲惫。这种夜晚容易带来一种隐约的自责：明明拥有可以自由支配的时间，为什么还是没有阅读、锻炼、学习，甚至连好好休息都没有做到？于是第二天开始制定更严格的计划，把下班后的每半小时都安排好。结果通常是，计划写得很完整，执行时只完成了开头，挫败感反而更重。问题未必出在懒惰，也不完全是意志力不足。更可能的原因是，我们把下班后的夜晚误认为一段完好、连续、精力充足的时间，却忽略了人从工作状态退出来，本身就需要过程。下班不等于工作状态已经结束离开工位只是空间发生了变化，注意力和情绪并不会同步下班。白天没有处理完的事项、等待回复的消息、一次不太顺利的沟通，都可能继续停留在脑子里。回到家后，人看似坐着不动，内心却还在反复检查：是不是遗漏了什么，明天要先做哪件事，那句话有没有说得不合适。在这种状态下，阅读并不只是“翻开书”这么简单。它要求注意力从零散信息转向连续文字；锻炼也不只是“换上衣服”，而是要从静止和疲惫中重新调动身体。每件事都包含一次切换，而切换需要额外消耗。手机恰好绕过了这个门槛。它不要求准备，不需要连续投入，也不会提醒我们刚才中断了多少次。每一条内容都很轻，但它们不断制造新的刺激，让大脑停留在既没有专注、也没有休息的中间地带。因此，下班后最先需要解决的，不是“今晚做什么”，而是“怎样结束白天”。给夜晚增加一个缓冲区比起到家后立刻执行计划，我更认可先留出一段没有产出要求的缓冲时间。它不是奖励，也不是浪费，而是从工作切换到生活的必要步骤。缓冲区可以很普通：换掉白天的衣服，洗脸或洗澡，简单收拾桌面，吃东西时不同时处理消息，去楼下慢慢走一圈。关键不在于选择哪一种活动，而在于让身体接收到明确的信号：今天的工作暂时结束了。这段时间最好有边界。完全没有边界的“休息一下”，很容易变成漫无目的地使用手机；边界太紧，又会让休息变成计时任务。可以用一顿晚饭、一段散步或一次洗澡作为自然分隔，而不必频繁盯着分钟数。我也会把仍然挂在心里的事情简单写下来，例如：明天上午先确认某个进度；有一封邮件需要补充信息；今天那次沟通让我不舒服，但今晚不继续分析；一个暂时没有答案的问题，留到工作时间再处理。写下来的作用不是安排得更精细，而是告诉自己：这件事没有被遗忘，但现在不必继续握在手里。一晚只保留一个重点许多夜晚计划失败，是因为我们同时想要太多：读书、运动、学语言、整理房间、回复朋友、早点睡。单独看，每件事都合理；放在一起，它们就组成了另一份晚班。更现实的做法，是每晚只保留一个重点。这个重点可以是读二十页书，也可以是做一顿饭、跑步、看电影，或者认真和家人说一会儿话。完成它以后，其他事情都算附加项。“一晚一件事”并不高效，却更接近真实生活。它承认精力有上限，也承认有些夜晚就是比另一些夜晚沉重。阅读尤其适合降低启动标准。不要要求自己必须读完一章、记下完整笔记。把书放在容易拿到的地方，读几页也可以停。如果读了十分钟仍然无法进入状态，就承认今天不适合读，而不是一边看字一边责备自己。真正能持续的阅读，往往不是靠一次投入很多时间，而是减少每次开始之前的心理负担。书不必承担“让我变得更好”的全部责任，它也可以只是让一个普通夜晚安静一点。区分主动休息和被动消耗不是所有看手机、看剧、发呆都应当被否定。休息没有统一的正确形式，问题在于，一件事结束以后，我们究竟更松弛了，还是更空虚、更烦躁。主动休息通常带有一点选择感。我知道自己想看哪部电影，决定看一集；我想了解一件事，搜索后读完相关内容；我想和朋友聊天，于是认真聊一会儿。即使这些活动没有产出，结束时也比较完整。被动消耗则缺少结束点。打开手机时没有明确目的，每一次刷新都在等待下一条刺激。手指一直在动，情绪却没有落脚处。等到困意出现，只能仓促洗漱，然后带着“今晚又没过好”的感觉上床。判断两者并不需要复杂工具。只要在开始前问一句：“我现在具体想做什么？”如果说不出来，可以先把手机放远一点，倒杯水，站到窗边，或者什么也不做地坐几分钟。无聊并不可怕，它常常是注意力恢复连续性的起点。几个容易掉进去的误区用更满的计划对抗失控时间失控后，人很容易通过制定计划重新获得安全感。但计划越满，对状态的要求越高，一旦第一项延误，后面便全部失效。夜晚更适合使用宽松的顺序，而不是精确到分钟的日程。把娱乐当成需要偿还的债看剧、玩游戏或躺着并不天然等于堕落。如果娱乐之后总有负罪感，人就既没有…

异步任务里的 traceId 为什么消失：用 MDC 打通 Java 线程池日志上下文
type: item
date: 2026-08-12T12:01:35+08:00
description: 生产日志按 traceId 检索时，异步任务常会突然断链。本文从 MDC 的线程绑定原理出发，给出 Servlet 入口、线程池上下文复制、异常记录与监控字段设计的完整实践。问题背景：一次请求，日志却被分成了两段生产环境中，一个接口往往会经过参数校验、数据库访问、远程调用和异步任务。为了把这些日志串起来，常见做法是在请求入口生成 traceId，写入 SLF4J 的 MDC，再让日志格式自动输出它。同步代码通常没有问题：[traceId=7f3a...] 开始创建订单 [traceId=7f3a...] 订单写入成功但代码一旦提交到线程池，日志可能变成：[traceId=] 开始发送通知这不是日志框架偶尔失效，而是 MDC 的使用边界没有覆盖线程切换。故障发生后，工程师能看到主流程成功，也能看到后台任务报错，却无法确认两者是否属于同一次请求。更麻烦的是，线程池线程会被反复复用；如果上下文只设置不清理，还可能把上一个请求的 traceId 带到下一个任务中，形成误导。核心原理：MDC 默认不会跨线程传递MDC（Mapped Diagnostic Context）可以理解为与当前线程关联的一组键值。业务代码调用：MDC.put("traceId", traceId);日志格式中的 %X{traceId} 就能读取该值。它适合保存 traceId、租户标识、调用来源等日志上下文，但不应承载业务状态。关键在于“当前线程”。Servlet 线程把任务交给线程池后，执行任务的是另一个线程，它看不到提交线程中的 MDC。即使某些底层实现涉及可继承线程本地变量，也不能依赖它解决线程池传播：池中线程通常早已创建，而且会持续复用。可靠的处理过程应当是：提交任务时复制调用线程的 MDC；执行任务前，把副本设置到工作线程；执行结束后，恢复工作线程原有上下文或清空；无论任务成功还是抛异常，清理动作都必须发生。在 HTTP 入口建立 traceId下面示例适用于使用 Jakarta Servlet API 的 Spring Boot 3.x 应用。若项目仍使用 Spring Boot 2.x，需要把 jakarta.servlet 包名换成 javax.servlet。package com.example.logging; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.MDC; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.UUID; import java.util.regex.Pattern; @Component public class TraceIdFilter extends OncePerRequestFilter { public static final String TRACE_ID = "traceId"; private static final Pattern SAFE_TRACE_ID = Pattern.compile("[A-Za-z0-9._-]{1,64}"); @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String traceId = resolveTraceId(request.getHeader("X-Trace-Id")); MDC.put(TRACE_ID, traceId); response.setHeader("X-Trace-Id", traceId); try { filterChain.doFilter(request, response); } finally { MDC.remove(TRACE_ID); } } private String resolveTraceId(String candidate) { if (c…

用可组合规则对象重构订单提交校验：告别散落的 if 与隐式顺序
type: item
date: 2026-08-11T12:01:04+08:00
description: 以订单提交校验为例，使用可组合规则对象拆分复杂条件，讨论快速失败、错误聚合、执行顺序、外部查询隔离及测试策略。问题背景订单提交、退款申请、优惠券领取这类业务，通常都存在一组不断增长的前置条件。最初只有两三个判断时，直接写 if 很自然：public void submit(Order order, Customer customer) { if (order.getStatus() != OrderStatus.CREATED) { throw new IllegalStateException("订单状态不允许提交"); } if (order.getAmount().compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("订单金额必须大于零"); } if (customer.isBlocked()) { throw new IllegalStateException("客户已被限制交易"); } if (!stockService.hasEnoughStock(order)) { throw new IllegalStateException("库存不足"); } order.submit(); }真正的问题并不是 if 本身，而是规则会持续变化：不同入口需要不同组合，页面希望一次展示全部错误，后台任务却只关心能否执行；部分判断依赖数据库，部分判断只依赖订单字段。继续把逻辑堆在一个方法里，会逐渐出现重复代码、隐式执行顺序和难以覆盖的分支。这时可以借鉴 Specification 思想，将每条业务规则表示为对象，并通过组合形成完整校验。重点不是消灭条件判断，而是给规则明确的边界和名字。设计一个可解释的规则接口只返回 boolean 虽然简单，却无法告诉调用方失败原因。下面定义一个适用于 Java 8 及以上版本的规则接口：@FunctionalInterface public interface Rule<T> { CheckResult check(T target); default Rule<T> and(Rule<T> next) { return target -> { CheckResult current = check(target); if (!current.isPassed()) { return current; } return next.check(target); }; } }and 采用短路语义：前一条规则失败后，不再执行下一条。这适合后台任务和命令处理，因为可以减少无意义的计算，也能避免后续规则建立在错误前提上。校验结果同时保存状态和错误信息：public final class CheckResult { private final boolean passed; private final List<String> errors; private CheckResult(boolean passed, List<String> errors) { this.passed = passed; this.errors = Collections.unmodifiableList( new ArrayList<>(errors)); } public static CheckResult pass() { return new CheckResult(true, Collections.emptyList()); } public static CheckResult fail(String error) { return new CheckResult(false, Collections.singletonList(error)); } public static CheckResult fail(List<String> errors) { return new CheckResult(false, errors); } public boolean isPassed() { return passed; } public List<String> getErrors() { return errors; } }这里让结果保持不可变，避免多个规则共享并修改同一个错误列表。规则对象如果注册为 Spring 单例，也不会因此产生并发问题。先准备上下文，再执行规则规则经常需要订单之外的信息，例如客户是否被限制、库存是否充足。一个常见错误是让每条规则自行查询数据库，最终一次校验触发多次重复查询，而且调用方无法看出成本。更稳妥的做法是由应用服务统一准备上下文：public final class ReviewC…

Java 微服务调用的可靠性边界：超时、重试与幂等如何一起设计
type: item
date: 2026-08-10T12:01:09+08:00
description: 一次 HTTP 调用失败，并不等于对方没有执行。本文从 Java HttpClient 出发，梳理连接超时、请求超时、重试边界和幂等键的关系，并给出一个可落地的微服务调用实现。问题背景：HTTP 失败不代表业务没有发生在微服务系统中，一个服务调用另一个服务，表面上只是发送一次 HTTP 请求，实际却跨越了 DNS、TCP 连接、连接池、负载均衡、网关、服务线程池、数据库等多个环节。任何一个环节变慢，都可能让调用方看到超时或连接异常。真正棘手的地方在于：调用方认为失败，不代表被调用方一定没有执行。例如，订单服务调用支付服务创建支付单：支付服务已经写入数据库；响应已经生成，但还没有返回到订单服务；中间网络断开，订单服务收到 SocketTimeoutException；订单服务进行重试；支付服务再次创建一笔支付单。如果没有幂等控制，重试就可能把一次业务请求变成两次扣款、两张订单或两条重复消息。因此，微服务 HTTP 调用的可靠性不能只靠“把超时时间调大”，而要同时考虑超时、重试、幂等和服务端状态。一、先区分两类超时1. 连接超时连接超时表示在规定时间内没有建立 TCP 连接。常见原因包括目标服务不可用、网络拥塞、连接地址错误或连接池无法及时提供连接。连接超时通常可以快速失败，因为此时请求大概率还没有到达对方服务。2. 请求超时请求超时表示请求已经发出，但在规定时间内没有完成响应。此时请求可能已经被对方接收，甚至已经完成了数据库写入，只是响应没有顺利返回。这也是最容易误判的地方：请求超时属于“结果未知”，而不是简单的“请求失败”。对于查询类接口，可以谨慎重试；对于创建、扣款、发货等写操作，必须先确认接口是否支持幂等。在 Java 代码中，不建议无限等待，也不建议所有接口共用一个很大的超时。超时时间应该根据接口的业务目标设置，并且小于上游调用方能够容忍的总时间。二、一个带边界的 HTTP 调用实现下面使用 Java 11 提供的 java.net.http.HttpClient 演示一个同步调用器。它不依赖具体 Web 框架，适合用来说明核心逻辑。示例针对“创建支付单”接口，假设服务端支持通过 Idempotency-Key 保证同一个业务请求只执行一次。import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.util.Set; import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; public final class PaymentClient { private static final Set<Integer> RETRYABLE_STATUS = Set.of(408, 429, 502, 503, 504); private final HttpClient httpClient; private final URI endpoint; private final int maxAttempts; public PaymentClient(String endpoint, int maxAttempts) { if (maxAttempts < 1) { throw new IllegalArgumentException("maxAttempts must be positive"); } this.httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofMillis(300)) .version(HttpClient.Version.HTTP_1_1) .build(); this.endpoint = URI.create(endpoint); this.maxAttempts = maxAttempts; } public PaymentResult createPayment(String orderId, long amount) { String idempotencyKey = UUID.randomUUID().toString(); String requestBody = "{\"orderId\":\"" + orderId + "\",\"amount\":" + amount + "}"; for (int attempt …

31岁，关于爱情、喜欢和自己
type: item
date: 2026-08-09T23:56:52+08:00
description: 31岁以后，喜欢不再只是心动和热烈，也是在疲惫的生活里彼此理解、保留边界，并把自己的人生过得完整。31岁，关于爱情、喜欢和自己前几天和朋友聊天，他问我：“你现在还会很喜欢一个人吗？”我愣了一下，没有马上回答。以前的我大概会觉得，喜欢就是喜欢啊，哪有那么多为什么。看见一个人会开心，收不到消息会胡思乱想，明明很忙，却还是会抽空去见面；哪怕只是一起吃个饭、走一段路，也会觉得这一天比平时亮一点。后来才发现，随着年龄一点点往上走，喜欢这件事好像没有消失，只是变得没有以前那么张扬了。31岁的人，已经不太会因为一句“在干嘛”高兴一整天，也不太会因为一条没有及时回复的消息就彻夜睡不着。不是变得冷淡了，更不是不相信爱情了，只是开始明白，生活里除了爱情，还有工作、父母、身体、房租、未来，还有那些说不出口但总会压在心里的焦虑。喜欢一个人，原来并不只是心动那么简单。它还意味着，你是否愿意把有限的时间分给对方；意味着你在疲惫不堪的一天结束后，依然愿意耐心听对方说几句没什么大不了的小事；也意味着，在生活并不总是顺利的时候，你们还能不能站在同一边。年轻时总觉得爱情一定要轰轰烈烈。要有很多聊天记录，要有频繁的见面，要有节日礼物和朋友圈里的公开，要有说走就走的旅行，要有“没有你不行”的笃定。那时候好像只要足够喜欢，就可以解决一切问题。两个人之间有一点矛盾，第一反应总是争个对错；稍微有一点距离，就觉得是不是不爱了；对方没有完全按照自己的想法来，心里就会委屈很久。后来才知道，爱情里最难的，从来不是“我喜欢你”。而是“我喜欢你，但我也知道你是一个和我完全不同的人。”你有你的脾气、你的习惯、你的过去，我也有我的不安、我的敏感和不愿意轻易示人的软弱。两个人靠近以后，看到的未必都是浪漫，更多时候，是彼此那些不那么完美的部分。有的人喜欢热闹，有的人更习惯安静；有的人表达爱意很直接，有的人却总把关心藏在行动里；有的人觉得陪伴就是每天都要黏在一起，有的人则觉得各自忙完以后还能想着对方，就已经很珍贵。以前总觉得，不一样就是不合适。现在慢慢觉得，不一样并不可怕。真正可怕的是，一个人只想让对方变成自己期待的样子，却从来没有认真理解过对方原本的样子。爱情不是改造，也不是拯救。谁都不是谁人生里的救世主。我们都只是普通人，会情绪不好，会犯错，会有顾及不到对方的时候。好的关系大概不是要求对方永远完美，而是在对方不那么完美的时候，依然愿意多一点理解，多一点耐心。当然，理解也不是无限度的忍让。以前写过，脾气好不代表好欺负，温和也应该有底线。放在爱情里也是一样。喜欢一个人，不是把自己弄丢；爱一个人，也不是把所有委屈都咽下去。真正舒服的感情，应该是两个人都能做自己。你可以偶尔脆弱，可以不那么坚强，可以坦然说“我今天很累”“我有点难过”“我希望你陪陪我”；而不是每次情绪上来的时候，都要先担心自己会不会显得麻烦，会不会被嫌弃，会不会因为说得太多而失去对方。人到了31岁，开始越来越珍惜这种不需要逞强的关系。不需要猜，不需要试探，不需要反复确认自己在不在对方心里。很多话可以直接说，很多误会也愿意及时解释。你知道对方不会因为你偶尔的沉默就随便离开，对方也知道你不是不爱，只是有时候不知道该怎么表达。这大概比那些轰轰烈烈的承诺更难得。31岁也让我慢慢接受了一件事：不是所有喜欢，都一定要有结果。有些人出现，就是让你明白，原来你也可以那么在乎一个人；有些遗憾，是提醒你以后遇到真正合适的人时，要更勇敢一点；有些告别虽然当时觉得很难，但过去很久以后回头看，才发现彼此没有走到最后，也许并不是谁不够好，只是那段路只能陪到那里。人生本来就不是所有愿望都会实现。就像小时候以为长大以后会拥有很多东西，后来才发现，得到与失去常常是一起发生的。你拥有过一些人，也弄丢过一些人；你有过很炽热的喜欢，也有过很长时间的沉默；你以为自己已经足够成熟了，却还是会在某一个深夜里，因为一句话、一首歌、一个熟悉的场景，而想起曾经那个很认真爱过的人。这也没什么不好。说明我们不是毫无波澜地活着，说明有些经历确实在生命里留下过痕迹。只是现在的我不太愿意把爱情看得太沉重了。它当然重要，但不是人生唯一的答案。比起期待有一个人来填满自己所有的空缺，我更希望先把自己的生活过得完整一点。好好工作，好好吃饭，好好睡觉；周末有空去散步、拍照、见朋友，偶尔回家看看父母，看看天上的云和晚上的月亮。一个人也可以把日子过得热气腾腾。不是因为不期待爱情，而是因为我希望将来遇到的那个人，不是来拯救我、填补我，而是和我一起分享这份已经不算太差的生活。我想，31岁以后的喜欢，应该是这样的：不是非你不可的执念，而是“有你很好”；不是失去自我的讨好，而是彼此都愿意靠近；不是永远没有争吵，而是争吵以后还愿意好好说话；不是只在开心的时候想起对方，而是在那些平淡、疲惫、甚至有些难熬的日子里，依然愿…

库存扣减为什么会被覆盖：Spring Data JPA 与 MySQL 的并发控制实践
type: item
date: 2026-08-09T12:01:02+08:00
description: 一次普通的库存扣减，在并发请求下可能悄悄变成覆盖更新。本文从 MySQL 事务隔离与 JPA 更新机制入手，比较乐观锁和原子 SQL 两种方案，并说明重试、批量更新及持久化上下文中的常见陷阱。问题背景库存扣减是数据库开发中很典型的一类操作。单看业务代码，它通常只有三步：查询商品、判断库存、扣减库存。@Transactional public void deduct(Long productId, int amount) { Product product = productRepository.findById(productId) .orElseThrow(() -> new IllegalArgumentException("商品不存在")); if (product.getStock() < amount) { throw new IllegalStateException("库存不足"); } product.setStock(product.getStock() - amount); }在单线程测试中，这段代码没有问题。但假设库存为 10，两个事务同时购买 1 件：事务 A 查询到库存 10；事务 B 也查询到库存 10；A 将库存更新为 9；B 根据之前读到的值，也将库存更新为 9。两次购买成功，数据库却只减少了 1 件。这就是典型的丢失更新。很多人认为给方法加上 @Transactional，或者使用 MySQL 默认的可重复读隔离级别，就能避免这个问题。实际上，事务保证的是一组操作的原子性，并不自动理解“库存不能被旧值覆盖”这样的业务约束。为什么事务没有阻止覆盖JPA 修改实体时，最终可能生成类似下面的 SQL：update product set stock = 9 where id = 1;InnoDB 执行 UPDATE 时会获取排他锁，因此两个更新不会在同一瞬间修改同一行。后到的事务会等待先到的事务释放锁，但等待结束后，它仍然可以执行 stock = 9。问题不在于写操作没有加锁，而在于 SQL 携带的是应用内存中计算出的旧结果，并且 WHERE 条件没有验证这条记录在读取之后是否已被别人修改。普通的快照读也不会因为处于可重复读事务中，就自动转换为悲观锁定读。隔离级别、行锁和业务并发控制是三个需要分别理解的概念。方案一：使用 JPA 乐观锁对于读取后还要修改多个字段、并且并发冲突不算频繁的业务，可以使用 JPA 的 @Version。import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Version; @Entity public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private int stock; @Version private long version; protected Product() { } public Product(String name, int stock) { this.name = name; this.stock = stock; } public void deduct(int amount) { if (amount <= 0) { throw new IllegalArgumentException("扣减数量必须大于 0"); } if (stock < amount) { throw new IllegalStateException("库存不足"); } stock -= amount; } public Long getId() { return id; } public int getStock() { return stock; } }在 Spring Boot 3 和 Jakarta Persistence 3 环境中，注解包名是 jakarta.persistence；较早的 Spring Boot 2 项目通常使用 javax.persistence。Repository 不需要编写特殊更新语句：import org.springframework.data.jpa.repository.JpaRepository; public interface Produc…