首页 > 解决方案 > 我们如何更新深度克隆的实体?

问题描述

描述

我在一个小型 Java 游戏服务器上工作......为了在另一个线程中更新和保存游戏,我被迫深度克隆我的一些实体。否则会发生内部休眠异常:更新我的实体时出现“ConcurrentModificationException”

所以我的流程目前看起来像这样:

它适用于简单的类,但关系存在巨大问题。

问题

当我深度克隆我的“块”实体(见下文)时,它的集合(inChunk)也会被深度克隆。我使用该深度克隆的实体并将其传递给“session.update”。

在更新期间,块总是插入它的集合子。它从不更新它们。因为我每分钟重复一次这个更新过程(见上文),它会在第二个更新周期导致“重复条目”异常。


// Run the database operation for updating the entities async in a new thread, return updated entities once done
        return CompletableFuture.runAsync(() -> {

            var session = database.openSession();
            session.beginTransaction();

            try {

                // Save entities
                for (var entity: entities)
                    session.update(entity);

                session.flush();
                session.clear();

                session.getTransaction().commit();
            } catch (Exception e){

                var messageComposer = new ExceptionMessageComposer(e);
                GameExtension.getInstance().trace("Update : "+messageComposer.toString());
                session.getTransaction().rollback();
            }

            session.close();
        }).thenApply(v -> entities);

@Entity
@Table(name = "chunk", uniqueConstraints = {@UniqueConstraint(columnNames={"x", "y"})}, indexes = {@Index(columnList = "x,y")})
@Access(value = AccessType.FIELD)
@SelectBeforeUpdate(false)
public class Chunk extends HibernateComponent{

    public int x;
    public int y;
    public Date createdOn;

    @OneToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "chunk_identity", joinColumns = @JoinColumn(name = "identity_id"), inverseJoinColumns = @JoinColumn(name = "id"), inverseForeignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
    @Fetch(FetchMode.JOIN)
    @BatchSize(size = 50)
    public Set<Identity> inChunk = new LinkedHashSet<>();

    @Transient
    public Set<ChunkLoader> loadedBy = new LinkedHashSet<>();

    public Chunk() {}
    public Chunk(int x, int y, Date createdOn) {
        this.x = x;
        this.y = y;
        this.createdOn = createdOn;
    }
}


/**
 * Represents a ID of a {@link com.artemis.Entity} which is unique for each entity and mostly the database id
 */
@Entity
@Table(name = "identity")
@Access(AccessType.FIELD)
@SQLInsert(sql = "insert into identity(tag, typeID, id) values(?,?,?) ON DUPLICATE KEY UPDATE id = VALUES(id), tag = values(tag), typeID = values(typeID)")
@SelectBeforeUpdate(value = false)
public class Identity extends Component {

    @Id public long id;
    public String tag;
    public String typeID;

    public Identity() {}
    public Identity(long id, String tag, String typeID) {
        this.id = id;
        this.tag = tag;
        this.typeID = typeID;
    }

}

问题

为什么hibernate总是插入我的孩子而不检查那些是否已经插入?我能做些什么来防止/解决这个问题?

我尝试过的事情

他们都没有工作

免责声明

我需要克隆实体,否则会导致内部休眠异常,请参阅上面的链接。

我使用一个名为“DeppClone”的库https://github.com/kostaskougios/cloning在另一个线程中克隆我的实体。

如果需要更多信息,请写评论。这是一个复杂的问题,很难一概而论,但我希望我已经正确描述了它。

标签: javaspringmultithreadinghibernatejpa

解决方案


您应该考虑实现两个主要更改:

  1. 关注点分离(读写)
  2. 选择一致性原则

使用#1,您不必再处理 ConcurrentModificationException 了。只有一个组件或服务涉及修改您的实体。

第二个组件或服务仅读取单独上下文中的实体。所有实体都必须是不可变的,以免发生实施事故。

两者都需要一个合同或接口(一般意义上,不是 Java 接口),这会将您带到#2。一旦发生更改,您的只读上下文中的任何加速技巧都必须刷新缓存或重新读取/合并更改。根据CAP 定理,您必须牺牲三者之一,具体取决于您首选的一致性策略。由于没有关于写入次数、实体和读取约束的提示,我无法提出更详细的建议。

如果我现在必须实现它,那么至少有 3 个模块(Java 11/ Jigsaw):

  • API 模块仅持有接口以强制例如跨以下两个模块的统一获取方法
  • Writer-modules,用你所有的 Hibernate 魔法来编写实体和一种侦听器模式,所以下一个模块可以注册自己
  • 具有 Hibernate 魔法的阅读器模块,用于读取实体并将它们提供给其他人(通过 REST、RCP 等),将自身注册到编写器以在更改时进行某种刷新。

读取器模块还可以在启动时读取数据库并使用写入器产生的事件。这些事件将是某种命令,在阅读器模块的内存中发生变化,而不是从数据库中重新读取。因此,您可以通过简单的内存缓存和一些事件建模来完全放弃 Hibernate。通过使用 BlockingQueues 和 ConcurrentHashMap(作为缓存),这仍然可以在单个 JVM 中工作。简单的 JDBC 就足以引导您的模型。

如果通过深度复制和一些 Thread.start 很容易,其他人会这样做。由于有很多关于并发持久性的模型、策略和模式,我建议重构。


推荐阅读