likes
comments
collection
share

用SQL / JDBC模拟延时的详细指南

作者站长头像
站长
· 阅读数 34

我遇到了一个有趣的小技巧,在测试一些SQL查询时,在你的开发环境中模拟延迟。可能的用途包括验证后端延迟不会导致你的前端瘫痪,或者你的用户体验仍然可以承受,等等:

😴#PostgreSQL的pg_sleep()函数对于模拟慢速查询和评估其对服务的影响非常实用。

把它包装成一个返回常量值的假函数,你可以简单地把它添加到你的JPQL/HQL的WHERE子句中。#Hibernatehttps://t.co/4mmkl8rggQ pic.twitter.com/u4qFuGAlaN

- Gunnar Morling 🌍 (@gunnarmorling)2021年2月14

这个解决方案是针对PostgreSQL和Hibernate的,虽然不一定要这样。此外,它使用了一个存储函数来解决PostgreSQL中的VOID 函数的限制,但这也可以用不同的方法来解决,不需要存储任何辅助的目录。

为了消除对Hibernate的依赖,你可以直接使用NULL 谓词来使用pg_sleep 函数,但不要这样尝试

select 1
from t_book
-- Don't do this!
where pg_sleep(1) is not null;

这将使每行睡眠1秒(!)。从解释计划中可以看出。让我们限制在3行来看看:

explain analyze
select 1
from t_book
where pg_sleep(1) is not null
limit 3;

而结果是:

Limit  (cost=0.00..1.54 rows=3 width=4) (actual time=1002.142..3005.374 rows=3 loops=1)
   ->  Seq Scan on t_book  (cost=0.00..2.05 rows=4 width=4) (actual time=1002.140..3005.366 rows=3 loops=1)

正如你所看到的,整个查询对于3行花费了大约3秒。事实上,这也是Gunnar在推特上的例子中发生的情况,只是他是通过ID过滤的,这 "有助于 "隐藏这种影响。

我们可以使用Oracle所说的标量子查询缓存,事实上标量子查询可以合理地预期没有副作用(尽管pg_sleep ),这意味着一些RDBMS会在每次查询执行时缓存其结果:

explain analyze
select 1
from t_book
where (select pg_sleep(1)) is not null
limit 3;

现在的结果是:

Limit  (cost=0.01..1.54 rows=3 width=4) (actual time=1001.177..1001.178 rows=3 loops=1)
   InitPlan 1 (returns $0)
     ->  Result  (cost=0.00..0.01 rows=1 width=4) (actual time=1001.148..1001.148 rows=1 loops=1)
   ->  Result  (cost=0.00..2.04 rows=4 width=4) (actual time=1001.175..1001.176 rows=3 loops=1) 

我们现在得到了想要的一次性过滤器。然而,我不太喜欢这个黑客,因为它依赖于一个优化,而这个优化是可选的,不是一个正式的保证。这对于快速模拟延迟来说可能足够好了,但在生产中不要轻率地依赖这种优化。

另一种似乎能保证这种行为的方法是使用MATERIALIZED CTE:

explain
with s (x) as materialized (select pg_sleep(1))
select *
from t_book
where (select x from s) is not null;

我现在又使用了一个标量子查询,因为我需要访问CTE,而且我不想把它放在FROM 子句中,这样会影响我的预测。

计划是这样的:

Result  (cost=0.03..2.07 rows=4 width=943) (actual time=1001.289..1001.292 rows=4 loops=1)

同样,包含一个一次性的过滤器,这就是我们在这里想要的。

使用基于JDBC的方法

如果你的应用程序是基于JDBC的,你就不必通过调整查询来模拟延迟了。你可以简单地以某种方式代理JDBC。让我们看一下这个小程序:

try (Connection c1 = db.getConnection()) {

    // A Connection proxy that intercepts preparedStatement() calls
    Connection c2 = new DefaultConnection(c1) {
        @Override
        public PreparedStatement prepareStatement(String sql) 
        throws SQLException {
            sleep(1000L);
            return super.prepareStatement(sql);
        }
    };

    long time = System.nanoTime();
    String sql = "SELECT id FROM book";

    // This call now has a 1 second "latency"
    try (PreparedStatement s = c2.prepareStatement(sql);
        ResultSet rs = s.executeQuery()) {
        while (rs.next())
            System.out.println(rs.getInt(1));
    }

    System.out.println("Time taken: " + 
       (System.nanoTime() - time) / 1_000_000L + "ms");
}

在哪里?

public static void sleep(long time) {
    try {
        Thread.sleep(time);
    }
    catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

为了简单起见,这里使用了jOOQ的 [DefaultConnection](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/tools/jdbc/DefaultConnection.html)作为一个代理,方便地将所有的方法委托给一些委托连接,只允许重写特定的方法。该程序的输出是:

1
2
3
4
Time taken: 1021ms

这模拟了prepareStatement() 事件的延迟。很明显,为了不使你的代码杂乱无章,你会把代理提取到一些工具中。你甚至可以在开发中代理所有的查询,只根据系统属性来启用睡眠调用。

另外,我们也可以在executeQuery() 事件上进行模拟:

try (Connection c = db.getConnection()) {
    long time = System.nanoTime();

    // A PreparedStatement proxy intercepting executeQuery() calls
    try (PreparedStatement s = new DefaultPreparedStatement(
        c.prepareStatement("SELECT id FROM t_book")
    ) {
        @Override
        public ResultSet executeQuery() throws SQLException {
            sleep(1000L);
            return super.executeQuery();
        };
    };

        // This call now has a 1 second "latency"
        ResultSet rs = s.executeQuery()) {
        while (rs.next())
            System.out.println(rs.getInt(1));
    }

    System.out.println("Time taken: " +
        (System.nanoTime() - time) / 1_000_000L + "ms");
}

现在这是在使用jOOQ的方便类 [DefaultPreparedStatement](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/tools/jdbc/DefaultPreparedStatement.html).如果你需要这些,只需添加jOOQ开源版的依赖关系(这些类中没有任何RDBMS的特定内容),与任何基于JDBC的应用程序,包括Hibernate:

<dependency>
  <groupId>org.jooq</groupId>
  <artifactId>jooq</artifactId>
</dependency>

另外,如果你不需要整个依赖关系,只需复制类的来源DefaultConnectionDefaultPreparedStatement ,或者你只需自己代理JDBC API。

一个基于jOOQ的解决方案

如果你已经在使用jOOQ(你应该这样做!),你可以更容易地做到这一点,通过实现一个 [ExecuteListener](https://www.jooq.org/doc/latest/manual/sql-execution/execute-listeners/).我们的程序现在看起来就像这样:

try (Connection c = db.getConnection()) {
    DSLContext ctx = DSL.using(new DefaultConfiguration()
        .set(c)
        .set(new CallbackExecuteListener()
            .onExecuteStart(x -> sleep(1000L))
        )
    );

    long time = System.nanoTime();
    System.out.println(ctx.fetch("SELECT id FROM t_book"));
    System.out.println("Time taken: " +
        (System.nanoTime() - time) / 1_000_000L + "ms");
}

还是同样的结果:

+----+
|id  |
+----+
|1   |
|2   |
|3   |
|4   |
+----+
Time taken: 1025ms

不同的是,通过一个拦截回调,我们现在可以把这个睡眠添加到所有类型的语句中,包括准备好的语句、静态语句、返回结果集的语句,或更新计数,或两者都是。

转载自:https://juejin.cn/post/7126372823685136415
评论
请登录