设计一个Web版数据库管理平台(之六):SQL执行监听器 —— 日志、监控与审计

在之前的系列文章中,我们完成了平台的基础架构、多数据库分页、DDL/DML补全以及设计模式优化。随着平台服务的用户越来越多(目前已有2000+),一个现实的问题逐渐暴露出来:当用户反馈"我的查询很慢"或者"我明明执行了删除,数据却还在"时,我们没有任何日志可查。 今天这篇文章,我们就为平台引入SQL执行监听器机制,在不侵入核心执行链路的前提下,实现日志记录、慢SQL监控和操作审计。

一、为什么需要监听器?

先来看几个真实场景:

场景一:性能排查

用户反馈:"执行 SELECT * FROM orders WHERE create_time > '2025-01-01' 要等十几秒。"

你问:"你什么时候执行的?完整SQL是什么?参数是什么?"

用户:"就刚才啊,SQL就是我发的这个。"

你:......(没有日志,无从查起)

场景二:操作审计

运维发现某张核心表的数据被删了,需要追查是谁在什么时候执行的 DELETE。

你翻遍代码,发现 StatementHandler 里只有 stmt.executeUpdate(),没有任何记录。

你:......(没有审计日志,无法追溯)

场景三:慢SQL治理

平台需要定期统计哪些SQL执行超过3秒,以便优化或提醒用户。

你发现没有任何地方记录了SQL的执行耗时。

你:......(没有监控数据,无法治理)

这些问题的根源在于:我们的执行链路只关心"把SQL执行完",不关心"谁在什么时候执行了什么,执行了多久,结果如何"。

解决方案就是引入监听器(Listener) 机制:在SQL执行的关键节点插入回调,让外部代码有机会记录、监控和审计,而核心执行逻辑不需要任何改动。

二、设计思路

2.1 监听器应该放在哪一层?

我们的架构分层是:

SqlSession → Executor → StatementHandler → ResultSetHandler

  • SqlSession层:太靠上,只负责对外API,不感知具体执行细节。
  • StatementHandler层:太靠下,每个方法内部都在操作PreparedStatement,插入监听器会导致代码碎片化。
  • Executor层 :刚刚好。Executor是执行器的统一入口,所有SELECT、DML、DDL都经过这里,且它能拿到完整的SQL、参数、命令类型和执行结果。在这里包装监听器,覆盖面最广,侵入性最小。

2.2 监听器的触发时机

我们定义三个核心节点:

节点 触发时机 用途
beforeExecute SQL执行前 记录开始时间、SQL、参数、用户信息
afterExecute SQL执行成功后 记录耗时、影响行数、结果集大小
onError SQL执行抛出异常时 记录异常信息、耗时、SQL、参数

2.3 监听器链的设计

参考MyBatis的Interceptor链,我们采用责任链模式 :多个监听器按顺序执行,每个监听器都可以独立处理事件。通过 getOrder() 方法控制执行顺序,数值越小越先执行。

三、核心接口设计

3.1 ExecutionEvent(执行事件)

封装一次SQL执行的全部上下文信息。

java 复制代码
public class ExecutionEvent {
    /** 执行的SQL语句 */
    private String sql;
    /** SQL参数 */
    private Object[] parameters;
    /** SQL命令类型:SELECT / DML / DDL */
    private SqlCommandType commandType;
    /** 执行开始时间(毫秒) */
    private long startTime;
    /** 执行结束时间(毫秒) */
    private long endTime;
    /** 执行结果(SELECT返回List大小,DML/DDL返回影响行数) */
    private int resultCount;
    /** 执行异常(仅onError时有值) */
    private Throwable error;
    /** 当前会话标识(可用于关联用户) */
    private String sessionId;

    // 计算耗时
    public long getCostTime() {
        return endTime - startTime;
    }

    // getter/setter 省略
}

3.2 ExecutionListener(监听器接口)

java 复制代码
public interface ExecutionListener {
    /**
     * SQL执行前触发
     */
    default void beforeExecute(ExecutionEvent event) {}

    /**
     * SQL执行成功后触发
     */
    default void afterExecute(ExecutionEvent event) {}

    /**
     * SQL执行异常时触发
     */
    default void onError(ExecutionEvent event, Throwable error) {}

    /**
     * 监听器执行顺序,数值越小越先执行
     */
    default int getOrder() {
        return 0;
    }
}

3.3 ListenerRegistry(监听器注册表)

参考第五篇博客中 DialectRegistry 的设计思想,我们为监听器也设计一个注册表,统一管理监听器的注册、排序和获取。

java 复制代码
public class ListenerRegistry {
    private final List<ExecutionListener> listeners = new CopyOnWriteArrayList<>();

    public void addListener(ExecutionListener listener) {
        listeners.add(listener);
        // 按order排序
        listeners.sort(Comparator.comparingInt(ExecutionListener::getOrder));
    }

    public List<ExecutionListener> getListeners() {
        return Collections.unmodifiableList(listeners);
    }

    public boolean isEmpty() {
        return listeners.isEmpty();
    }
}

四、与现有架构集成

4.1 扩展Configuration

在 Configuration 中持有 ListenerRegistry,并提供注册监听器的入口。

java 复制代码
public class Configuration {
    private DataSource dataSource;
    private DatabaseType databaseType;
    private Dialect dialect;
    private final ListenerRegistry listenerRegistry = new ListenerRegistry();  // 新增

    // ... 原有构造方法 ...

    public void addListener(ExecutionListener listener) {
        listenerRegistry.addListener(listener);
    }

    public ListenerRegistry getListenerRegistry() {
        return listenerRegistry;
    }
}

4.2 改造SimpleExecutor

这是核心改动点。我们在 SimpleExecutor 的每个执行方法中,包装监听器的触发逻辑。

为了避免在每个方法中重复写监听器代码,我们抽取一个模板方法 executeWithListener:

java 复制代码
public class SimpleExecutor implements Executor {
    private final Configuration configuration;
    private final StatementHandler statementHandler;

    public SimpleExecutor(Configuration configuration) {
        this.configuration = configuration;
        this.statementHandler = new SimpleStatementHandler(configuration);
    }

    @Override
    public List<Map<String, Object>> query(String sql, Object... parameters) {
        ExecutionEvent event = createEvent(sql, parameters, SqlCommandType.SELECT);
        return executeWithListener(event, () -> {
            List<Map<String, Object>> result = statementHandler.query(sql, parameters);
            event.setResultCount(result.size());
            return result;
        });
    }

    @Override
    public int dml(String sql, Object... parameters) {
        if (!configuration.isDmlEnabled()) {
            throw new UnsupportedOperationException("DML operations are disabled");
        }
        ExecutionEvent event = createEvent(sql, parameters, SqlCommandType.DML);
        return executeWithListener(event, () -> {
            int count = statementHandler.dml(sql, parameters);
            event.setResultCount(count);
            return count;
        });
    }

    @Override
    public int ddl(String sql, Object... parameters) {
        if (!configuration.isDdlEnabled()) {
            throw new UnsupportedOperationException("DDL operations are disabled");
        }
        ExecutionEvent event = createEvent(sql, parameters, SqlCommandType.DDL);
        return executeWithListener(event, () -> {
            int count = statementHandler.ddl(sql, parameters);
            event.setResultCount(count);
            return count;
        });
    }

    // ==================== 核心模板方法 ====================
    private <T> T executeWithListener(ExecutionEvent event, SqlCallable<T> callable) {
        ListenerRegistry registry = configuration.getListenerRegistry();

        // 无监听器时直接执行,零开销
        if (registry.isEmpty()) {
            try {
                return callable.call();
            } catch (SQLException e) {
                throw new RuntimeException("SQL execution failed: " + event.getSql(), e);
            }
        }

        // 1. 前置通知
        event.setStartTime(System.currentTimeMillis());
        for (ExecutionListener listener : registry.getListeners()) {
            try {
                listener.beforeExecute(event);
            } catch (Exception e) {
                // 监听器异常不影响主流程,仅记录日志
                log.warn("Listener beforeExecute error", e);
            }
        }

        try {
            // 2. 执行SQL
            T result = callable.call();
            event.setEndTime(System.currentTimeMillis());

            // 3. 后置通知
            for (ExecutionListener listener : registry.getListeners()) {
                try {
                    listener.afterExecute(event);
                } catch (Exception e) {
                    log.warn("Listener afterExecute error", e);
                }
            }
            return result;
        } catch (Exception e) {
            event.setEndTime(System.currentTimeMillis());
            event.setError(e);

            // 4. 异常通知
            for (ExecutionListener listener : registry.getListeners()) {
                try {
                    listener.onError(event, e);
                } catch (Exception ex) {
                    log.warn("Listener onError error", ex);
                }
            }
            throw new RuntimeException("SQL execution failed: " + event.getSql(), e);
        }
    }

    private ExecutionEvent createEvent(String sql, Object[] parameters, SqlCommandType type) {
        ExecutionEvent event = new ExecutionEvent();
        event.setSql(sql);
        event.setParameters(parameters);
        event.setCommandType(type);
        return event;
    }

    @FunctionalInterface
    private interface SqlCallable<T> {
        T call() throws SQLException;
    }

    // ... 分页查询、close 等方法保持不变 ...
}

关键设计:监听器执行本身如果抛异常,绝不能影响主流程。因此每个监听器调用都包裹了try-catch,只记录告警日志。

4.3 类图

text 复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                            SqlSession                                      │
│  + selectList(sql, params): List<Map>                                     │
│  + executeDML(sql, params): int                                           │
│  + executeDDL(sql, params): int                                           │
└────────────────────────────────┬────────────────────────────────────────────┘
                                 │ 持有
                                 ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                         SimpleExecutor                                    │
│  - configuration: Configuration                                           │
│  - statementHandler: StatementHandler                                     │
│  + query(sql, params): List<Map>                                          │
│  + dml(sql, params): int                                                  │
│  + ddl(sql, params): int                                                  │
│  - executeWithListener(event, callable): T      ← 核心模板方法            │
└────────────────────────────────┬────────────────────────────────────────────┘
                                 │ 使用
                                 ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                       ListenerRegistry                                    │
│  - listeners: List<ExecutionListener>                                     │
│  + addListener(listener): void                                            │
│  + getListeners(): List<ExecutionListener>                                │
│  + isEmpty(): boolean                                                     │
└────────────────────────────────┬────────────────────────────────────────────┘
                                 │ 持有
                                 ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                      ExecutionListener (接口)                             │
│  + beforeExecute(event): void                                             │
│  + afterExecute(event): void                                              │
│  + onError(event, error): void                                            │
│  + getOrder(): int                                                        │
└────────────────────────────────┬────────────────────────────────────────────┘
                                 │ 实现
                                 ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                    LoggingExecutionListener                               │
│                    SlowSqlExecutionListener                               │
│                    AuditExecutionListener                                 │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Configuration                                          │
│  - listenerRegistry: ListenerRegistry                                     │
│  + addListener(listener): void                                            │
│  + getListenerRegistry(): ListenerRegistry                                │
└─────────────────────────────────────────────────────────────────────────────┘

五、内置监听器实现

5.1 日志监听器(LoggingExecutionListener)

记录每次SQL执行的完整信息,便于排查问题。

java 复制代码
public class LoggingExecutionListener implements ExecutionListener {
    private static final Logger log = LoggerFactory.getLogger(LoggingExecutionListener.class);

    @Override
    public void beforeExecute(ExecutionEvent event) {
        log.info("[SQL-BEFORE] type={}, sql={}, params={}",
                event.getCommandType(),
                event.getSql(),
                Arrays.toString(event.getParameters()));
    }

    @Override
    public void afterExecute(ExecutionEvent event) {
        log.info("[SQL-AFTER] type={}, cost={}ms, resultCount={}, sql={}",
                event.getCommandType(),
                event.getCostTime(),
                event.getResultCount(),
                event.getSql());
    }

    @Override
    public void onError(ExecutionEvent event, Throwable error) {
        log.error("[SQL-ERROR] type={}, cost={}ms, sql={}, params={}, error={}",
                event.getCommandType(),
                event.getCostTime(),
                event.getSql(),
                Arrays.toString(event.getParameters()),
                error.getMessage(), error);
    }

    @Override
    public int getOrder() {
        return 0;  // 最先执行
    }
}

5.2 慢SQL监听器(SlowSqlExecutionListener)

只关注执行时间超过阈值的SQL,用于性能监控和告警。

java 复制代码
public class SlowSqlExecutionListener implements ExecutionListener {
    private static final Logger log = LoggerFactory.getLogger(SlowSqlExecutionListener.class);
    private final long thresholdMs;

    public SlowSqlExecutionListener(long thresholdMs) {
        this.thresholdMs = thresholdMs;
    }

    @Override
    public void afterExecute(ExecutionEvent event) {
        if (event.getCostTime() > thresholdMs) {
            log.warn("[SLOW-SQL] cost={}ms (threshold={}ms), type={}, sql={}, params={}",
                    event.getCostTime(),
                    thresholdMs,
                    event.getCommandType(),
                    event.getSql(),
                    Arrays.toString(event.getParameters()));
            // 这里可以扩展:发送告警邮件、写入监控系统、推送钉钉/企微消息
        }
    }

    @Override
    public int getOrder() {
        return 10;
    }
}

5.3 审计监听器(AuditExecutionListener)

记录所有DML和DDL操作,用于安全审计。这是平台最需要的功能之一。

java 复制代码
public class AuditExecutionListener implements ExecutionListener {
    private static final Logger auditLog = LoggerFactory.getLogger("AUDIT");
    private final AuditRepository auditRepository;  // 审计日志存储(可写数据库或文件)

    public AuditExecutionListener(AuditRepository auditRepository) {
        this.auditRepository = auditRepository;
    }

    @Override
    public void afterExecute(ExecutionEvent event) {
        // 只审计DML和DDL,SELECT不记录(量太大)
        if (event.getCommandType() == SqlCommandType.SELECT) {
            return;
        }

        AuditRecord record = new AuditRecord();
        record.setSessionId(event.getSessionId());
        record.setCommandType(event.getCommandType().name());
        record.setSql(event.getSql());
        record.setParameters(Arrays.toString(event.getParameters()));
        record.setResultCount(event.getResultCount());
        record.setCostTime(event.getCostTime());
        record.setExecuteTime(new Date(event.getStartTime()));
        record.setSuccess(true);

        auditRepository.save(record);

        auditLog.info("[AUDIT] session={}, type={}, sql={}, resultCount={}, cost={}ms",
                event.getSessionId(),
                event.getCommandType(),
                event.getSql(),
                event.getResultCount(),
                event.getCostTime());
    }

    @Override
    public void onError(ExecutionEvent event, Throwable error) {
        if (event.getCommandType() == SqlCommandType.SELECT) {
            return;
        }

        AuditRecord record = new AuditRecord();
        record.setSessionId(event.getSessionId());
        record.setCommandType(event.getCommandType().name());
        record.setSql(event.getSql());
        record.setParameters(Arrays.toString(event.getParameters()));
        record.setCostTime(event.getCostTime());
        record.setExecuteTime(new Date(event.getStartTime()));
        record.setSuccess(false);
        record.setErrorMessage(error.getMessage());

        auditRepository.save(record);
    }

    @Override
    public int getOrder() {
        return 20;
    }
}

六、使用示例

6.1 注册监听器

java 复制代码
DataSource dataSource = ...;
Configuration configuration = new Configuration(dataSource);

// 注册日志监听器
configuration.addListener(new LoggingExecutionListener());

// 注册慢SQL监听器(阈值3秒)
configuration.addListener(new SlowSqlExecutionListener(3000));

// 注册审计监听器
configuration.addListener(new AuditExecutionListener(auditRepository));

try (SqlSession session = new SqlSession(configuration)) {
    // 执行查询
    session.selectList("SELECT * FROM user WHERE age > ?", 18);

    // 执行DML(会被审计)
    session.executeDML("UPDATE user SET age = ? WHERE id = ?", 26, 1);

    // 执行DDL(会被审计)
    session.executeDDL("CREATE TABLE test(id INT)");
}

6.2 日志输出示例

text 复制代码
[SQL-BEFORE] type=SELECT, sql=SELECT * FROM user WHERE age > ?, params=[18]
[SQL-AFTER] type=SELECT, cost=45ms, resultCount=23, sql=SELECT * FROM user WHERE age > ?

[SLOW-SQL] cost=5230ms (threshold=3000ms), type=SELECT, sql=SELECT * FROM orders WHERE ..., params=[]

[AUDIT] session=abc123, type=DML, sql=UPDATE user SET age = ? WHERE id = ?, resultCount=1, cost=12ms

七、总结

7.1 零开销设计

当 ListenerRegistry 为空时,executeWithListener 直接执行SQL,没有任何额外的性能开销。只有注册了监听器,才会有回调逻辑。这对于不需要监听器的场景非常友好。

7.2 监听器异常隔离

每个监听器的调用都包裹了 try-catch。如果某个监听器抛异常(比如写审计日志时数据库连接失败),不会影响主流程的SQL执行,只会记录一条告警日志。这是监听器机制必须遵守的原则。

7.3 与现有架构无缝集成

监听器没有改变 StatementHandler、ResultSetHandler 的任何代码,也没有改变 SqlSession 的对外API。它只是在 Executor 层做了一层"包装",完全符合开闭原则。

7.4 可扩展性

新增监听器只需实现 ExecutionListener 接口,注册到 Configuration 即可。

通过 getOrder() 控制多个监听器的执行顺序。

审计监听器可以对接任意存储(数据库、文件、Elasticsearch、消息队列)。

相关推荐
bksczm3 小时前
MySQL进阶篇之范式及E-R图
数据库·sql·mysql
步行cgn4 小时前
Spring 负责注入的注解详解
java·sql·spring
码农-00416 小时前
Springboot 集成 Ehcache操作数据库显示SQL语句设置
数据库·spring boot·sql
钱栈up16 小时前
全量 21 个失败、单跑全绿:泄漏进连接池的 SQL 变量
sql·单元测试·测试
旺仔不是程序员16 小时前
pg_trgm GIN 索引:PostgreSQL 正则、模糊与近似度查询的三合一加速器
数据库·后端·sql
考虑考虑16 小时前
SQL中的 CASE WHEN
数据库·后端·sql
旺仔不是程序员16 小时前
索引膨胀与重建:PostgreSQL REINDEX 生产实践
数据库·后端·sql
漂着的圆木6 天前
Agent 功能参与度:Copilot 怎么算
sql·数据分析·agent·githubcopilot·度量
旺仔不是程序员6 天前
LIMIT 1:PostgreSQL 只取一行的高效查询姿势
数据库·后端·sql