用SQL / JDBC模拟延时的详细指南
我遇到了一个有趣的小技巧,在测试一些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>
另外,如果你不需要整个依赖关系,只需复制类的来源DefaultConnection
或DefaultPreparedStatement
,或者你只需自己代理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