<?xml version="1.0" encoding="utf-8" ?><rss version="2.0"><channel><title><![CDATA[Leighteen]]></title><description><![CDATA[Java后端开发技术博客]]></description><link>https://blog.youkuaiyun.com/Treeers</link><language>zh-cn</language><generator>https://blog.youkuaiyun.com/</generator><copyright><![CDATA[Copyright &copy; Treeers]]></copyright><item><title><![CDATA[ThreadLocal 内存泄漏：`Entry` 的 key 都用弱引用了，为什么还会泄漏]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/163508770</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/163508770</guid><author>Treeers</author><pubDate>Wed, 05 Aug 2026 16:46:04 +0800</pubDate><description><![CDATA[`ThreadLocal` 是个好东西：给每个线程一份独立的变量副本，天然线程隔离，常用来存用户上下文、事务、`SimpleDateFormat` 这类"每线程一份"的东西。

但它有个著名的坑：**用不好会内存泄漏。** 更让人困惑的是，很多人知道"`ThreadLocalMap` 的 `Entry` 用了弱引用来防泄漏"，于是产生一个疑问——**既然都用弱引用了，为什么还会泄漏？**]]></description><category></category></item><item><title><![CDATA[时区的坑：为什么存进数据库的时间，差了 8 小时]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/163508686</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/163508686</guid><author>Treeers</author><pubDate>Wed, 05 Aug 2026 16:43:05 +0800</pubDate><description><![CDATA[几乎每个后端都踩过这个坑：代码里 `new Date()` 明明是当前时间，存进数据库一看，**时间差了整整 8 小时**。或者反过来，从库里查出来的时间，展示到页面上又莫名早了 8 小时。
`8` 这个数字很有辨识度——它就是**东八区（北京时间）与 UTC 的时差**。只要看到时间偏差是 8 小时（或它的整数倍），基本可以断定：**某个环节的时区没对齐。**]]></description><category></category></item><item><title><![CDATA[`try-finally` 里的 `return`：为什么 `finally` 会悄悄改掉返回值、吞掉异常]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/163334848</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/163334848</guid><author>Treeers</author><pubDate>Thu, 30 Jul 2026 14:05:37 +0800</pubDate><description><![CDATA[- `try` 里的 `return` 会**先把返回值暂存**，再执行 `finally`，最后返回**暂存的值**——所以 `finally` 改局部变量，改不动已经暂存的返回值。
- `finally` 里如果有 `return` 或 `throw`，会**截胡**：覆盖 `try` 的返回值、并**吞掉 `try` 中正在传播的异常**——这是异常凭空消失的元凶。
- 正解：**`finally` 只做清理，不写 `return`、不抛异常、不改返回变量**；关资源优先用 `try-with-res]]></description><category></category></item><item><title><![CDATA[DCL 单例为何要 `volatile`：一次半初始化对象引发的血案]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/163331299</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/163331299</guid><author>Treeers</author><pubDate>Thu, 30 Jul 2026 12:00:10 +0800</pubDate><description><![CDATA[双重检查锁定（DCL）是经典的单例实现方式，但若省略 volatile 修饰符，可能在高并发下返回未构造完的“半初始化对象”。问题的根源在于 new Singleton() 并非原子操作，实际分为分配内存、初始化对象、引用赋值三步。指令重排可能导致引用赋值先于对象初始化，此时其他线程绕过锁检查直接读取到未初始化的实例。]]></description><category></category></item><item><title><![CDATA[`==`、`equals` 和 Integer 缓存：为什么 `128 == 128` 是 false]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/163244291</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/163244291</guid><author>Treeers</author><pubDate>Mon, 27 Jul 2026 15:12:07 +0800</pubDate><description><![CDATA[文章摘要：Java中Integer类型比较的反直觉现象源于==与equals的差异及IntegerCache机制。==比较对象地址，equals比较值。Integer.valueOf()对-128~127范围内的数值返回缓存对象，导致127 == 127为true，而128 == 128因超出范围返回新对象为false。正确做法是包装类比较一律用equals，避免数值大小影响结果。同类陷阱存在于Long等包装类中，基本类型比较不受影响。]]></description><category></category></item><item><title><![CDATA[`volatile` 到底保证了什么：可见性、有序性，和它管不到的原子性]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/163244057</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/163244057</guid><author>Treeers</author><pubDate>Mon, 27 Jul 2026 15:04:58 +0800</pubDate><description><![CDATA[有个 bug 特别典型：一个线程把开关 `flag` 改成了 `true`，另一个线程里 `while (!flag)` 的循环却像有没听见一样，一直转个不停，就是不退出。

把 `flag` 加上 `volatile`，问题立刻消失。于是很多人得出一个模糊的结论："`volatile` 就是个轻量级的 `synchronized`，加上就线程安全了。"
这个理解害人不浅。因为紧接着他们会写出 `volatile int count; count++;` 这种代码，然后在多线程下发现 `count` 的最终]]></description><category></category></item><item><title><![CDATA[ORDER BY + LIMIT 的坑：为什么加了 `LIMIT` 结果顺序还乱]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/163101453</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/163101453</guid><author>Treeers</author><pubDate>Sat, 25 Jul 2026 22:32:07 +0800</pubDate><description><![CDATA[有个 bug 特别诡异：一条 `ORDER BY ... LIMIT` 的分页 SQL，测试环境好好的，上线后偶尔出现"第 2 页的数据，第 1 页也出现过"或者"翻页时有数据莫名跳过"。
更让人抓狂的是，把 `LIMIT` 去掉，`ORDER BY` 的结果看着又是对的。于是很多人得出结论："是 `LIMIT` 把顺序搞乱了。"
这个锅 `LIMIT` 不背。真正的原因是：**你的排序字段有重复值，而重复值之间的顺序，MySQL 从不保证。** `LIMIT` 只是把这个一直存在的隐患**暴露**了出]]></description><category></category></item><item><title><![CDATA[索引失效大全：记住这一条主线，推导出所有失效场景]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/163135182</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/163135182</guid><author>Treeers</author><pubDate>Sat, 25 Jul 2026 22:30:11 +0800</pubDate><description><![CDATA["我明明建了索引，`EXPLAIN` 一看却是全表扫描，索引怎么就失效了？"
这大概是 MySQL 里最让人头疼的问题之一。上网一搜，清一色是"索引失效的 15 种场景"——函数、隐式转换、最左前缀、`!=`、`LIKE '%x'`……列一大串，背完就忘，遇到新写法又懵了。
这篇文章想换个讲法：**这些场景根本不用背。** 所有索引失效，追到底只有两条主线。把这两条主线理解透，你不但能解释已知的十几种场景，还能自己推导出没见过的新情况。]]></description><category></category></item><item><title><![CDATA[MySQL 深分页优化：`LIMIT 1000000, 20` 为什么越翻越慢]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/163079211</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/163079211</guid><author>Treeers</author><pubDate>Fri, 24 Jul 2026 10:03:57 +0800</pubDate><description><![CDATA[做列表接口，几乎都逃不过分页。第 1 页永远飞快，可一旦用户翻到第几万页，接口就卡成 PPT，甚至直接超时。

奇怪的是，每一页明明都只要 20 条数据，为什么翻得越深越慢？
答案藏在 `LIMIT m, n` 的执行方式里。这篇文章我们把深分页慢的根因讲透，再给出三种从"通用"到"最快"的优化方案，每一种都配上真实的 `EXPLAIN` 和耗时日志。]]></description><category></category></item><item><title><![CDATA[DISTINCT 和 GROUP BY 去重，哪个好？]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/163101302</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/163101302</guid><author>Treeers</author><pubDate>Fri, 24 Jul 2026 09:54:16 +0800</pubDate><description><![CDATA["查去重数据，该用 `DISTINCT` 还是 `GROUP BY`？"
这是个高频疑问，网上的说法照例是两派：有人说 `GROUP BY` 快，因为"它能用索引分组"；有人说 `DISTINCT` 快，因为"它专门为去重设计"。还有人凭感觉——`GROUP BY` 看着更"重"，所以肯定更慢。
这些多半是想当然。这篇文章用真实的执行计划给个明确答案：**在单纯去重这件事上，现代 MySQL 里两者性能几乎没有差别**，因为优化器最后把它们变成了同一套执行方案。真正该纠结的不是"谁快"，而是"什么时候用]]></description><category></category></item><item><title><![CDATA[为什么金额不能用 `float` 存：一次精度丢失引发的对账事故]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/163082715</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/163082715</guid><author>Treeers</author><pubDate>Thu, 23 Jul 2026 09:55:23 +0800</pubDate><description><![CDATA["存个金额而已，用 `float` 不就行了？"
这大概是新手最容易踩的坑之一。程序跑起来没报错，测试也过了，直到某天财务对账，发现总额差了几分钱——查了半天，罪魁祸首就是那个 `float`。

《阿里巴巴 Java 开发手册》对此有一条毫不含糊的**【强制】**规定：

> 【强制】小数类型为 `decimal`，禁止使用 `float` 和 `double`。
> 说明：`float` 和 `double` 在存储的时候，存在精度损失的问题，很可能在值的比较时，得到不正确的结果。如果存储的数据范围超]]></description><category></category></item><item><title><![CDATA[为什么 `!=` 和 `NOT IN` 会让索引失效：从 B+ 树的有序性说起]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/163101358</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/163101358</guid><author>Treeers</author><pubDate>Thu, 23 Jul 2026 09:54:59 +0800</pubDate><description><![CDATA["这条 SQL 明明在索引列上查，怎么还是全表扫描？"
如果你把 `WHERE status = 1` 改成 `WHERE status != 1`，很可能就会遇到这个现象：同一个列、同一个索引，等值查询走得好好的，一换成 `!=`（或 `NOT IN`、`<>`）索引就"失效"了。]]></description><category></category></item><item><title><![CDATA[为什么不要用 `SELECT *`：一条军规背后的底层原理]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/163082655</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/163082655</guid><author>Treeers</author><pubDate>Wed, 22 Jul 2026 09:34:39 +0800</pubDate><description><![CDATA["别用 `SELECT *`"——这条几乎是每个程序员入行时都听过的军规。
但如果追问一句"为什么"，很多人只能答出"会多查没用的字段"。这个回答对，但太浅。它甚至被《阿里巴巴 Java 开发手册》列为**【强制】**条款——能上大厂强制规范的规则，背后一定有比"多传几个字段"更硬的理由。`SELECT *` 真正的代价，藏在 InnoDB 的**回表**机制里。
这篇文章把这条常识讲透：`SELECT *` 到底多做了哪些事，为什么它能让一个本可以走覆盖索引的查询，退化成回表查询。]]></description><category></category></item><item><title><![CDATA[一条 SQL 是怎么走索引的：从 EXPLAIN 读懂执行计划]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/163070811</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/163070811</guid><author>Treeers</author><pubDate>Wed, 22 Jul 2026 09:34:20 +0800</pubDate><description><![CDATA[线上一个接口突然变慢，第一反应往往是"加个索引试试"。加完可能好了，可能没用，甚至更慢了——因为你根本不知道优化器到底走没走你加的索引。
`EXPLAIN` 就是你和优化器之间的翻译器。它告诉你：这条 SQL 优化器打算怎么执行、走哪个索引、要扫多少行、有没有回表、有没有额外的排序和临时表。读懂 EXPLAIN，是 SQL 调优的入场券。
本文用一套可以直接跑起来的建表和造数脚本，配合真实的 `EXPLAIN` 输出日志，把执行计划里每个关键字段讲清楚。]]></description><category></category></item><item><title><![CDATA[COUNT 到底哪个快：`COUNT(*)` vs `COUNT(1)` vs `COUNT(字段)`]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/163079642</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/163079642</guid><author>Treeers</author><pubDate>Tue, 21 Jul 2026 16:32:43 +0800</pubDate><description><![CDATA[面试时被问过这个问题的人不少："`COUNT(*)` 和 `COUNT(1)` 哪个快？"
网上的答案五花八门：有人说 `COUNT(1)` 快，因为不用取字段；有人说 `COUNT(*)` 会扫描所有列所以慢；还有人斩钉截铁"用 `COUNT(1)` 性能好"。
这些说法大多是**想当然**。这篇文章用真实的 SQL 和执行计划把它讲清楚：三者到底有没有性能差别、差在哪、以及什么时候真的会慢。]]></description><category></category></item><item><title><![CDATA[MongoDB 基础语法与数据结构：从 BSON 到 CRUD 与查询操作符]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/162975848</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/162975848</guid><author>Treeers</author><pubDate>Tue, 21 Jul 2026 15:34:12 +0800</pubDate><description><![CDATA[本文介绍了MongoDB的核心概念和基本CRUD操作。主要内容包括： 核心概念对比：MongoDB文档模型与传统关系型数据库的结构差异，重点说明文档是无固定结构的BSON格式数据。 BSON数据类型：详细介绍了MongoDB支持的丰富数据类型，特别是ObjectId的组成结构和Decimal128对金融数据的重要性。]]></description><category></category></item><item><title><![CDATA[MongoDB 文档模型设计：从关系型思维到文档型思维的转变]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/162975875</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/162975875</guid><author>Treeers</author><pubDate>Mon, 20 Jul 2026 14:04:51 +0800</pubDate><description><![CDATA[文档模型最核心的能力，是让**一份"业务上原本就该在一起"的数据，在物理上也真的存在一起**。理解这一点，比记住任何 API 都重要。这篇我们就把这套思维掰开讲透：关系型和文档型的根本差异在哪、内嵌与引用到底怎么选、有哪些反直觉的坑。]]></description><category></category></item><item><title><![CDATA[MongoDB 安装与环境搭建：Windows / Linux / Docker 三种方式详解]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/162972881</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/162972881</guid><author>Treeers</author><pubDate>Mon, 20 Jul 2026 10:45:07 +0800</pubDate><description><![CDATA[本文介绍了MongoDB 7.x社区版在Windows、Linux(Ubuntu)和Docker三种环境下的安装方法。重点说明了MongoDB的核心组件（mongod服务进程、mongosh命令行客户端等）及其作用，特别指出从6.0版本开始mongo客户端已被mongosh取代的版本变化。]]></description><category></category></item><item><title><![CDATA[@Async 异步任务的五个坑]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/162818944</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/162818944</guid><author>Treeers</author><pubDate>Sun, 12 Jul 2026 22:25:07 +0800</pubDate><description><![CDATA[本文深入剖析了Spring中@Async异步任务注解的五个常见陷阱，通过可运行的demo和真实日志展示了问题现象与解决方案。]]></description><category></category></item><item><title><![CDATA[当 @Transactional 不再可靠：批量任务中的编程式事务实践]]></title><link>https://blog.youkuaiyun.com/Treeers/article/details/162796140</link><guid>https://blog.youkuaiyun.com/Treeers/article/details/162796140</guid><author>Treeers</author><pubDate>Sat, 11 Jul 2026 21:58:50 +0800</pubDate><description><![CDATA[`@Transactional` 是 Spring 里最常用的注解之一：方法上一挂，事务就有了，简单到几乎不用思考。但它的"简单"是有前提的——一旦脱离了"外部调用一个 public 方法、异常自然向上抛"这个理想模型，它就可能悄悄失效，而且**不报错、不提示**，只在数据里留下一堆对不上的脏记录。]]></description><category></category></item></channel></rss>