我们如何更新深度克隆的实体?

时间:2020-11-08 12:35:19

标签: java spring multithreading hibernate jpa

描述

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

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

  • 标记游戏实体以进行更新
  • 将这些实体传递到另一个线程
  • 克隆那些实体
  • 在克隆的实体上调用“ session.update”
  • 在一分钟内重复

在简单的类上工作正常,但是关系方面存在很大的问题。

问题

当我深度克隆我的“块”实体时(请参见下文),其集合(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;
    }

}

问题

为什么冬眠总是在不检查孩子的情况下总是插入我的孩子?我该怎么做才能防止/解决此问题?

我尝试过的事情

  • 删除“ @SelectBeforeUpdate”注释
  • 通过“全部”或“合并”将级联添加到“ Chunk.InChunk”关系中
  • 运行“ session.merge”而不是“ session.update”,这将导致相同的重复输入异常,而忽略了其子级已被插入的事实。

他们都没有工作

免责声明

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

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

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

1 个答案:

答案 0 :(得分:1)

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

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

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

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

两者都需要使您进入#2的合同或接口(一般意义上,不是Java接口)。一旦发生更改,您的只读上下文中的任何提速技巧都必须刷新缓存或重新读取/合并更改。根据{{​​3}},您必须牺牲三者之一,具体取决于您的首选一致性策略。由于没有关于写入,实体和读取约束的数量的提示,因此我无法提出更详细的信息。

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

  • 仅持有接口的API模块可在以下两个模块之间强制使用例如统一的获取方法
  • 编写器模块,使用所有的Hibernate魔术来编写实体和某种侦听器模式,以便下一个模块可以自行注册
  • 具有Hibernate魔术的读取器模块,用于读取实体并将其提供给其他对象(通过REST,RCP等),将其自身注册到编写者,以对更改进行某种刷新。

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

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