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:
- 线程 A 插入键为
T的记录并提交。 - 线程 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 示例。锁超时或连接异常也不等于重复键错误。尤其是提交阶段连接中断时,可能需要先核实提交结果,再决定是否重试。
备注:内容仅供参考。
