PAYATHON 2026

Java 中 GridDB 如何处理同一 row key 的并发写入并避免覆盖?

技术老齐

结论

GridDB 会协调同一 row key 的并发写入。put() 的语义是“存在则更新,不存在则插入”,因此即使两个线程都写入成功,同一个键通常也只保留一条记录,后生效的写入可能覆盖先前的内容。GridDB Java API 参考

具体处理方式取决于业务需求:

  • 如果两条数据都要保留,为每条事件分配唯一键,将 timestamp 保存为普通字段。
  • 如果必须拒绝重复 timestamp,使用手动事务,根据 put() 的返回值决定提交还是回滚。
  • 如果多个线程需要修改同一条已有记录,在手动事务中通过 get(key, true) 加锁读取,再修改并提交。

仅给 put() 加 Java 锁无法阻止重复键覆盖。即使顺序调用,两次相同键的 put() 仍然会更新同一行。

同键并发写入时会发生什么

GridDB 使用锁协调相互冲突的更新。一个事务持有目标数据的锁时,其他冲突写入需要等待锁释放,也可能因超时而失败。Collection 按行加锁,TimeSeries 的锁范围可能涉及同一内部存储单元中的其他行。GridDB 数据库功能说明

例如,两个线程都生成了键 T:

  1. 线程 A 插入键为 T 的记录并提交。
  2. 线程 B 随后执行 put(),发现键 T 已存在,于是更新该记录。

锁会让操作按事务规则执行,重复键本身不会自动触发错误。最终保留哪份数据,也不能根据线程的启动顺序判断。

System.currentTimeMillis() 返回毫秒时间值,同一毫秒内可以产生多条事件。将这个值包装成 Timestamp 也不能保证唯一。

需要保留全部数据:将事件标识作为 row key

如果同一时间发生的两条记录都有效,仅靠 timestamp 就无法区分它们。可以改用 Collection,将唯一事件标识作为 row key:

import com.toshiba.mwcloud.gs.RowKey;
import java.util.Date;

public class SensorEvent {
    @RowKey
    public String eventId;

    public Date timestamp;
    public double value;
}

写入时使用业务系统分配的唯一事件标识:

SensorEvent row = new SensorEvent();
row.eventId = eventId; // 由业务系统保证唯一
row.timestamp = new Date(System.currentTimeMillis());
row.value = value;

collection.put(row);

这里的 collection 应对应 Collection<String, SensorEvent>。Collection 支持字符串 row key;TimeSeries 使用时间类型的 row key,不能直接套用这个结构。GridDB Java API 参考

事件标识必须在所有写入实例之间保持唯一。如果采用“设备标识 + 序号”,需要保证同一设备的序号不会因并发、重启或多实例部署而重复。同一事件重试时应复用原标识,避免将一次事件存成多条记录。

如果必须使用 TimeSeries,可以按设备划分容器,减少不同设备之间的键冲突。同一设备在同一时间键下产生多条数据时,仍需重新设计存储方式。

需要拒绝重复键:在手动事务中判断并回滚

put() 的布尔返回值表示写入前是否存在相同键的行:

  • false:此前不存在,执行插入。
  • true:此前已存在,执行更新。

在自动提交模式下,拿到 true 时更新已经提交,事后检查无法阻止覆盖。

结合 put() 的返回值、写锁和 abort() 的回滚语义,可以用一个独立的手动事务实现“只接受新键”:先执行 put(),发现记录已存在就回滚,否则提交。这种做法组合了现有 API 的语义,API 中没有名为 putIfAbsent() 的接口。Container API 定义

以下示例适用于普通 Collection,假设 SensorData.timestamp 已声明为 row key,映射类型为 java.util.Date:

import com.toshiba.mwcloud.gs.Collection;
import com.toshiba.mwcloud.gs.GSException;
import java.util.Date;

// container 由当前线程独占,且没有其他待提交的操作。
// 返回 true 表示插入成功,false 表示重复键被拒绝。
// 调用后 container 保持手动提交模式。
public static boolean insertOnly(
        Collection<Date, SensorData> container,
        SensorData row) throws GSException {

    container.setAutoCommit(false);

    try {
        boolean existed = container.put(row);

        if (existed) {
            container.abort();
            return false;
        }

        container.commit();
        return true;
    } catch (GSException | RuntimeException e) {
        try {
            container.abort();
        } catch (GSException rollbackError) {
            e.addSuppressed(rollbackError);
        }
        throw e;
    }
}

遇到重复键时,更新会被回滚,原记录保留。返回 false 表示新记录没有保存,调用方需要决定是拒绝写入、记录冲突,还是改用新的唯一键写入。

这个事务只应包含当前这一次写入,否则 abort() 会同时撤销事务中的其他修改。所有需要遵守“重复键不得覆盖”规则的写入入口,都必须采用相同策略。

注意事项

普通的“先查再写”不能用作并发保护:

if (container.get(key) == null) {
    container.put(row);
}

两个线程可能都读到 null,随后分别写入同一个键。仅关闭自动提交,也不会让这段检查具有排他性。

对于已有记录的“读取后修改”,可以先调用 setAutoCommit(false),再通过 get(key, true) 加锁读取,修改行对象后调用 put() 和 commit()。更新锁要求使用手动提交模式,并持续到事务结束或超时;能否用它锁定尚不存在的键,需要另行确认,不能将其当作通用方案。GridDB Java API 参考

多线程应用还需要正确管理客户端对象。Java API 不保证单个 GridStore 的线程安全,可以让每个工作线程使用独立的 GridStore 及其关联的 Container。数据库端的并发控制不能替代客户端对象的线程隔离。GridStoreFactory 线程安全说明

启用压缩的 TimeSeries 对更新和时间顺序还有额外限制,不能直接套用上述 Collection 示例。锁超时或连接异常也不等于重复键错误。尤其是提交阶段连接中断时,可能需要先核实提交结果,再决定是否重试。

备注:内容仅供参考。