202509|技术日志
0901
SQL索引失效的情况
面试的时候经常会考察SQL的索引,而SQL索引失效的情况就是在业务场景中使用SQL索引时经常会遇到的问题
常见的索引失效,也就是我们通常说的查询未命中索引,这样的情况有:
- 复合索引是遵循最左匹配原则,如果索引列顺序错了,会导致索引无法命中
- MySQL 分析本次查询最终查询出来的数据有900条,整张表有1000条数据,查询索引的成本比全表扫描来得高,因此会直接扫描全表不会命中索引
分析查询是否命中索引
可以使用 MySQL 自带的 EXPLAIN 关键字
工厂方法模式
已整理成单独文章笔记
0902
CPU占用过高排查
top显示进程CPU利用率,找到最高的那个进程,定位PID
之后通过 jstack
命令导出这个进程的堆栈信息以便定位是哪一段代码
1 |
|
同时我们查看这个进程中哪些线程占用过高,获得他们的线程ID
1 |
|
有了线程ID,就可以去 stack.log 中查找,但是堆栈日志文件中是十六进制,因此需要先转换一下
1 |
|
1 |
|
这样就可以定位具体是哪里出了问题
策略模式
笔记回顾整理
0903
Fail Fast
我们在开发中有的时候会遇到这样的错误
常见错误1: 迭代中修改
1 |
|
常见错误2: 多线程修改
1 |
|
modCount
Fail Fast 是软件编程的一种设计思想
是指当检测到错误发生的时候,立刻执行拒绝策略
我们平时写接口的时候会优先处理参数,当发现参数非法的时候立刻返回错误响应,其实也是这种思想的落地
在 JDK的 ArrayList 中就有 Fail Fast 思想的落地实现
ArrayList
继承自 AbstractList
,在 AbstractList
中定义了 modCount
变量,用于记录集合被修改的次数:
1 |
|
每当集合发生结构性修改(添加、删除元素,或调整容量导致底层数组替换)时,modCount
会自增。
例如 ArrayList
的 add()
方法:
1 |
|
而在 ArrayList 的内部迭代器中,初始化的时候会拷贝一份当前的 modCount
1 |
|
在每一次遍历的时候会优先检测是否在迭代中非法调用 remove() 导致 modCount 被修改
1 |
|
若通过迭代器自身的 remove()
方法修改集合,会同步更新 expectedModCount = modCount
,避免抛出异常(这是唯一允许的修改方式)
而当通过集合自身的方法(如 add()
、remove()
)修改集合时,只会更新 modCount
,但不会同步更新迭代器的 expectedModCount
,导致二者不一致
1 |
|
因此,若在遍历期间调用 list.remove()
(而非 iterator.remove()
),modCount
会增加,而 expectedModCount
不变,下次调用 next()
时就会抛出异常
0904
Fail Safe
Fail-Safe 机制是一种以尽可能保证系统正常运行为目标的设计方式,即使在遇到错误或异常情况下,系统仍能继续工作,避免崩溃或数据损坏
它的核心原则是:宁可以退为进,也要保证系统的健壮性和稳定性
在 JDK 中,Fail Safe 的实现可以参考 CopyOnWriteArrayList
1 |
|
0908
HashMap 和 HashTable
HashMap | HashTable | |
---|---|---|
线程安全 | 不安全 效率高 | 安全(底层synchronized)效率低 |
容量 | 初始16,始终保证2的整次幂 | 初始11,扩容2n+1 |
null key | 支持,单只能有一个 | 一个都不允许存在 |
底层数据结构 | 数组+链表,若相同数组位置中的链表长度大于8(并且数组长度大于64,转为红黑树,否则优先进行数组的扩容操作) | 数组+链表(无转换优化机制) |
HashSet 如何检查重复
当我们在 HashSet 中调用 add,实际上也只是底层调用了 HashMap 的put
1 |
|
而 Map 的 put 方法,底层调用的 putVal 方法返回值的定义是:元素不存在返回 null,元素已存在返回该key原先的元素
因此我们程序多次调用同一个 set 对相同的 key 进行 add 操作,都是对底层的 map 进行了 put 覆盖操作,只是 HashSet 封装了一层,通过 HashMap 返回的是否存在元素,来间接返回布尔值
0909
为什么n小于64的时候优先扩容
因为对数组扩容是在数据量不大的时候更为高效的方式,数组扩容可以显著解决哈希冲突的问题
如果数组长度不大,当链表长度超过8的时候直接就转红黑树,由于红黑树本身维持自平衡需要自旋,本身也有性能开销,过早引入反而会提升复杂度影响 HashMap 的性能
自定义对象作为HashMap key时的最佳实践
重写equals和hashCode
当采用自定义对象作为 HashMap 的 key 时,需要保证该对象重写了 hashCode()
以及 equals()
这是由于 HashMap 在底层维护数据时的逻辑结构决定的
HashMap 底层首先会根据 key 对象的 hashCode()
计算出哈希值,这个值决定了数组中桶(bucket)的位置。当多个 key
的哈希值相同时(哈希冲突),HashMap
会通过 equals()
逐一比较桶中的对象,找到真正相等的 key
。若 equals
实现不当,会导致无法识别相同的 key
保证参与 hash 值计算的字段不可变性
由于该对象作为 HashMap 的 key,参与该对象 hashCode 方法的字段需要严格保证不可变性
1 |
|
正确的做法应该是在代码层面严格保证参与 hashCode 方法的字段在初始化后不可变
具体的实现思路有:
- 使用 final 修饰:强推,语言自带关键字约束0910
- 不提供字段的
setter
方法 - 如果外部真的需要修改对象某个参与 hash 值计算字段的状态,返回新的实例(参考 String 类的实现,replace 方法看似修改实际上是创建了新的字符串,为的就是保证不可变性,因为 String 常常用作 HashMap 的 key)
1 |
|
多线程交替打印 0-100
整理成博客
0910
Builder
整理成博客
几种Factory模式对比
0911
OOM排查
0912
JDBC 动态参数最佳实践
问题分析
今天遇到一个问题,在调用JDBC传入代码中SQL参数的时候,参数SQL使用了类似如下的语法:
1 |
|
代码中采用 PreparedStatement 来执行 SQL 参数,此时针对占位符的参数传入的是 list 的字符串形式 'foo','bar'
.
从主观理解上来说,我们希望这一段SQL可以去查询 c_column 这一个字段值为 foo
/bar
的所有记录。
但是这条 SQL 执行完成后没有任何结果,奇怪的是也没有任何的报错。
其实这是由于 PreparedStatement 的特性造成的。
我们都知道 PreparedStatement 支持防止 SQL 注入,因此引入占位符机制。上述我们的 SQL,看似参数是传入了两个元素,通过 IN 来实现 or 的查询效果。但是在 PreparedStatement 的作用下,实际上 MySQL 会将其解析为一个完整的元素,因此 IN 的括号中只算做了一个元素。相当于去表里寻找 code = ‘foo,bar’ 的记录,那当然是没有的,自然也没有任何结果,也不会有任何的报错。
网络上常见的一种实现方式是采用动态拼接 ?
占位符的方式进行实现。
但是实际上这样的实现方式并不合理。
要理解为什么不能这么做以及应该如何做,我们需要了解 PreparedStatement 和 Statement 语句的区别。
PreparedStatement和Statement
在 JDBC 中,PreparedStatement
和普通 Statement
都是用于执行 SQL 语句的接口,但核心区别在于:
- 预编译机制:
PreparedStatement
会对 SQL 语句进行预编译,而普通Statement
每次不会。 - SQL 注入防护:
PreparedStatement
通过参数绑定(?
占位符)避免 SQL 注入,而普通Statement
直接拼接字符串有注入风险。 - 性能:重复执行相同结构的 SQL 时,
PreparedStatement
性能更优(预编译只需一次)。
我们重点关注预编译机制。预编译语句在数据库侧是需要通过缓存进行维护的。我们在服务代码中通过 JDBC 调用创建了 PreparedStatement 对象,此时数据库会预先解析 SQL 结构、生成执行计划,之后只需传入参数即可重复执行,无需重新解析。这样的场景十分适合 “相同结构的SQL“,也就是那些参数位置和数量都是在程序运行期间确定的 SQL.
而对于上述问题中的 SQL,我们可能传入需要 IN 查询的 list,某一个请求是3个长度,也有可能是100个长度。如果采用根据数组长度动态拼接占位符的策略,在实际执行 PreparedStatement 的时候,对应的就是两个完全不一样的 SQL,但数据库侧都需要进行维护。
此时预编译的效果已经失效,因为前一次根据 list 动态拼接占位符生成SQL的预编译缓存,不一定就会在下一次 list 参数的到来时进行命中。同时由于数据库侧还需要不断维护这些缓存数据,不但没有利用上预编译的性能优化,反倒增大了系统的开销。数据库连接池对 PreparedStatement
数量有上限,大量动态 IN
子句会耗尽这个上限,甚至导致内存溢出(OOM)。
不要简单地认为在代码中显式调用 close()
释放了 PreparedStatement
对象就可以销毁这些缓存。数据库仍可能大量维护预编译语句的缓存,原因与数据库的 预编译语句缓存机制 有关,而非我们作为应用层的服务中对象的生命周期。
此时,直接使用 Statement 动态根据 list 参数拼接条件更为妥当。无论 IN
子句参数数量如何变化,都只需生成一次 SQL 并执行,不会产生大量冗余的预编译对象。
总结
对于结构固定相同的SQL,在程序运行期间能够确定参数位置和数量的SQL,使用 PreparedStatement 执行,充分利用预编译机制
对于在程序运行期间可能存在动态参数拼接导致结构改变的SQL,采用 Statement 执行,避免预编译缓存爆炸导致的性能问题。
0916
try-with-resource
确保在 try 语句块结束时自动关闭资源,从而避免资源泄漏
但是要求使用的资源类实现AutoClosable
接口,JVM底层会在try
块代码执行完成后调用实现类重写的close()
相比于普通的 try-catch 而言更好用:
- 更简洁,当申请多个系统资源都需要catch的时候,传统方法会出现catch金字塔现象
- 能够有效解决某些资源在catch中进行关闭但是失败导致原先try中抛出的异常被覆盖的问题
下面直接以代码举例
写法更简洁
1 |
|