JDBC事务实测:操作日志写入失败时,让客户修改一起回滚

客户表已经更新,审计表因为字段过长插入失败。若业务要求"每次已提交修改必须有审计记录",继续保留客户更新就不符合这项要求。

下面用JDK 21和H2 2.3.232跑一个同库事务实验:让第二次写入真实报SQL异常,随后从另一个连接查询客户数据与审计条数。

事务边界包括两次写入

关闭自动提交后,客户更新与审计插入复用同一Connection,完成后commit;异常路径rollback并继续抛出。不能在更新方法、日志方法中各自新开一个连接,再认为它们属于同一个本地事务。

为了让读者能观察明确的SQL失败,审计event列故意只允许12个字符。这是实验故障开关,不是建议真实业务把日志字段设得这么短。

完整代码保存为AuditTx.java:

java 复制代码
import java.sql.*;

public class AuditTx {
    static final String URL = "jdbc:h2:mem:audit;DB_CLOSE_DELAY=-1";

    static void change(String name, String event) throws SQLException {
        try (Connection c = DriverManager.getConnection(URL)) {
            c.setAutoCommit(false);
            try {
                try (var p = c.prepareStatement("UPDATE customer SET name=? WHERE id=1")) {
                    p.setString(1, name);
                    if (p.executeUpdate() != 1) throw new SQLException("客户不存在");
                }
                // 两次写入复用同一个连接;审计表长度约束用来制造真实SQL失败。
                try (var p = c.prepareStatement("INSERT INTO audit(event) VALUES(?)")) {
                    p.setString(1, event);
                    p.executeUpdate();
                }
                c.commit();
            } catch (SQLException | RuntimeException e) {
                try { c.rollback(); } catch (SQLException rollbackError) {
                    e.addSuppressed(rollbackError);
                }
                throw e;
            }
        }
    }

    static void check(String expectedName, int expectedLogs) throws SQLException {
        // 用另一个连接读已提交结果,不能只断言方法调用次数。
        try (var c = DriverManager.getConnection(URL); var s = c.createStatement()) {
            try (var r = s.executeQuery("SELECT name FROM customer WHERE id=1")) {
                if (!r.next() || !r.getString(1).equals(expectedName)) {
                    throw new AssertionError("客户状态不符");
                }
            }
            try (var r = s.executeQuery("SELECT COUNT(*) FROM audit")) {
                r.next();
                if (r.getInt(1) != expectedLogs) throw new AssertionError("审计条数不符");
            }
        }
        System.out.println("name=" + expectedName + ", audit=" + expectedLogs);
    }

    public static void main(String[] args) throws SQLException {
        try (var c = DriverManager.getConnection(URL); var s = c.createStatement()) {
            s.execute("CREATE TABLE customer(id INT PRIMARY KEY, name VARCHAR(30))");
            s.execute("CREATE TABLE audit(event VARCHAR(12) NOT NULL)");
            s.execute("INSERT INTO customer VALUES(1, 'before')");
        }
        try {
            change("must_rollback", "event_too_long_for_column");
            throw new AssertionError("应触发SQL失败");
        } catch (SQLException expected) {
            System.out.println("auditWrite=failed");
        }
        check("before", 0);
        change("after", "NAME_CHANGED");
        check("after", 1);
    }
}

从Maven Central的H2 2.3.232目录取得h2-2.3.232.jar,与源码放在同一目录。

bash 复制代码
javac -encoding UTF-8 AuditTx.java

Windows运行:

powershell 复制代码
java -cp ".;h2-2.3.232.jar" AuditTx

Linux或macOS把类路径分隔符改成冒号:

bash 复制代码
java -cp ".:h2-2.3.232.jar" AuditTx

本地H2实验结果:

text 复制代码
auditWrite=failed
name=before, audit=0
name=after, audit=1

失败时,客户仍为before且审计0条;成功时客户为after且审计1条。check方法使用独立连接读取结果,断言的对象是提交后的数据库状态。

成功记录、失败尝试和运行日志要区分

这张audit表只表达成功提交的业务变更。失败请求若也要保留,应写成失败事件,选择单独的可靠记录方式;把失败记录塞在同一个被回滚的事务里,当然也会一起消失。

普通运行日志不一定要求与业务更新一起提交。是否因为记录失败而阻止业务,要由业务用途决定。本例有意选择"成功修改必须留下审计"的要求,不把它当成所有系统的默认答案。

程序也没有实现完整审计系统:事件没有操作者与字段差异,未覆盖认证、并发版本检查、查询权限和历史留存。它只验证同库两次写入的原子提交。接入Spring事务时还需确认实际事务边界、异常传播与数据源,不能直接用这里的结果证明代理配置正确。

如果写入的是外部日志服务或另一个数据库,本地Connection无法替它们统一提交。需要另行讨论事务内事件表、可靠发送以及消费方重复处理,这不是把insert换成HTTP调用就能保持的性质。

你们如何验证这类回滚:只检查抛异常,还是重新连接数据库查最终状态?

参考:Java 21 Connection文档。

相关推荐
OxYGC1 小时前
Spring 面经系列 · 第 1 篇:Bean 生命周期、单例语义、依赖注入与线程安全
java·安全·spring
小物学长1 小时前
Java 程序员的 IoT 协议入门(一):物联网平台到底在做什么
java
const_qiu1 小时前
第四章 07:4.7 本章小结
java·运维·服务器·人工智能
鶴哥只手遮天1 小时前
Spring AI 2.0.1 深度解析-第 22 章 Spring Boot 集成:自动配置、Properties 与条件装配
java·人工智能·开源
WJ.Polar2 小时前
final、单例类、枚举类、抽象类、接口
java·开发语言
江湖有缘2 小时前
Docker实战 | 使用Docker部署Imagor Studio图片编辑工具
java·docker·eureka
小羊没烦恼!2 小时前
C#综合揭秘——Entity Framework 并发处理详解
java·开发语言·前端·javascript·数据库·mysql·c#
刘 大 望2 小时前
Agent 的规划与执行范式:Plan-Execute-Replan 与 ReAct
java·python·ai·chatgpt·langchain·ai编程