【问题标题】:Innodb autoincrement per custom column, bad concurrency每个自定义列的 Innodb 自动增量,错误的并发性
【发布时间】:2017-04-16 02:00:29
【问题描述】:

我们有表 MySql 5.5:

CREATE TABLE IF NOT EXISTS `invoices` (
  `id` varchar(36) NOT NULL,
  `client_id` smallint(4) NOT NULL,
  `invoice_number` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `client_id_2` (`client_id`,`invoice_number`),
  KEY `client_id` (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

我们像这样将数据插入到该表中:

INSERT INTO `invoices` ( `id` , `client_id` ,  `invoice_number`   )  
VALUES (
    UUID(),
    10 ,
    ( SELECT (MAX(`invoice_number`) +1)  as next_invoice_number FROM `invoices`  WHERE `client_id`  = 10 )
);

“10”是client_id 值。

它可以工作,但是它的并发性很差。我怎样才能有工作的解决方案,具有良好的并发性?

复合主键自动增量不是解决方案。我们需要每个 client_id 值的自动增量。复合主键自动增量在整个表中提供自动增量,而不是每个 client_id 列值。

【问题讨论】:

  • 并发性差是什么意思?您的意思是运行大量查询时速度很慢吗?您真的需要为每个用户提供后续发票编号吗?
  • 只有一个进程可以创建发票。它锁定了桌子。当另一个应用程序进程尝试插入同一个表时,它基本上会超时。有时可能有 5 个进程启动,等待表解锁。
  • 哦,酷,我看到有纯 sql 解决方案。它在这里的另一个问题中得到解决。会将其标记为 stackoverflow.com/questions/677542/auto-increment-by-group 的重复项
  • @dre-hh 复合主键自动增量不是解决方案。我们需要每个client_id 值的自动增量。复合主键自动增量在整个表中提供自动增量,而不是根据 client_id 列值。
  • 在我们的例子中主键不是自动增量,所以它不是重复的问题。

标签: mysql innodb


【解决方案1】:

不确定您在这里所说的糟糕的并发是什么意思。虽然每个 DML 操作都在隐式事务上运行,但您也可以使用 begin transaction ... end 构造将其包装在显式事务块中。

【讨论】:

  • 我们有! Bad Concurrency 意味着,当时只有一个进程(会话)可以访问invoices 表。
【解决方案2】:

看来mysql真的把整个表都锁在insert into ... select上了。

在伪代码中对我们有用(针对类似问题)的策略

function insert_user(){
  begin_transaction
  next_invoice_number = select_max_invoice_number + 1
  insert_user(next_invoice_number)
  end_transaction
}

function perform_insert(){
  try
   insert_user
  catch RecordNotUniqueError 
   perform_insert
  end
} 

这需要使用某种高级编程语言执行查询。 您基本上开始了一个交易,您首先执行查询以读取用户的下一个发票编号。之后,您使用 next_invoice_number 执行插入查询并希望获得最好的结果。如果有一个并发进程尝试为同一用户插入相同的发票编号,则其中一个进程的事务将失败。然后它可以尝试重复它。最后不应该对同一个发票号进行并发操作,每笔交易都会成功。

【讨论】:

  • 好的,这里的想法是捕获错误 #1062 - 键 'unique_columns' 的重复条目 'XXX-XXX' 并在应用程序级别重试插入另一个“自动递增”值。跨度>
  • 是的,没错。您在应用程序级别增加发票编号,在大多数情况下,这将起作用。对于并发错误,您只需重试
  • 有一件事情需要担心。我们也在使用交易。 BEGINCOMMIT -查询。 Error #1062 - Duplicate entry 'XXX-XXX' for key 'unique_columns' 可以中断交易吗?例如 Invoice-INSERT-query 之前的所有查询可能会被遗忘?此解决方案是否适用于交易?
  • 大多数 db lib 实现中的错误将中止整个事务。它是事务的原子性属性。这使您可以安全地重复失败事务的所有操作。
  • @hekep 关于这个问题的任何更新?请选择/投票一个适合你的答案
【解决方案3】:

我在这里看到了一些问题。

首先,对于每个发票登记,您都在扫描同一张表,以查找该特定客户应使用的下一个发票编号。

一个更快的解决方案是创建一个包含两列的表:Customer_ID(键)和最后一张发票 ID。

每当您需要注册新发票时,您只需从这个新表中获取并更新新发票编号,然后在插入中使用它。

其次,是什么让您认为您在示例中显示的操作不应该锁定表?

由于这种情况只是偶尔发生,最好的解决方案是尽量减少碰撞的可能性,这里介绍的方法肯定会做到这一点。

【讨论】:

  • 我们正在尝试这种方法。触发器应该为上次使用的 invoice_number 更新“新表”“invoices_history”...我们收到错误消息:Error: SQLSTATE[HY000]: General error: 1442 Can't update table 'invoices_history' in stored function/trigger because it is already used by statement which invoked this stored function/trigger. 我们真的想要万无一失的解决方案。
  • 我需要查看您的代码。我不太明白您为什么会收到该错误(我的意思是,MySQL 当然没有错,但您的代码可能)。请编辑您的问题并添加插入代码和触发器代码。
  • 获取新发票编号应该是您的应用程序中的一个明确步骤,不是触发器中发生的副作用。
  • @RickJames,这是一项商业决策,而不是技术决策。此外,我不太明白为什么在触发器中实现的机制比显式调用的机制更有价值(我的意思是,系统的正确文档,代码将涵盖这些方面)。在任何情况下,我建议作为触发器的一部分的相同代码都可以作为通用存储过程。由 OP 选择最适合他/她的东西。
【解决方案4】:

以这种方式重新格式化查询。这可能更简单、更快。

INSERT INTO `invoices` ( `id` , `client_id` ,  `invoice_number`   )  
    SELECT UUID(),
           10 ,
           MAX(`invoice_number`) +1
        FROM `invoices`
        WHERE `client_id`  = 10;

这本身就在交易中吗? autocommit=1?

或者这是一个更大的命令集的一部分?并且可能它们是导致错误的部分原因?

您随后将如何获取客户的 UUID 和/或 invoice_number?应用程序不需要显示它们和/或将它们存储在其他表格中吗?

【讨论】:

  • 我们有 begincommit ,因为发票有很多 invoice_rows 。插入invoice_row 的任何失败都应跳过整个事务。其他 sql 查询不会导致此事务出现问题。
猜你喜欢
  • 2016-04-06
  • 1970-01-01
  • 2013-02-17
  • 1970-01-01
  • 2014-08-07
  • 2012-08-11
相关资源
最近更新 更多