coderbee笔记 https://coderbee.net 日拱一卒,不期速成,厚积薄发。 wen866595@gmail.com Sun, 08 Sep 2024 00:40:16 +0000 zh-Hans hourly 1 https://wordpress.org/?v=6.5.8 WeakReference 使用不当导致OOM? https://coderbee.net/index.php/framework/20240908/2296 https://coderbee.net/index.php/framework/20240908/2296#respond Sun, 08 Sep 2024 00:35:16 +0000 https://coderbee.net/?p=2296 继续阅读 ]]> 9月5、6号两天,有个古老的应用连续出现频繁的 FullGC,最终OOM。
这个古老应用的框架是十多年前基于 Spring 魔改的,在部署上分为 web、app 两层,web通过 EJB 进行调用app里的服务访问数据库。

这两层访问 Spring 容器里的 Bean 一般都是通过类似下面的写法来获取:

SomeService service = CustAppContextHolder.getContext(contextName).getBean(beanName);
service.doSomething();

// CustAppContextHolder.getContext 的实现大致如下:
public class CustAppContextHolder {
private static Map<String, WeakReference> caches = new ConcurrentHashMap<String, WeakReference>();

public static ApplicationContext getContext(String contextName) {
    WeakReference<ApplicationContext> weakReference = caches.get(contextName);
    ApplicationContext context;
    if (weakReference != null && (context = weakReference.get()) != null) {
        return context;
    } else {
        synchronized (CustAppContextHolder.class) {
            context = initContext(contextName);
            caches.put(contextName, new WeakReference<>(context));
        }
    }
    return context;
}

private static ApplicationContext initContext(String contextName) {
    System.out.println("init context:" + contextName);
    return null;
}

}

从内存 dump 分析,ZookeeperRegistry.subscribed 对象占用了内存导致没法回收,同一个服务,里面好几千个消费者实例。每个服务对应的 URL 里带有一个时间戳,导致进行服务订阅时都认为是不同的服务。
首先找负责该 RPC 框架的团队,他说通过这种方式 SomeService service = CustAppContextHolder.getContext(contextName).getBean(beanName); 获取 RPC 消费者时,每次都会创建一个新的实例,这个实例会在 ZK 注册。这种写法其实一直是这样的,十多年了,中间经历了从 weblogic 切换到信创基于 apusic 的中间件。

第一天 5号排查时主要围绕这个 RPC 调用的地方进行,但从代码看,很多服务都没有进行调用了,但仍然有消费者实例。因此说是 RPC 调用导致是说不通的。

晚上回来又想了想,不是 RPC 被调用了,而应该是容器被初始化,导致创建了 RPC 消费者实例。
第二天到了公司,查了下日志,5号那天,有接近1万次的 “init context:” 输出,也就是说应用容器被创建了近上万次,怪不得那么多消费者实例。在4号发版之前的日志里,每天也有小几百次的初始化日志输出,不至于把内存占用太多。
输出日志的线程都是 ORBWorker,后来咨询负责 apusic 中间件的团队,说这个是 apusic 负责处理 EJB 调用的线程。他们还指出可能是 CustAppContextHolder 使用 WeakReference 导致上下文被回收后,再次来获取上下文时创建新的,这个老框架的下一个大版本就把这个 WeakReference 移除了。

这个问题目前还没稳定地复现出来,生产上也是个别应用实例会 OOM,如果真是这个 WeakReference 导致的,可能的优化方案有:
1、在应用代码里定义一个跟 CustAppContextHolder 包和类名完全一样的类,把 WeakReference 移除掉,通过类加载优先级替换框架的类。
2、针对 WeakReference 的特定,它引用的对象在没有强引用时可以被 GC 回收,定义静态变量来维持强引用,保证初始化的上下文不会被回收,从而可以复用,减少 RPC 消费者的实例。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

]]>
https://coderbee.net/index.php/framework/20240908/2296/feed 0
一次存储过程调用是一个事务吗? https://coderbee.net/index.php/db/20240828/2293 https://coderbee.net/index.php/db/20240828/2293#respond Wed, 28 Aug 2024 15:22:03 +0000 https://coderbee.net/?p=2293 继续阅读 ]]> 工作中经常接触一些 Oracle 的存储过程,里面各种复杂的业务逻辑,在调用存储过程之前、之后的 Java 代码里还有各种业务逻辑操作。
存储过程里还可能有显式的 commit/rollback 等事务控制语句,容易让人对系统的事务管理产生困惑。

对于事务管理,可以从 Oracle 数据库和 JDBC 应用端两个角度来说。

对于 Oracle 数据库,只有碰到 commit 语句时才会提交事务,然后开启新事务;碰到 rollback,回滚当前事务的操作,再开启新事务。

应用端角度通过 JDBC 与数据库进行交互,JDBC 有两种事务管理模式:自动提交(默认)、手动管理。

对于自动提交模式,JDBC 向数据库发出一个 SQL 命令后,如果 SQL 执行成功,则发出 commit 提交事务,否则发出 rollback 回滚事务。

回到标题,一次存储过程调用是一个事务吗?如果存储过程里没有显式的 commit/rollback 语句,且 JDBC 是自动提交的,那么一次存储过程调用就是一个事务。

]]>
https://coderbee.net/index.php/db/20240828/2293/feed 0
Oracle 绑定变量过多导致 DML 阻塞 https://coderbee.net/index.php/db/20240327/2265 https://coderbee.net/index.php/db/20240327/2265#respond Wed, 27 Mar 2024 15:31:48 +0000 https://coderbee.net/?p=2265 继续阅读 ]]> 生产有个功能是上传一个 excel 来进行批量数据操作,excel 里面有三列数据,构建出来的查询大概如下:

select * from t_test t where (t.col1, t.col2, t.col3) in (
(:1, :2, :3),
(:4, :5, :6)
)

IN 子句的长度取决于上传的 excel 里的行数,每一行对应一个 元组。

由于没有限制 excel 里最大行数,导致构建出来的 SQL 有1万多个绑定变量,然后这个 SQL 一直处于解析中,阻塞了这个表上的其他操作(当时主要是插入操作)。(DBA 联系厂商分析给出的结论)

之前一直以为 IN 的列表不能超过 1000,那么生产为什么没有报错,当时拿了那个引发问题的 excel 文件在测试环境复现,是可以正常执行完成的。
DBA 的解释是 11g 里不会报错,19c 开始会报错。DBA 另外提到 IN 列表过长的执行效率是不高的,IN 列表每多一行,执行计划 UNION ALL 下也多一行:

今天重新提起这个,有同事写了个 SQL 在本地验证是报错的:

select * from t_test t where t.col1 in (1, 2, 3, ..., 1001);

为啥当时测试环境复现通过,这个为啥报错?必须搞清楚了。

原因如下:
https://docs.oracle.com/en/database/oracle/oracle-database/12.2/sqlrf/Expression-Lists.html#GUID-5CC8FC75-813B-44AA-8737-D940FA887D1E

A comma-delimited list of expressions can contain no more than 1000 expressions. A comma-delimited list of sets of expressions can contain any number of sets, but each set can contain no more than 1000 expressions.
用逗号分隔的表达式列表中不能包含超过1000个表达式。用逗号分隔的表达式集合列表可以包含任意数量的集合,但每个集合不能包含超过1000个表达式。

顺便狗到了 IN 怎么超过 1000 的方法:

select * from worktracker
where rmid in ( 1, ..., 999 )
or rmid in ( 1001, ..., 1999 )
or ...

select * from worktracker
where ( 1, rmid ) in (
( 1, '1001' ),
( 1, '1212' ),
...
)


欢迎关注我的微信公众号: coderbee笔记

]]>
https://coderbee.net/index.php/db/20240327/2265/feed 0
HikariCP 与 SQLTimeoutException https://coderbee.net/index.php/framework/20220827/2207 https://coderbee.net/index.php/framework/20220827/2207#respond Sat, 27 Aug 2022 03:41:54 +0000 https://coderbee.net/?p=2207 继续阅读 ]]> 最近碰到一个问题:项目的数据库连接池使用的是 HkiariCP,对每个 SQL 语句的执行超时时间设置为 30秒,结果有个 SQL 超时了,抛出异常 SQLTimeoutException,应用层回滚事务时抛出了连接已关闭的异常。但事实上事务却提交了。

写了个简单的代码来模拟生产场景:
在 Spring 的声明式事务内,有一个 insert 操作,然后是一个 update 操作,在数据库客户端执行 select for update 把要更新的行锁住,这样 update 操作就会超时。

多次调试发现 HikariCP 在碰到 SQL 异常时有个检查机制,满足特定条件的异常会直接关闭底层数据库连接,Spring 拿到的是连接的代理,由于连接已关闭,自然没法回滚事务,会碰到连接已关闭异常。

HikariProxyPreparedStatement

public int executeUpdate() throws SQLException {
    try {
        return super.executeUpdate();
    } catch (SQLException var2) {
        throw this.checkException(var2);
    }
}

final SQLException checkException(SQLException e)
{
  return connection.checkException(e);
}

ProxyConnection

final SQLException checkException(SQLException sqle)
{
  boolean evict = false;
  SQLException nse = sqle;
  final SQLExceptionOverride exceptionOverride = poolEntry.getPoolBase().exceptionOverride;
  for (int depth = 0; delegate != ClosedConnection.CLOSED_CONNECTION && nse != null && depth < 10; depth++) {
     final String sqlState = nse.getSQLState();
     if (sqlState != null && sqlState.startsWith("08")
         || nse instanceof SQLTimeoutException
         || ERROR_STATES.contains(sqlState)
         || ERROR_CODES.contains(nse.getErrorCode())) {

        if (exceptionOverride != null && exceptionOverride.adjudicate(nse) == DO_NOT_EVICT) {
           break;
        }

        // broken connection
        evict = true;
        break;
     }
     else {
        nse = nse.getNextException();
     }
  }

  if (evict) {
     SQLException exception = (nse != null) ? nse : sqle;
     LOGGER.warn("{} - Connection {} marked as broken because of SQLSTATE({}), ErrorCode({})",
        poolEntry.getPoolName(), delegate, exception.getSQLState(), exception.getErrorCode(), exception);
     leakTask.cancel();
     poolEntry.evict("(connection is broken)");  // 这里由异步线程来关闭底层的物理连接
     delegate = ClosedConnection.CLOSED_CONNECTION; // 连接代理被替换为已关闭的,Spring 自然无法回滚事务
  }

  return sqle;
}

那么还有个问题,为什么连接关闭会提交事务,得看看 JDBC Connection 的文档,有如下的特别说明:连接关闭时,事务是提交还是回滚取决于时具体的实现,毕竟 JDBC 只是一个规范、上层 API 。
https://docs.oracle.com/javase/8/docs/api/java/sql/Connection.html#close–

It is strongly recommended that an application explicitly commits or rolls back an active transaction prior to calling the 
close method.  If the 
close method is called and there is an active transaction, the results are implementation-defined. 

那么怎么由上层应用来决定是提交还是回滚事务呢,从 checkException 方法可以发现,如果配置一个 SQLExceptionOverride 实现类且其方法 adjudicate(nse) 返回 DO_NOT_EVICT 就不会直接关闭底层连接。

我们可以定制一个 SQLExceptionOverride 实现如下,然后通过配置属性 exceptionOverrideClassName 来指定。

public class MyExceptionOverride implements SQLExceptionOverride {
    public Override adjudicate(SQLException sqlException) {
        // HikariCP 碰到 SQLException 不直接关闭底层连接,由上层应用来决定。
        return Override.CONTINUE_EVICT;
    }
}

欢迎关注我的微信公众号: coderbee笔记

]]>
https://coderbee.net/index.php/framework/20220827/2207/feed 0
小心 fastjson 的这种“智能” https://coderbee.net/index.php/java/20220710/2205 https://coderbee.net/index.php/java/20220710/2205#respond Sun, 10 Jul 2022 14:50:18 +0000 https://coderbee.net/?p=2205 继续阅读 ]]> 最近碰到一个现象或者说问题,同一个 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);
}

得到的结果是这样的:

Person(birthDay=Sun Jul 10 00:00:00 CST 2022, name=coderbee)
{"birthDay":"2022-07-10","name":"coderbee"}

com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "20220710": expected format "yyyy-MM-dd"
 at [Source: (String)"{"birthDay":"20220710", "name": "coderbee"}"; line: 1, column: 13] (through reference chain: net.coderbee.algorithm.SnowFlakeTest$Person["birthDay"])

可以看到 fastjson 解析没有报错,“智能”地处理了格式不正确的问题,而 jackson 报错了。
我们再看看上面输出的第二行,就是把 fastjson 解析到的对象再次转换为 JSON 字符串,可以看到 birthDay 的格式是符合代码定义的,这样的字符串与输入是不一致的。

当我把 birthDay 的字符串替换为 “2022710”,仍然没有报错,但解析到的结果是错误的了:

Person(birthDay=Thu Jan 01 08:33:42 CST 1970, name=coderbee)
{"birthDay":"1970-01-01","name":"coderbee"}

不赞成这种“智能”,该报错的时候就得报错,这样才知道传错了。最主要的是,要切换其他 JSON 实现库才不会突然报错。


欢迎关注我的微信公众号: coderbee笔记

]]>
https://coderbee.net/index.php/java/20220710/2205/feed 0
Druid 与 HikariCP 获取连接的区别 https://coderbee.net/index.php/java/20220108/2197 https://coderbee.net/index.php/java/20220108/2197#respond Sat, 08 Jan 2022 15:04:26 +0000 https://coderbee.net/?p=2197 继续阅读 ]]> 在之前的文章《踩坑 Druid 连接池》说踩了坑,后面经人提醒,发现根因是一个等待获取连接的 Job 线程被终止了,通过直接调用线程的 stop 方法终止的,这种方式破坏了 ReentrantLock 锁的模型。

下面这个方法是在持有锁的情况下执行的,执行到 1491 行时,job 线程会把自己加入条件对象的等待队列、然后释放锁,等待其他线程来唤醒;

其他线程调用 notEmpty.signal() 方法时,会把 job 线程从条件对象的等待队列转移到 AQS 的获取队列上,让 job 线程重新获取锁、继续执行。

当上一个持有锁的线程释放锁后,它会唤醒下一个,即执行 662 行。

线程被唤醒后执行下面的方法,问题是如果被唤醒的线程(job 线程)已经被终止了,就不会执行这个方法,导致 job 线程的等待节点一直在 head.next 位置,它之后的线程也不会被唤醒。

要能能明白的是,JVM 里 Thread 实例还在、但对应的操作系统线程是已终止,这是可以的,只是是不正常的。

HikariCP 为了达到高性能,做了不少有意思的实现,通过一个定制的数据结构 ConcurrentBag 来管理连接。

private final CopyOnWriteArrayList<T> sharedList;
private final ThreadLocal<List<Object>> threadList;
private final SynchronousQueue<T> handoffQueue;

public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
{
  // 优先从本地队列获取,竞争小
  final List<Object> list = threadList.get();
  for (int i = list.size() - 1; i >= 0; i--) {
     final Object entry = list.remove(i);
     @SuppressWarnings("unchecked")
     final T bagEntry = weakThreadLocals ? ((WeakReference<T>) entry).get() : (T) entry;
     if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
        return bagEntry;
     }
  }

  // 获取不到再从全局队列获取
  // 读多写少,全局队列适合用 COW 的实现队列
  final int waiting = waiters.incrementAndGet();
  try {
     for (T bagEntry : sharedList) {
        if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
           // If we may have stolen another waiter's connection, request another bag add.
           if (waiting > 1) {
              listener.addBagItem(waiting - 1);
           }
           return bagEntry;
        }
     }

     listener.addBagItem(waiting);

// 还获取不到则在同步队列上等待获取
     timeout = timeUnit.toNanos(timeout);
     do {
        final long start = currentTime();
        final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
        if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
           return bagEntry;
        }

        timeout -= elapsedNanos(start);
     } while (timeout > 10_000);

     return null;
  }
  finally {
     waiters.decrementAndGet();
  }
}

新连接加入池里:

public void add(final T bagEntry)
{
   if (closed) {
      LOGGER.info("ConcurrentBag has been closed, ignoring add()");
      throw new IllegalStateException("ConcurrentBag has been closed, ignoring add()");
   }

   sharedList.add(bagEntry);

   // spin until a thread takes it or none are waiting
   while (waiters.get() > 0 && bagEntry.getState() == STATE_NOT_IN_USE && !handoffQueue.offer(bagEntry)) {
      Thread.yield();
   }
}

如上,新连接首先加入到全局队列,然后看是否有线程在等待,有则通过 handoffQueue 进行交付。

可以发现,HikariCP 与 Druid 在这块的不同是,它不通过 Lock/Condition 来协调创建连接与获取连接的线程。

SynchronousQueue 底层的实现是无锁的,因此即使出现某个等待线程被终止,也不会影响后续的线程。它是适合于这种传递的场景的。


欢迎关注我的微信公众号: coderbee笔记

]]>
https://coderbee.net/index.php/java/20220108/2197/feed 0
聊个线程有关的 https://coderbee.net/index.php/java/20211225/2193 https://coderbee.net/index.php/java/20211225/2193#respond Sat, 25 Dec 2021 09:02:04 +0000 https://coderbee.net/?p=2193 继续阅读 ]]> 最近看到逻辑类似下面的代码:

乍一看,我觉得那段异步执行的代码是没法正确把 userId 保存进数据库的,查了数据发现保存的没有问题。呃,有点意思了,为啥没有问题呢。。。

看了 UserUtil 的源码、线程池 executor 实例的初始参数、以及这个接口的请求频率后,想明白了为什么没有踩坑。
但坑是在的,一个坑没有踩中不代表不存在,可以想想请求频率什么样的时候,这个逻辑就会踩坑呢。

]]>
https://coderbee.net/index.php/java/20211225/2193/feed 0
又踩坑了。ThreadPoolExecutor? https://coderbee.net/index.php/java/20211128/2189 https://coderbee.net/index.php/java/20211128/2189#respond Sun, 28 Nov 2021 11:39:13 +0000 https://coderbee.net/?p=2189 继续阅读 ]]> 问题是出现在 24 号的时候,当时有台 weblogic 实例出现阻塞,运维 dump 线程栈后重启了,有个同事进行分析。

该同事分析线程栈后认为问题出在一个被外部系统调用的接口,这个接口收到请求后会从数据库查询数据,然后把数据处理后发提交到线程池,再由线程池异步发送到 MQ 服务器,调用方监听 MQ 进行数据处理,接口代码大致如下:

@RequestMapping("publish")
public void publish() {
    String msg = RandomStringUtils.randomAlphabetic(32);
    log.info("before submit task :" + msg);
    ThreadPoolUtil.getPoolExecutor().execute(() -> {
        log.info("publish msq to mq done:" + msg);
    });
    log.info("submit task done:" + msg);
}

public class ThreadPoolUtil {
    private static ThreadPoolExecutor pool = new ThreadPoolExecutor(10, 30,
            20, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),
            new CustomThreadFactory(), new CustomRejectHandler());

    private static AtomicInteger counter = new AtomicInteger(0);

    public static ThreadPoolExecutor getPoolExecutor() {
        return pool;
    }

    public static void shutdown() {
        pool.shutdown();
    }

    static class CustomThreadFactory implements ThreadFactory {
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setName(ThreadPoolUtil.class.getName() + counter.incrementAndGet());
            return thread;
        }
    }

    static class CustomRejectHandler implements RejectedExecutionHandler {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                pool.getQueue().put(r);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

那同事问了发起调用的系统的人,对方每次取 300 个任务来发起调用,他觉得是对方调用的并发太大导致的。

虽然隔得有点远,我听到后觉得有疑惑,调用方每次取300个任务更可能是串行调用、而不是300个并发。

我找他要了线程栈文件过来,发现大多数工作线程都是空闲,阻塞的线程的线程栈与下面的相似:

"http-nio-8001-exec-10" #36 daemon prio=5 os_prio=0 tid=0x0000000026b8c800 nid=0x2f64 waiting on condition [0x000000002778c000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000837cd8b8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
        at java.util.concurrent.ArrayBlockingQueue.put(ArrayBlockingQueue.java:353)
        at net.coderbee.cloud.controller.ThreadPoolUtil$CustomRejectHandler.rejectedExecution(ThreadPoolUtil.java:34)
        at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
        at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
        at net.coderbee.cloud.controller.PublishController.publish(PublishController.java:22)

看了又看之后,结合线程栈和源码,我觉得这个接口只是一个受害者,根本的问题不是这个接口,而在于线程池,线程池为什么不再继续处理提交的任务呢。

我一开始是怀疑是不是踩了 ThreadPoolExecutor 的 bug(滑稽脸,太膨胀了,竟然怀疑 JUC 的 bug),搜了下并没有什么启发。

因为线程是被阻塞了, weblogic 里线程的状态是 stuck,再拿 “ThreadPoolExecutor stuck” 去搜,在 stackoverflow 看到这个 Java ThreadPoolExecutor getting stuck while using ArrayBlockingQueue

回答里提到的调用 shutdown 方法敲醒了我,这个线程池果然在一个定时任务调用的地方被调用:ThreadPoolUtil.getPoolExecutor(),然后调用了 shutdown 方法(就在上一个版本引入的代码)。

线程池被 shuadown 后,提交上来的任务被执行 reject ,由于 RejectHandler 是定制的,把提交任务的线程阻塞住了。

到此问题算是搞清楚了,有如下反思:
1、 只要方法、对象暴露出来了,就可能会被调用,像这种全局的线程池就不应暴露出来。
2、 定位问题,最好是有证据的,不要只是“应该就是这个”。
3、 碰到没什么思路的问题,多检查下最近版本的代码,问题很多都是在最近版本引进的。

]]>
https://coderbee.net/index.php/java/20211128/2189/feed 0
踩坑 Druid 连接池 https://coderbee.net/index.php/java/20211120/2178 https://coderbee.net/index.php/java/20211120/2178#respond Sat, 20 Nov 2021 14:36:21 +0000 https://coderbee.net/?p=2178 继续阅读 ]]> 这周有个应用的一个实例出现了没有响应,庆幸运维那边在重启前做了线程和内存的 dump 。

线程 dump 文件打开一看,竟然4万多行。。后来发现同事用一个可视化工具来分析线程栈,我也把这个工具加入工具箱:IBM Thread and Monitor Dump Analyzer for Java

下图是这个工具的方法栈分析视图:

可以按线程名词、状态、方法栈的深度来进行排序。

下面说说这次踩的坑。

从线程栈来看,200个 tomcat 线程都在等待获取数据库连接,首先怀疑是不是数据库连接泄漏。

全局搜了代码,发现直接使用 Connection 的地方都不存在泄漏的。

想起这个 SpringBoot druid 踩坑笔记,怀疑是不是配置有问题,本地验证了下,配置是没问题的,连接池最大个数 50 个。

因为之前也听过 Druid 存在死锁的故事,网上搜到了这篇文章 【问题经验】记一次Dubbo线程耗尽的问题-druid数据库连接池突发性能

核对线程栈发现确实存在类似的情况:Druid 专门负责创建连接的线程在创建好连接准备添加到池里时、阻塞在获取锁上,这个锁在线程栈上 dump 文件上并没有找到被哪个线程持有,通过内存 dump 发现这个锁也确实没有被人持有,妥妥的的一个并发问题的 bug 。

因为公司网络隔离,没有截取保存当时分析的内容。

Druid 的版本是 1.0.15 。

通过 visualvm 查看对象的值:


欢迎关注我的微信公众号: coderbee笔记

]]>
https://coderbee.net/index.php/java/20211120/2178/feed 0
Spring 事务原理与集成 MyBatis 事务管理 https://coderbee.net/index.php/framework/20210726/2171 https://coderbee.net/index.php/framework/20210726/2171#respond Mon, 26 Jul 2021 14:09:33 +0000 https://coderbee.net/?p=2171 继续阅读 ]]> 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) {}
}

3. TransactionSynchronizationManager

TransactionSynchronizationManager 委托集中管理与每个线程绑定的资源和事务同步钩子。

private static final ThreadLocal<Map<Object, Object>> resources =
        new NamedThreadLocal<>("Transactional resources");

private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
        new NamedThreadLocal<>("Transaction synchronizations");

private static final ThreadLocal<String> currentTransactionName =
        new NamedThreadLocal<>("Current transaction name");

private static final ThreadLocal<Boolean> currentTransactionReadOnly =
        new NamedThreadLocal<>("Current transaction read-only status");

private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
        new NamedThreadLocal<>("Current transaction isolation level");

private static final ThreadLocal<Boolean> actualTransactionActive =
        new NamedThreadLocal<>("Actual transaction active");

4. 织入事务

spring-boot-autoconfig 模块会导入:TransactionAutoConfiguration -> EnableTransactionManagement -> ProxyTransactionManagementConfiguration

ProxyTransactionManagementConfiguration会注册 Bean : TransactionInterceptor(要切入的逻辑)、AnnotationTransactionAttributeSource(切入点)、BeanFactoryTransactionAttributeSourceAdvisor(切面)。

@Configuration
public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {

    @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor() {
        BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
        advisor.setTransactionAttributeSource(transactionAttributeSource());
        advisor.setAdvice(transactionInterceptor());
        advisor.setOrder(this.enableTx.<Integer>getNumber("order"));
        return advisor;
    }

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public TransactionAttributeSource transactionAttributeSource() {
        return new AnnotationTransactionAttributeSource();
    }

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public TransactionInterceptor transactionInterceptor() {
        TransactionInterceptor interceptor = new TransactionInterceptor();
        interceptor.setTransactionAttributeSource(transactionAttributeSource());
        if (this.txManager != null) {
            interceptor.setTransactionManager(this.txManager);
        }
        return interceptor;
    }
}

AnnotationAwareAspectJAutoProxyCreator 在创建 Bean 的代理对象时,会获取到 BeanFactoryTransactionAttributeSourceAdvisor 判断该 Bean 的哪些方法需要织入事务,从而把 TransactionInterceptor 织入到代理对象。

5. TransactionInterceptor 事务拦截器

TransactionInterceptor 被 AOP 织入到代理对象,拦截对事务方法的调用,然后调用父类 TransactionAspectSupport.invokeWithinTransaction,该方法调用 TransactionManager 实现类,在执行目标方法前后加入 获取事务、提交事务或回滚事务的控制。

// TransactionInterceptor 
public Object invoke(MethodInvocation invocation) throws Throwable {
    Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

    // Adapt to TransactionAspectSupport's invokeWithinTransaction...
    return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

// TransactionAspectSupport
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
        final InvocationCallback invocation) throws Throwable {
    // 获取类和方法的事务属性。如果不是事务方法, txAttr 是 null 。
    final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);

    // 根据事务属性获取事务管理器
    final PlatformTransactionManager tm = determineTransactionManager(txAttr);
    final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

    // 省略 ReactiveTransactionManager 相关的。。

    // 基于数据库的 DataSourceTransactionManager 是 PlatformTransactionManager 的子类
    PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
    final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

    if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
        // 标准事务 demarcation with getTransaction and commit/rollback calls.
        // 创建一个新的事务
        TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

        Object retVal;
        try {
            // 调用目标方法
            retVal = invocation.proceedWithInvocation();
        }
        catch (Throwable ex) {
            // 处理异常:回滚事务或正常提交事务
            completeTransactionAfterThrowing(txInfo, ex);
            throw ex;
        }
        finally {
            cleanupTransactionInfo(txInfo);
        }

        if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
            // Set rollback-only in case of Vavr failure matching our rollback rules...
            TransactionStatus status = txInfo.getTransactionStatus();
            if (status != null && txAttr != null) {
                retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
            }
        }

        // 正常提交事务
        commitTransactionAfterReturning(txInfo);
        return retVal;
    }

    // 省略 CallbackPreferringPlatformTransactionManager 相关的
}

protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
    if (txInfo != null && txInfo.getTransactionStatus() != null) {
        if (logger.isTraceEnabled()) {
            logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
        }
        txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
    }
}

6. AbstractPlatformTransactionManager 事务控制实现类

AbstractPlatformTransactionManager 在获取事务时实现了 Spring 事务传播行为(创建新事务、创建新事务并挂起当前事务)。

事务提交或回滚前后回调已注册的 TransactionSynchronization 的相关方法。

6.1 创建事务

public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
    // 这里只是创建了一个事务对象,并没有绑定到底层的资源,比如 JDBC 连接。
    Object transaction = doGetTransaction();

    boolean debugEnabled = logger.isDebugEnabled();

    if (definition == null) {
        definition = new DefaultTransactionDefinition();
    }

    if (isExistingTransaction(transaction)) {
        // 如果当前已存在事务,要根据新的传播行为来决定如何处理
        return handleExistingTransaction(definition, transaction, debugEnabled);
    }

    // 到这里说明当前没有事务存在。

    // 新事务要求当前必须存在事务,抛出异常
    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
        throw new IllegalTransactionStateException(
                "No existing transaction found for transaction marked with propagation 'mandatory'");
    }
    else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
            definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
            definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
        // 挂起当前事务,null 表示没有事务,但可能存在同步钩子。
        SuspendedResourcesHolder suspendedResources = suspend(null);

        try {
            boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
            // 创建一个事务对象状态。
            DefaultTransactionStatus status = newTransactionStatus(
                    definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);

            // 这里会获得真实的数据库连接
            // TransactionSynchronizationManager.bindResource(this.getDataSource(), txObject.getConnectionHolder());
            doBegin(transaction, definition);

            // 初始化 TransactionSynchronizationManager 的线程本地变量,更新为当前事务的。
            prepareSynchronization(status, definition);
            return status;
        }
        catch (RuntimeException ex) {
            resume(null, suspendedResources);
            throw ex;
        }
        catch (Error err) {
            resume(null, suspendedResources);
            throw err;
        }
    }
    else {
        // 创建一个"空"事务:非真实的事务,但准备了同步钩子
        boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
        return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
    }
}

protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) {
    if (status.isNewSynchronization()) {
        TransactionSynchronizationManager.setActualTransactionActive(status.hasTransaction());
        TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(
                definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ?
                        definition.getIsolationLevel() : null);
        TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());
        TransactionSynchronizationManager.setCurrentTransactionName(definition.getName());
        TransactionSynchronizationManager.initSynchronization();
    }
}

// DataSourceTransactionManager
protected void doBegin(Object transaction, TransactionDefinition definition) {
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
    Connection con = null;

    try {
        if (txObject.getConnectionHolder() == null ||
                txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
            Connection newCon = this.dataSource.getConnection();
            if (logger.isDebugEnabled()) {
                logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
            }
            txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
        }

        txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
        con = txObject.getConnectionHolder().getConnection();

        Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
        txObject.setPreviousIsolationLevel(previousIsolationLevel);

        // Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
        // so we don't want to do it unnecessarily (for example if we've explicitly
        // configured the connection pool to set it already).
        if (con.getAutoCommit()) {
            txObject.setMustRestoreAutoCommit(true);
            if (logger.isDebugEnabled()) {
                logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
            }
            con.setAutoCommit(false);
        }
        txObject.getConnectionHolder().setTransactionActive(true);

        int timeout = determineTimeout(definition);
        if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
            txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
        }

        // Bind the session holder to the thread.
        if (txObject.isNewConnectionHolder()) {
            TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
        }
    }

    catch (Throwable ex) {
        if (txObject.isNewConnectionHolder()) {
            DataSourceUtils.releaseConnection(con, this.dataSource);
            txObject.setConnectionHolder(null, false);
        }
        throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
    }
}

6.2 事务提交

事务提交并不是每次调用都提交底层的事务,而是只有初始事务才会提交。

private void processCommit(DefaultTransactionStatus status) throws TransactionException {
    try {
        boolean beforeCompletionInvoked = false;

        try {
            boolean unexpectedRollback = false;
            prepareForCommit(status);
            triggerBeforeCommit(status);
            triggerBeforeCompletion(status);
            beforeCompletionInvoked = true;

            if (status.hasSavepoint()) {
                if (status.isDebug()) {
                    logger.debug("Releasing transaction savepoint");
                }
                unexpectedRollback = status.isGlobalRollbackOnly();
                status.releaseHeldSavepoint();
            }
            else if (status.isNewTransaction()) {
                // 是初始事务才会提交
                unexpectedRollback = status.isGlobalRollbackOnly();
                doCommit(status);
            }
            else if (isFailEarlyOnGlobalRollbackOnly()) {
                unexpectedRollback = status.isGlobalRollbackOnly();
            }

            if (unexpectedRollback) {
                throw new UnexpectedRollbackException(
                        "Transaction silently rolled back because it has been marked as rollback-only");
            }
        }
        catch (UnexpectedRollbackException ex) {
            // can only be caused by doCommit
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
            throw ex;
        }
        catch (TransactionException ex) {
            // can only be caused by doCommit
            if (isRollbackOnCommitFailure()) {
                doRollbackOnCommitException(status, ex);
            }
            else {
                triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
            }
            throw ex;
        }
        catch (RuntimeException | Error ex) {
            if (!beforeCompletionInvoked) {
                triggerBeforeCompletion(status);
            }
            doRollbackOnCommitException(status, ex);
            throw ex;
        }

        try {
            triggerAfterCommit(status);
        } finally {
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
        }

    }
    finally {
        // 这个事务结束后,进行清理,然后恢复被挂起的事务
        cleanupAfterCompletion(status);
    }
}

private void cleanupAfterCompletion(DefaultTransactionStatus status) {
    status.setCompleted();
    if (status.isNewSynchronization()) {
        TransactionSynchronizationManager.clear();
    }
    if (status.isNewTransaction()) {
        doCleanupAfterCompletion(status.getTransaction());
    }
    if (status.getSuspendedResources() != null) {
        // 恢复被挂起的事务
        Object transaction = (status.hasTransaction() ? status.getTransaction() : null);
        resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources());
    }
}

triggerXXX 方法都是回调与这个事务相关的 TransactionSynchronization.xXXX 方法。

7. MyBatis 与 Spring 事务

MybatisAutoConfiguration 配置类定义了 Bean:SqlSessionFactorySqlSessionTemplate

在构建 SqlSessionFactory 时,SqlSessionFactoryBean.buildSqlSessionFactory 方法里会把 transactionFactory 初始化为 SpringManagedTransactionFactory 的实例,并把这个实例传给 Environment

@MapperScan 注解会导入 MapperScannerRegistrar 来扫描 Mapper 接口类,封装成 MapperFactoryBean,该 bean 注入了 SqlSessionTemplate 用于执行各种数据库操作。

MyBatis-Spring 模块还会把获得的 Session 封装在 SqlSessionHolder,以 SqlSessionFactory 为键,以 SqlSessionHolder 为值缓存在 TransactionSynchronizationManager 的 resource 里,方便快速获取。

为了与 Spring 的事务动作协调,还向 TransactionSynchronizationManager 注册了 SqlSessionSynchronization,以便在 suspend/resume/commit 等动作前后处理 SqlSessionHolder

数据库连接获取的链: 代理对象 -> SqlSessionTemplate -> SqlSessionInterceptor -> SpringManagedTransaction -> DataSourceUtils -> Connection

MyBatis 代理对象获取 SqlSession

SqlSessionInterceptor 调用 SqlSessionUtils.getSqlSession 方法来获取事务。

// SqlSessionUtils
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
    // 先从当前事务找
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
        return session;
    }

    // 当前没有 SqlSession,创建一个。
    session = sessionFactory.openSession(executorType);

    // 注册新的 SqlSession 到 TransactionSynchronizationManager
    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
}

private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
    SqlSessionHolder holder;
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
        Environment environment = sessionFactory.getConfiguration().getEnvironment();
        if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
            // 把当前的 SqlSessionHolder 绑定到 resource,与 ConnectionHolder 一致。
            holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
            TransactionSynchronizationManager.bindResource(sessionFactory, holder);

            // 注册事务同步钩子,以便于 Spring 事务协调。
            TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
            holder.setSynchronizedWithTransaction(true);
            holder.requested();
        } else {
            if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) {
            } else {
                throw new TransientDataAccessResourceException(
                    "SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");
            }
        }
    } else {    }
}

SpringManagedTransaction 获取数据库连接

Executor 最终要执行数据库操作时,必须调用 SqlSession.getConnection 方法获取连接,也就会调用到 SpringManagedTransaction.getConnection 方法,如下。

// SpringManagedTransaction
public Connection getConnection() throws SQLException {
    if (this.connection == null) {
        openConnection();
    }
    return this.connection;
}

private void openConnection() throws SQLException {
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional =   DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
}


// DataSourceUtils
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
    try {
        return doGetConnection(dataSource);
    }
    catch (SQLException ex) {
        throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
    }
}

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
    ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
    if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
        conHolder.requested();
        if (!conHolder.hasConnection()) {
            logger.debug("Fetching resumed JDBC Connection from DataSource");
            conHolder.setConnection(dataSource.getConnection());
        }
        return conHolder.getConnection();
    }
    // Else we either got no holder or an empty thread-bound holder here.

    Connection con = dataSource.getConnection();

    if (TransactionSynchronizationManager.isSynchronizationActive()) {
        ConnectionHolder holderToUse = conHolder;
        if (holderToUse == null) {
            holderToUse = new ConnectionHolder(con);
        }
        else {
            holderToUse.setConnection(con);
        }
        holderToUse.requested();
        TransactionSynchronizationManager.registerSynchronization(
                new ConnectionSynchronization(holderToUse, dataSource));
        holderToUse.setSynchronizedWithTransaction(true);
        if (holderToUse != conHolder) {
            TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
        }
    }

    return con;
}

欢迎关注我的微信公众号: coderbee笔记

]]>
https://coderbee.net/index.php/framework/20210726/2171/feed 0