标签搜索

使用SQL / JDBC模拟延迟

冰封一夏
2021-08-05 08:23:21 / 102 阅读 / 正在检测是否收录...

测试一些SQL查询时,我遇到了一个有趣的小技巧,可以在开发环境中模拟延迟。可能的用例包括验证后端延迟不会降低您的前端,或者您的UX仍然可以承受,等等。

该解决方案特定于PostgreSQL和Hibernate,尽管不必如此。此外,它使用存储的函数来解决VOIDPostgreSQL中函数的局限性,但是也可以以不同的方式解决,而无需存储任何对目录的辅助。

要删除Hibernate依赖项,您可以直接使用谓词直接使用该pg_sleep函数NULL,但不要这样尝试!

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

每行将睡1秒(!)。从解释计划中可以看出。让我们限制为3行以查看:

explain analyzeselect 1from t_bookwhere pg_sleep(1) is not nulllimit 3;

结果是:

限制(费用= 0.00..1.54行= 3宽度= 4)(实际时间= 1002.142..3005.374行= 3循环= 1)
   ->在t_book上进行Seq扫描(成本= 0.00..2.05行= 4宽度= 4)(实际时间= 1002.140..3005.366行= 3循环= 1)
过滤器:(pg_sleep('1':: double precision)IS NOT NULL)
 计划时间:2.036毫秒
 执行时间:3005.401 ms

如您所见,整个查询花了3秒钟的3行时间。实际上,这也是在Gunnar的推文示例中发生的情况,只是他正在按ID进行过滤,从而“帮助”隐藏了这种影响。

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

explain analyzeselect 1from t_bookwhere (select pg_sleep(1)) is not nulllimit 3;

现在的结果是:

限制(费用= 0.01..1.54行= 3宽度= 4)(实际时间= 1001.177..1001.178行= 3循环= 1)
   InitPlan 1(返回$ 0)
     ->结果(成本= 0.00..0.01行= 1宽度= 4)(实际时间= 1001.148..1001.148行= 1循环= 1)
   ->结果(成本= 0.00..2.04行= 4宽度= 4)(实际时间= 1001.175..1001.176行= 3循环= 1)
 一次性过滤器:($ 0不为空)
         ->在t_book上进行Seq扫描(成本= 0.00..2.04行= 4宽度= 0)(实际时间= 0.020..0.021行= 3循环= 1)
 计划时间:0.094毫秒
执行时间:1001.223 ms

现在,我们获得了所需的一次性过滤器。但是,我不太喜欢这种黑客攻击,因为它取决于优化,这是可选的,而不是正式的保证。这对于快速模拟延迟来说可能已经足够了,但不要轻易依赖生产中的这种优化。

似乎可以保证这种行为的另一种方法是使用MATERIALIZEDCTE:

explainwith s (x) as materialized (select pg_sleep(1))select *from t_bookwhere (select x from s) is not null;

我现在再次使用标量子查询,因为我不知何故需要访问CTE,并且我不想将其放在FROM子句中,因为这会影响我的投影。

该计划是:

结果(成本= 0.03..2.07行= 4宽度= 943)(实际时间= 1001.289..1001.292行= 4循环= 1)
一次性过滤器:($ 1不为空)
   CTE
     ->结果(...)(实际时间= 1001.262..1001.263行= 1循环= 1)
   InitPlan 2(返回$ 1)
     ->在s上进行CTE扫描(成本= 0.00..0.02行= 1宽度= 4)(实际时间= 1001.267..1001.268行= 1循环= 1)
   ->在t_book上进行Seq扫描(成本= 0.03..2.07行= 4宽度= 943)(实际时间= 0.015..0.016行= 4循环= 1)
 计划时间:0.049毫秒
执行时间:1001.308 ms

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

使用基于JDBC的方法

如果您的应用程序基于JDBC,则无需通过调整查询来模拟延迟。您可以简单地以一种或另一种方式代理JDBC。让我们看一下这个小程序:

try (Connection c1 = db.getConnection()) { // A Connection proxy that intercepts preparedStatement() callsConnection c2 = new DefaultConnection(c1) {@Overridepublic 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();}}

为简单起见,此方法使用jOOQDefaultConnection作为代理,可将所有方法方便地委派给某个委托连接,仅允许覆盖特定方法。该程序的输出为:

1个
2个
3
4
花费时间:1021ms

这模拟了prepareStatement()事件的延迟。显然,您将把代理提取到某些实用程序中,以免使您的代码混乱。您甚至可以代理开发中的所有查询,并仅基于系统属性启用睡眠调用。

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

try (Connection c = db.getConnection()) {long time = System.nanoTime(); // A PreparedStatement proxy intercepting executeQuery() callstry (PreparedStatement s = new DefaultPreparedStatement(c.prepareStatement("SELECT id FROM t_book")) {@Overridepublic 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。如果需要这些,只需将jOOQ Open Source Edition依赖项添加到所有基于JDBC的应用程序中,这些依赖项(这些类中没有RDBMS特定),包括Hibernate:

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

或者,只需复制类的源代码,DefaultConnection或者DefaultPreparedStatement如果您不需要整个依赖关系,或者您自己代理JDBC API。

基于jOOQ的解决方案

如果您已经在使用jOOQ(并且应该使用!),则可以通过实现更加轻松地实现此目的ExecuteListener。我们的程序现在看起来像这样:

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 |
+ ---- +
耗时:1025ms

区别在于,通过单个拦截回调,我们现在可以将此睡眠添加到所有类型的语句中,包括准备好的语句,静态语句,返回结果集的语句或更新计数,或两者。

4

评论

博主关闭了所有页面的评论