You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring事务管理(Oracle)下插入fileId重复问题求助

解决并发插入时fileId重复的问题

嘿,我完全懂你遇到的这个麻烦——并发场景下用max(fileId)+1生成新ID,结果两个事务撞车生成了重复值,这是典型的竞态条件坑,咱们来拆解原因并给出靠谱的解决方案:

问题根源

当两个事务同时执行插入时,它们都会先执行SELECT max(fileId) FROM Table1 WHERE name="file2" AND path="/folder1"。在默认的事务隔离级别(比如MySQL的REPEATABLE READ)下,两个事务都只能读到已提交的最新值(也就是201),各自加1后都插入202,自然就重复了。本质是因为读取和插入不是原子操作,中间有空隙被其他事务钻了空子。

靠谱的解决方案

按推荐优先级给你列几种办法:

1. 用数据库行锁+辅助表(最推荐)

创建一个专门维护name+path组合版本号的辅助表,利用数据库的行锁机制确保同一时间只有一个事务能更新版本号:

-- 先建辅助表,用name+path做主键
CREATE TABLE file_version (
    name VARCHAR(255),
    path VARCHAR(255),
    current_max INT NOT NULL,
    PRIMARY KEY (name, path)
);

-- 初始化数据(对应你的file2/folder1)
INSERT INTO file_version (name, path, current_max) VALUES ("file2", "/folder1", 201);

然后在事务里先更新辅助表(这一步会加行锁),再拿更新后的值插入主表:

temp.execute(new TransactionCallbackWithoutResult() {
    @Override
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        // 1. 更新辅助表,自动加行锁,其他事务必须等当前事务提交
        jdbcTemplate.update(
            "UPDATE file_version SET current_max = current_max + 1 WHERE name = ? AND path = ?",
            "file2", "/folder1"
        );
        // 2. 获取更新后的最新版本号
        Integer newFileId = jdbcTemplate.queryForObject(
            "SELECT current_max FROM file_version WHERE name = ? AND path = ?",
            Integer.class,
            "file2", "/folder1"
        );
        // 3. 插入主表
        jdbcTemplate.update(
            "INSERT INTO Table1 (fileId, name, path) VALUES (?, ?, ?)",
            newFileId, "file2", "/folder1"
        );
    }
});

这种方式把版本号的更新变成原子操作,彻底避免竞态,性能也不错。

2. 给查询加排他锁(简单直接)

在查询max(fileId)的时候用FOR UPDATE加排他锁,这样其他事务在执行同样的查询时会被阻塞,直到当前事务提交:

INSERT INTO Table1 (fileId, name, path) 
VALUES (
    (SELECT max(fileId) + 1 FROM Table1 WHERE name="file2" AND path="/folder1" FOR UPDATE),
    "file2",
    "/folder1"
);

⚠️ 注意:一定要给name和path建联合索引!否则FOR UPDATE会锁全表,严重影响性能。

3. 提升事务隔离级别(不推荐高并发场景)

把事务隔离级别设为SERIALIZABLE(最高级别),数据库会对查询范围加锁,强制事务串行执行:

TransactionTemplate temp = new TransactionTemplate((PlatformTransactionManager) new DataSourceTransactionManager(dsource));
// 设置隔离级别为SERIALIZABLE
temp.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
temp.execute(new TransactionCallbackWithoutResult() {
    @Override
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        // 原来的插入逻辑
    }
});

这种方式简单但性能极差,高并发场景下会导致大量事务等待,只适合低并发的小系统。

4. 乐观锁+重试(适合低并发)

如果你的系统并发不高,可以用乐观锁的思路:插入时检查当前的max值是否和之前查询的一致,不一致就重试:

temp.execute(new TransactionCallbackWithoutResult() {
    @Override
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        // 1. 查询当前max值
        Integer currentMax = jdbcTemplate.queryForObject(
            "SELECT max(fileId) FROM Table1 WHERE name=? AND path=?",
            Integer.class,
            "file2", "/folder1"
        );
        int newFileId = currentMax + 1;
        // 2. 插入时带条件,确保max值没被修改
        int affectedRows = jdbcTemplate.update(
            "INSERT INTO Table1 (fileId, name, path) VALUES (?, ?, ?) " +
            "WHERE (SELECT max(fileId) FROM Table1 WHERE name=? AND path=?) = ?",
            newFileId, "file2", "/folder1",
            "file2", "/folder1", currentMax
        );
        // 3. 如果没插入成功,说明被并发修改了,回滚并重试(可以加循环重试逻辑)
        if (affectedRows == 0) {
            status.setRollbackOnly();
            // 这里可以加重试机制,比如最多重试3次
        }
    }
});

这种方式不需要加锁,但并发高的话会有很多重试失败的情况,体验不好。

总结

优先选辅助表+行锁或者SELECT ... FOR UPDATE的方案,既能保证正确性,又不会太影响性能。记得给name和path加联合索引,避免锁全表的坑!

内容的提问来源于stack exchange,提问作者user3458271

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:32:42