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文档。

相关推荐
秋天的怀念_David24 分钟前
算法一千零一夜 · 第二十八夜 注册
java·程序人生·算法
Java码农也是农27 分钟前
Spring 事务详解:@Transactional 如何生效、何时回滚、传播行为怎么选
java·spring boot·spring·事务
学长毕设1 小时前
基于SpringBoot的游乐园管理系统-附源码
java·spring boot·后端
SimonKing2 小时前
震惊:怎么回事,opencode一轮对话耗尽免费额度
java·后端·程序员
装不满的克莱因瓶2 小时前
【SpringAI Alibaba】ReactAgent响应SSE实时流式传输案例实战
java·人工智能·ai·sse·springai
杨凯凡2 小时前
Spring Boot 文件上传怎么做才安全?流式落盘、路径校验与数据库一致性实战
java·spring boot·文件上传·后端安全·数据库一致性
这个昵称不容易2 小时前
旅游管理系统的设计与实现(SpringBoot + MySQL + Vue 前后端分离)
java·springboot·毕设
Rain的Java大神之路2 小时前
LeetCode 37 解数独面试攻略:回溯 + 位掩码 + MRV,从暴力到毫秒级求解
java·后端·leetcode
天天被压力3 小时前
【A股分钟级高频数据完全手册 #11】券商实时行情数据接口:实时交易(券商源)接入与字段
java·人工智能·python
天天被压力3 小时前
【A股分钟级高频数据完全手册 #12】股票五档盘口数据接口:买卖五档实时盘口解析
java·人工智能·python