问题 如何使用Spring和JDBCTemplate取消长时间运行的查询?


JDBC java.sql.Statement 班有一个 cancel() 方法。这可以在另一个线程中调用以取消当前运行的语句。

如何使用Spring实现这一目标?在运行查询时,我找不到获取对语句的引用的方法。我也找不到类似取消的方法。

这是一些示例代码。想象一下,这需要10秒才能执行,有时根据用户的要求,我想取消它:

    final int i = simpleJdbcTemplate.queryForInt("select max(gameid) from game");

我如何修改这个,所以我有一个参考 java.sql.Statement 目的?


7179
2018-06-28 10:06


起源



答案:


让我简化一下oxbow_lakes的答案:你可以使用 PreparedStatementCreator 获取对语句的访问权的查询方法的变体。

所以你的代码:

final int i = simpleJdbcTemplate.queryForInt("select max(gameid) from game");

应该变成:

final PreparedStatement[] stmt = new PreparedStatement[1];
final int i = (Integer)getJdbcTemplate().query(new PreparedStatementCreator() {
    public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
        stmt[0] = connection.prepareStatement("select max(gameid) from game");
        return stmt[0];
    }
}, new ResultSetExtractor() {
    public Object extractData(ResultSet resultSet) throws SQLException, DataAccessException {
        return resultSet.getString(1);
    }
});

现在取消你可以打电话

stmt[0].cancel()

您可能想要参考 stmt 在实际运行查询之前到某个其他线程,或者只是将其存储为成员变量。否则,你真的无法取消任何东西......


11
2017-07-29 15:11





你可以通过执行东西 JdbcTemplate 允许你传入的方法 PreparedStatementCreator。你总是可以用它来拦截调用(也许使用一个 Proxy)导致了一个 cancel 一些人在一个单独的线程上发生 cond 成为 true

public Results respondToUseRequest(Request req) {
    final AtomicBoolean cond = new AtomicBoolean(false);
    requestRegister.put(req, cond);
    return jdbcTemplate.query(new PreparedStatementCreator() {
             public PreparedStatement createPreparedStatement(Connection conn) {
               PreparedStatement stmt = conn.prepareStatement();
               return proxyPreparedStatement(stmt, cond);
             }
         }, 
         new ResultSetExtractor() { ... });
}        

这个 canceller 成功完成后可以取消;例如

private final static ScheduledExecutorService scheduler =
                 Executors.newSingleThreadedScheduledExecutor();  

PreparedStatement proxyPreparedStatement(final PreparedStatement s, AtomicBoolean cond) {
    //InvocationHandler delegates invocations to the underlying statement
    //but intercepts a query 
    InvocationHandler h = new InvocationHandler() {

        public Object invoke(Object proxy, Method m, Object[] args) {
            if (m.getName().equals("executeQuery") {
                Runnable cancel = new Runnable() {
                    public void run() { 
                        try {
                            synchronized (cond) {
                                while (!cond.get()) cond.wait();
                                s.cancel(); 
                            }
                        } catch (InterruptedException e) { }
                    } 
                }
                Future<?> f = scheduler.submit(cancel);
                try {
                    return m.invoke(s, args);
                } finally {
                    //cancel the canceller upon succesful completion
                    if (!f.isDone()) f.cancel(true); //will cause interrupt
                }
            }
            else {
                return m.invoke(s, args);
            }   
        }

    }

    return (PreparedStatement) Proxy.newProxyInstance(
                getClass().getClassLoader(), 
                new Class[]{PreparedStatement.class}, 
                h);

所以现在响应用户取消的代码如下所示:

cond.set(true);
synchronized (cond) { cond.notifyAll(); }

1
2018-06-28 12:17



一个有趣而奇怪的例子,并不完全确定如何应用于手头的问题。显然,10秒自动取消需要更换外部触发的东西。 - skaffman
为什么必须外部触发? OP没有提及任何有关用户定义的信息 - oxbow_lakes
OP在这里:实际上,我确实希望在响应用户操作时触发取消。 - Steve McLeod
然后用我的方法也很容易。而不是只是睡觉的取消器动作你可以运行某种类型的监听器,它在完成时唤醒取消器。我修改了我的答案 - oxbow_lakes
我所做的修改只是一个概念验证;显然你不想让你的线程池线程在现实中等待 - 你要添加某种UserTriggerListener - oxbow_lakes


答案:


让我简化一下oxbow_lakes的答案:你可以使用 PreparedStatementCreator 获取对语句的访问权的查询方法的变体。

所以你的代码:

final int i = simpleJdbcTemplate.queryForInt("select max(gameid) from game");

应该变成:

final PreparedStatement[] stmt = new PreparedStatement[1];
final int i = (Integer)getJdbcTemplate().query(new PreparedStatementCreator() {
    public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
        stmt[0] = connection.prepareStatement("select max(gameid) from game");
        return stmt[0];
    }
}, new ResultSetExtractor() {
    public Object extractData(ResultSet resultSet) throws SQLException, DataAccessException {
        return resultSet.getString(1);
    }
});

现在取消你可以打电话

stmt[0].cancel()

您可能想要参考 stmt 在实际运行查询之前到某个其他线程,或者只是将其存储为成员变量。否则,你真的无法取消任何东西......


11
2017-07-29 15:11





你可以通过执行东西 JdbcTemplate 允许你传入的方法 PreparedStatementCreator。你总是可以用它来拦截调用(也许使用一个 Proxy)导致了一个 cancel 一些人在一个单独的线程上发生 cond 成为 true

public Results respondToUseRequest(Request req) {
    final AtomicBoolean cond = new AtomicBoolean(false);
    requestRegister.put(req, cond);
    return jdbcTemplate.query(new PreparedStatementCreator() {
             public PreparedStatement createPreparedStatement(Connection conn) {
               PreparedStatement stmt = conn.prepareStatement();
               return proxyPreparedStatement(stmt, cond);
             }
         }, 
         new ResultSetExtractor() { ... });
}        

这个 canceller 成功完成后可以取消;例如

private final static ScheduledExecutorService scheduler =
                 Executors.newSingleThreadedScheduledExecutor();  

PreparedStatement proxyPreparedStatement(final PreparedStatement s, AtomicBoolean cond) {
    //InvocationHandler delegates invocations to the underlying statement
    //but intercepts a query 
    InvocationHandler h = new InvocationHandler() {

        public Object invoke(Object proxy, Method m, Object[] args) {
            if (m.getName().equals("executeQuery") {
                Runnable cancel = new Runnable() {
                    public void run() { 
                        try {
                            synchronized (cond) {
                                while (!cond.get()) cond.wait();
                                s.cancel(); 
                            }
                        } catch (InterruptedException e) { }
                    } 
                }
                Future<?> f = scheduler.submit(cancel);
                try {
                    return m.invoke(s, args);
                } finally {
                    //cancel the canceller upon succesful completion
                    if (!f.isDone()) f.cancel(true); //will cause interrupt
                }
            }
            else {
                return m.invoke(s, args);
            }   
        }

    }

    return (PreparedStatement) Proxy.newProxyInstance(
                getClass().getClassLoader(), 
                new Class[]{PreparedStatement.class}, 
                h);

所以现在响应用户取消的代码如下所示:

cond.set(true);
synchronized (cond) { cond.notifyAll(); }

1
2018-06-28 12:17



一个有趣而奇怪的例子,并不完全确定如何应用于手头的问题。显然,10秒自动取消需要更换外部触发的东西。 - skaffman
为什么必须外部触发? OP没有提及任何有关用户定义的信息 - oxbow_lakes
OP在这里:实际上,我确实希望在响应用户操作时触发取消。 - Steve McLeod
然后用我的方法也很容易。而不是只是睡觉的取消器动作你可以运行某种类型的监听器,它在完成时唤醒取消器。我修改了我的答案 - oxbow_lakes
我所做的修改只是一个概念验证;显然你不想让你的线程池线程在现实中等待 - 你要添加某种UserTriggerListener - oxbow_lakes


我假设你的意思是使用JdbcDaoTemplate和/或JdbcTemplate?如果是这样,这并没有真正帮助或阻碍您解决问题。

我假设你的用例是你在一个线程中执行DAO操作,另一个线程进来并想要取消第一个线程的操作。

你必须解决的第一个问题是,第二个线程如何知道取消哪一个?这是一个具有固定数量线程的GUI,还是具有多个线程的服务器?

一旦你解决了那个部分,你需要弄清楚如何取消第一个线程中的语句。一个简单的方法是将第一个线程的PreparedStatement存储在某个字段中(可能在一个简单的字段中,可能在一个线程ID到语句的映射中),允许第二个线程进入,检索statwment并调用cancel()在上面。

请记住,取决于您的JDBC驱动程序和数据库,cancel()可能会阻塞。另外,请确保你在这里认真考虑同步,你的线程是否会进入战斗。


0
2018-06-29 07:24



这是非常详细的信息,但我希望得到回答的问题是:如何通过JdbcTemplate运行查询时获得对语句的引用? - Steve McLeod
那么你应该这么说:)正如@oxbow所说,你可以使用PreparedStatementCreator的自定义实例来控制语句创建,然后将其传递给JdbcTemplate。 - skaffman


您可以注册类型的回调对象 StatementCallback 上 JdbcTemplate 将以当前活动语句作为参数执行。在此回调中,您可以取消该声明:

simpleJdbcTemplate.getJdbcOperations().execute(new StatementCallback() {

    @Override
    public Object doInStatement(final Statement statement) throws SQLException, DataAccessException {
        if (!statement.isClosed()) {
            statement.cancel();
        }

        return null;
    }
});

0
2017-11-26 10:40