Grails 2 Gorm Hibernate 4:2个循环服务器导致jdbc4.MySQLIntegrityConstraintViolationException:关键字“PRIMARY”的重复条目“493”

bttbmeg0  于 2023-10-23  发布在  Go
关注(0)|答案(1)|浏览(143)

我正在做一个Grails 2.5版本的项目。我遇到了一个关于Gorm / Hibernate的问题。该项目部署在2台服务器上,每台服务器都将以循环方式获得调用。当我们点击我们的一个API时,它试图创建一个新的实体并在DB中持久化。
问题是,当我们调用这个API时,几乎所有其他请求都失败了,并显示错误消息:

com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '493' for key 'PRIMARY'

我尝试设置id值,但它不接受该值,而是尝试自己的值。我还尝试在子类中Mapid generator 'assigned'(不能接触基类,因为有其他子类,基类的生成器设置为identity)
我的问题是,我们如何清除hibernate认为是主键的下一个值的内容?我试过禁用二级缓存和查询缓存,但这不起作用。我们有没有一种方法可以编程地清除hibernate缓存,以便它进入DB查看那里的最大id值?不仅如此,如果你知道任何其他方法来解决它,那么请张贴他们太。

编辑

下面是基类和子类的Map:

基类

abstract class BaseEntity implements Serializable {
    static mapping = {
        tablePerHierarchy false  // avoid creating the base_domain table
        tablePerConcreteClass true
        id generator: 'identity'
        createdDate index: true  // index this column in each subclass table
        inUse index: true  // index this column in each subclass table
    }
}

子类

class ActualEntity1 extends BaseEntity {

    static mapping = {
        id generator: 'assigned'
    }
}

这些是gorm域类。我故意删除了类的其他字段。

9wbgstp7

9wbgstp71#

如果调用相同的API,则可能存在争用条件。所以,现在,我想做的第一件事就是不要像现在这样频繁地出现这个问题。
之后,如果发生这种情况,我们可以考虑在Redis中使用一个公共密钥,我们可以继续递增。
我正在尝试所有不涉及更改DB模式的解决方案
确实,以循环方式使用多个服务器可能会导致主键的下一个值出现竞争条件。当您使用identity生成器时,Hibernate依赖于底层数据库来生成ID,通常是通过自动递增列。
在多服务器环境中处理这种情况的最直接的方法是使用序列生成器,数据库将始终生成一个唯一的标识符,确保没有重复的键。
但是,这涉及到修改数据库模式。
您可以开始并尝试使用UUIDs (Universally Unique Identifiers) as primary keys。UUID是以这样一种方式生成的,即它们实际上保证是唯一的。
这不需要更改数据库模式,但需要修改域对象以支持UUID作为主键。

class ActualEntity1 extends BaseEntity {
    UUID id

    static mapping = {
        id generator: 'uuid'
    }
}

它确实具有几乎消除碰撞可能性的优点。
但是:UUID比常规的整数ID大,在某些情况下可能更难管理。
由于您已经尝试在ActualEntity1中使用assigned生成器,因此可以通过查询数据库以获得最大当前ID,然后向其添加一个ID,以编程方式确定下一个ID。
然而,这有潜在的竞争风险。您必须实现一些锁定机制来确保ID不重复。
您也可以使用外部服务:

  • 构建一个单独的微服务,只负责生成唯一的ID。每当需要持久化一个新实体时,首先调用此服务以检索一个新ID。这种方法的主要优点是,ID生成逻辑被抽象出来并集中起来。但是这引入了另一个故障点和额外的延迟。
  • 使用ZooKeeperetcd等分布式系统工具来维护全局唯一ID生成器。这些系统可以维护分布式锁,确保您始终获得唯一的ID,即使是跨多个服务器。但是,引入这样的新系统组件可能很复杂。

有关插图“Generating Distributed UUID's using Zookeeper",请参见Phani Kumar Yadavilli,2019年5月。
问题是,将DB中的这个键更改为UUID不仅是对该特定表的更改,而且对其他保留FK的表也是如此。*
所以,我想避免大量的数据迁移,这将到来。此外,我尝试了生成器分配,但只有当它的工作作为记录,我不会粘贴这个问题。
如果有一种方法可以在grails中实现,而不需要设置zookeeper / redis / DB更改,我将非常感激。
好的,考虑到这些限制,另一种方法可能是结合使用编程分配和同步。
这将涉及使用专用数据库表作为分布式锁机制。这将有助于确保在给定时间只有一台服务器可以分配ID,从而有效地消除竞争条件。
您需要:

  • 创建一个名为DistributedLock的表,其中只有一行。
  • 当服务器需要分配一个ID时,它将尝试获取该行上的锁。在SQL术语中,这将是一个SELECT ... FOR UPDATE语句,它试图在事务期间锁定行。
  • 如果服务器获得锁,它将从目标表中获取最大ID,递增它,将其分配给新实体,然后释放锁(提交事务)。
  • 如果另一台服务器试图在第一台服务器锁定该锁时获取该锁,它将等待第一台服务器释放该锁。

这种方法确实增加了一点锁定的开销,但可以确保在不更改主键类型或引入外部系统的情况下处理争用条件。
伪代码可能如下所示:

// Try to obtain a lock. If another server already has the lock, this will wait.
def lock = DistributedLock.executeQuery("SELECT d FROM DistributedLock d WHERE ... FOR UPDATE")

// Now that we have the lock, fetch the max ID
def maxId = ActualEntity1.executeQuery("SELECT max(id) FROM ActualEntity1")

// Assign the next ID
newEntity.id = maxId + 1

// Save the entity
newEntity.save()

// Commit the transaction, which releases the lock.

但是,如果系统的负载非常重,需要频繁创建实体,那么这可能不是最具可伸缩性的解决方案,您可能需要探索其他途径。

相关问题