Oracle SQL 解析阻塞导致的一次生产异常
2024.3.1
某模块excel批量导入数据进行处理,没有限制行数,导致生成的 select 有1万多个绑定变量,Oracle 解析该 select 一直没完成,阻塞了该表其他 SQL 的解析(当时生产被阻塞的是一个高频 insert)。
该SQL大致如下,in 列表是根据导入数据动态生成的:
select count(*) from t_table t where (t.col1, t.col2, t.col3) in (
(:1, :2, :3),
(:4, :5, :6)
)
DBA提示:
1、Oracle 19C in 列表超过1000会报错,11G不会报错(我们用的是 11G)。
2、太长的 SQL 解析时可能会遇到一些内存争用。
3、DBA还提示,该 sql 效率不高,in 列表每增加一个值,执行计划就相当于多一次范围扫描,执行计划会很长。
HikariCP 与 SQLTimeoutException
最近碰到一个问题:项目的数据库连接池使用的是 HkiariCP,对每个 SQL 语句的执行超时时间设置为 30秒,结果有个 SQL 超时了,抛出异常 SQLTimeoutException,应用层回滚事务时抛出了连接已关闭的异常。但事实上事务却提交了。
写了个简单的代码来模拟生产场景:
在 Spring 的声明式事务内,有一个 insert 操作,然后是一个 update 操作,在数据库客户端执行 select for update
把要更新的行锁住,这样 update 操作就会超时。
多次调试发现 HikariCP 在碰到 SQL 异常时有个检查机制,满足特定条件的异常会直接关闭底层数据库连接,Spring 拿到的是连接的代理,由于连接已关闭,自然没法回滚事务,会碰到连接已关闭异常。
小心 fastjson 的这种“智能”
最近碰到一个现象或者说问题,同一个 JSON 格式的字符串,Spring 默认的 Jackson 类库解析报错,fastjson 却没报错、正常解析了。
场景大概是这样的,有个类有个日期属性,格式指定为 “yyyy-MM-dd”。
@Data
static class Person {
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") // Jackson
@JSONField(format = "yyyy-MM-dd") // fastjson
Date birthDay;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@JSONField(format = "yyyy-MM-dd")
Date today;
String name;
}
测试代码如下
public void testFastJson() throws JsonProcessingException {
String json = "{\"birthDay\":\"2022710\", \"name\": \"coderbee\", \"today\":\"2022-07-10\"}";
Person person = JSONObject.parseObject(json, Person.class);
System.out.println(person); // 输出解析到对象
System.out.println(JSONObject.toJSONString(person)); // 把对象转换为 JSON 字符串,再输出。
ObjectMapper mapper = new ObjectMapper();
Person jacksonPerson = mapper.readValue(json, Person.class);
System.out.println(jacksonPerson);
}
Druid 与 HikariCP 获取连接的区别
在之前的文章《踩坑 Druid 连接池》说踩了坑,后面经人提醒,发现根因是一个等待获取连接的 Job 线程被终止了,通过直接调用线程的 stop 方法终止的,这种方式破坏了 ReentrantLock 锁的模型。
下面这个方法是在持有锁的情况下执行的,执行到 1491 行时,job 线程会把自己加入条件对象的等待队列、然后释放锁,等待其他线程来唤醒;
其他线程调用 notEmpty.signal() 方法时,会把 job 线程从条件对象的等待队列转移到 AQS 的获取队列上,让 job 线程重新获取锁、继续执行。
当上一个持有锁的线程释放锁后,它会唤醒下一个,即执行 662 行。
聊个线程有关的
最近看到逻辑类似下面的代码:
乍一看,我觉得那段异步执行的代码是没法正确把 userId 保存进数据库的,查了数据发现保存的没有问题。呃,有点意思了,为啥没有问题呢。。。
看了 UserUtil 的源码、线程池 executor 实例的初始参数、以及这个接口的请求频率后,想明白了为什么没有踩坑。
但坑是在的,一个坑没有踩中不代表不存在,可以想想请求频率什么样的时候,这个逻辑就会踩坑呢。
又踩坑了。ThreadPoolExecutor?
问题是出现在 24 号的时候,当时有台 weblogic 实例出现阻塞,运维 dump 线程栈后重启了,有个同事进行分析。
该同事分析线程栈后认为问题出在一个被外部系统调用的接口,这个接口收到请求后会从数据库查询数据,然后把数据处理后发提交到线程池,再由线程池异步发送到 MQ 服务器,调用方监听 MQ 进行数据处理,接口代码大致如下:
踩坑 Druid 连接池
这周有个应用的一个实例出现了没有响应,庆幸运维那边在重启前做了线程和内存的 dump 。
线程 dump 文件打开一看,竟然4万多行。。后来发现同事用一个可视化工具来分析线程栈,我也把这个工具加入工具箱:IBM Thread and Monitor Dump Analyzer for Java
可以按线程名词、状态、方法栈的深度来进行排序。
下面说说这次踩的坑。
Spring 事务原理与集成 MyBatis 事务管理
1. 事务管理器抽象
一个事务管理器只需要三个基本的能力:获取一个事务、提交事务、回滚事务。
public interface PlatformTransactionManager extends TransactionManager {
// 获取一个事务
TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException;
// 提交事务
void commit(TransactionStatus status) throws TransactionException;
// 回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
DataSourceTransactionManagerAutoConfiguration
配置类导入了数据库事务管理器 DataSourceTransactionManager
。
2. 事务同步回调钩子
事务同步回调钩子让我们有机会在事务的各个阶段加入一些协调的动作。
public interface TransactionSynchronization extends Flushable {
/** Completion status in case of proper commit. */
int STATUS_COMMITTED = 0;
/** Completion status in case of proper rollback. */
int STATUS_ROLLED_BACK = 1;
/** Completion status in case of heuristic mixed completion or system errors. */
int STATUS_UNKNOWN = 2;
default void suspend() {}
default void resume() {}
default void flush() {}
default void beforeCommit(boolean readOnly) {}
default void beforeCompletion() {}
default void afterCommit() {}
default void afterCompletion(int status) {}
}
新一代 GC 神器 ZGC
0. 标记-复制算法
标记-复制算法分三个阶段:
- 标记阶段:从 GC Roots 出发,标记存活对象。
- 转移阶段:把存活对象复制到新的内存地址,原来的内存空间变成可回收的。
- 重定位阶段:存活对象被复制到其他地方后,所有指向对象旧地址的指针都要调整到对象新的地址上。
1. 概述
Z Garbage Collector,即ZGC,是一个可伸缩的、低延迟的垃圾收集器。只支持 64 位的系统。
ZGC 未分代,每次 GC 都是 FullGC 。
1.1 ZGC 的设计目标
- 亚毫秒级的最大暂停时间。
- 暂停时间不随堆、存活对象数量、根集合大小的增长而增长。
- 可处理的堆大小从 8MB 到 16TB。
总的来说,ZGC 具有如下特性:
- 并发
- 基于 Region
- 压缩
- NUMA 友好
- 使用着色指针
- 使用读屏障 load barriers
截至 JDK 14,ZGC 已在主流的操作系统上获得支持。
ConcurrentHashMap
1. 为什么 key 和 value 不允许为 null
HashMap 中允许 key、value 为 null,key 为 null 时哈希值为 0 。
ConcurrentHashMap 中都不能为 null 是因为作者 Doug Lea 认为:在并发编程中,null 值容易引来歧义,当调用 get(key) 返回 null 时,无法确定是 key 对应的 value 就是 null ,还是说这个 key 不存在。
非并发编程中可以通过调用 containsKey 方法来判断,但并发编程中无法保证这两个方法之间没有其他线程来修改 key 值。
2. 并行度 concurrencyLevel
ConcurrentHashMap 构造函数里有个参数 concurrencyLevel 提供了建议支持的并行度。
在 JDK 1.7 的实现里,分段锁段数组的大小由 concurrencyLevel 决定,为大于 concurrencyLevel 的最小的 2 次幂值,但不能超过 2^16
,初始化后不能修改。
在 JDK 1.8 里,concurrencyLevel 会影响 Node 数组的初始容量,由于并发粒度是数组的元素,从而影响并发度。
concurrencyLevel 并不是指定了精确的并发度。