手动增量的复合键

时间:2016-10-15 18:02:17

标签: mysql database

如何在多会话/事务环境中安全地将行插入包含带有(手动)增量键的主复合键的表中。

如何获取column_c的最新增量值,LAST_INSERT_ID()不会返回所需的值。

我调查了SELECT FOR UPDATE ... INSERTINSERT INTO SELECT,但无法决定使用哪个。

在交易安全(锁定),隔离级别和性能方面实现这一目标的最佳方法是什么。

更新 - 另一个问题

假设两个事务/会话尝试同时插入相同的column_a,column_b对(示例1,1)。我该怎么做;

  1. 按顺序执行插入查询。第一个插入(事务1)应该产生1,1, 1 的复合键,第二个(事务2)1,1, 2 。我需要某种锁定机制

  2. 检索插入的column_c值。我可能需要利用变量?

  3. 表格定义

    CREATE TABLE `table` (
            `column_a` int(11) unsigned NOT NULL,
            `column_b` int(11) unsigned NOT NULL,
            `column_c` int(11) unsigned NOT NULL,
            PRIMARY KEY (column_a, column_b, column_c)
     ) ENGINE=InnoDB;
    

    示例数据

    +----------+----------+----------+
    | column_a | column_b | column_c |
    +----------+----------+----------+
    |        1 |        1 |        1 |
    |        1 |        1 |        2 |
    |        1 |        1 |        3 |
    |        2 |        1 |        1 |
    |        2 |        1 |        2 |
    |        2 |        1 |        3 |
    +----------+----------+----------+
    

    将插入内容添加到选择查询

    INSERT INTO `table` (`column_a`, `column_b`, `column_c`)
    SELECT 2,1, IFNULL(MAX(`column_c`), 0) + 1 FROM `table` 
    WHERE `column_a` = 2 and `column_b` = 1;
    

6 个答案:

答案 0 :(得分:1)

如果数据完整性对您很重要,请考虑以下事项:

DROP TABLE IF EXISTS my_table;

CREATE TABLE my_table 
(id SERIAL PRIMARY KEY
,m CHAR(1) NOT NULL
,n CHAR(1) NOT NULL
) ENGINE=InnoDB;

INSERT INTO my_table (m,n) VALUES 
('a','b'),
('a','b'),
('a','c'),
('a','b'),
('j','p'),
('j','b'),
('j','p'),
('a','c');

SELECT x.*
     , COUNT(*) i
  FROM my_table x
  JOIN my_table y
    ON y.m = x.m
   AND y.n = x.n
   AND y.id <= x.id
 GROUP 
    BY x.id
 ORDER
    BY m,n,i;

+----+---+---+---+
| id | m | n | i |
+----+---+---+---+
|  1 | a | b | 1 |
|  2 | a | b | 2 |
|  4 | a | b | 3 |
|  3 | a | c | 1 |
|  8 | a | c | 2 |
|  6 | j | b | 1 |
|  5 | j | p | 1 |
|  7 | j | p | 2 |
+----+---+---+---+

此设计不假定删除-或仅在非常特殊的情况下删除

OTOH,如果数据完整性无关紧要,请考虑关系数据库是否适合您的需求。

答案 1 :(得分:0)

BEGIN;
SELECT @c := MAX(c) + 1
    FROM t
    WHERE a = ? AND b = ?
    FOR UPDATE;           -- important
if row found              -- in application code (or Stored Proc)
then
    INSERT INTO t (a,b,c)
        VALUES
        (?, ?, @c);
else
    INSERT INTO t (a,b,c)
        VALUES
        (?, ?, 1);
COMMIT;

希望FOR UPDATE将停止,直到它可以获得锁定和所需的c值。然后剩下的交易应该顺利进行。

我不认为transaction_isolation的设置很重要,但这值得研究。

答案 2 :(得分:0)

您可以使用存储过程:

我从未遇到过这种问题,如果我这样做,我会这样做:

CREATE DEFINER=`root`@`localhost` PROCEDURE `sp_insert_when_duplicate`(val1 int, val2 int, val3 int)
BEGIN

     -- catch duplicate insert error
     DECLARE EXIT HANDLER FOR 1062
     BEGIN
        -- we could recursively try to insert the same val1 and val2 but increasing val3 by 1
        call sp_insert_when_duplicate(val1,val2,val3+1);
     END;

     -- by default mysql recursive limit is 0, you could set as 10 or 100 as per your wish
    SET max_sp_recursion_depth=10;

     -- [Trying] to insert the values, if no duplicate this should continue and end the script.. if duplicate, above handler should catch and try to insert again with 1+ value for val3
    INSERT INTO `table` (`column_a`, `column_b`, `column_c`) values (val1,val2,val3);


END

用法是:

call sp_insert_when_duplicate(1,1,1);
call sp_insert_when_duplicate(1,1,1);
call sp_insert_when_duplicate(1,1,1);
call sp_insert_when_duplicate(2,1,1);
call sp_insert_when_duplicate(2,1,1);
call sp_insert_when_duplicate(2,2,1);
select * from `table`;

结果:

+----------+----------+----------+
| column_a | column_b | column_c |
+----------+----------+----------+
|        1 |        1 |        1 |
|        1 |        1 |        2 |
|        1 |        1 |        3 |
|        2 |        1 |        1 |
|        2 |        1 |        2 |
|        2 |        2 |        1 |
+----------+----------+----------+

同样适用于交易:

start transaction;
call sp_insert_when_duplicate(1,1,1);
call sp_insert_when_duplicate(1,1,1);
call sp_insert_when_duplicate(1,1,1);
call sp_insert_when_duplicate(2,1,1);
call sp_insert_when_duplicate(2,1,1);
call sp_insert_when_duplicate(2,2,1);
commit;

select * from `table`;


+----------+----------+----------+
| column_a | column_b | column_c |
+----------+----------+----------+
|        1 |        1 |        1 |
|        1 |        1 |        2 |
|        1 |        1 |        3 |
|        2 |        1 |        1 |
|        2 |        1 |        2 |
|        2 |        2 |        1 |
+----------+----------+----------+

但我没有尝试过并行交易!

答案 3 :(得分:0)

让我们将包含这3列的表格调用为ThreeColumnTable,以避免因您提供的名称而引起的混淆 - table

column_c是手动递增的。将其拉出并跟踪另一个表中此列的最后一个值。

解决方案的步骤:

  1. 创建一个存储用于column_c的最后一个值的表。我们称这个表为LastUsedIdTableLastUsedIdTable只包含三列:
    • 要手动增加其列的表的名称(示例:ThreeColumnTable);
    • 列本身的名称(示例:column_c);
    • 该列使用的最后一个值(示例:121)。
  2. 现在,为了便于使用,请编写一个在LastUsedIdTable上执行事务的存储过程。此proc将读取您案例中column_c使用的最后一个值。增加它。将递增的值返回给您。 (当然,您每次都可以进行直接查询。存储过程是更好的选择。)
  3. 现在为您冻结了column_c的返回值,因为存储过程已将LastUsedIdTable行的值增加到ThreeColumnTable行。任何想要向ThreeColumnTable添加另一行的人都会调用存储过程并获取非冲突和递增的值,即使您尚未在table中插入先前的值。
  4. 演示解决方案的可行性:

    为了保持演示的一般性,请考虑在ThreeColumnTable中同时插入 n 的请求。

    所有 n 请求必须先调用存储过程。由于存储过程使用LastUsedIdTable上的事务,因此一次只有1个请求将访问ThreeColumnTable的行,目前看起来像:

    +-----------------------------------+
    | ThreeColumnTable | column_c | 121 |
    +-----------------------------------+
    

    现在,第一个请求将锁定此行并获取122作为值,并将表格行中的值更新为122.到 n 请求时完成后,LastUsedIdTable的{​​{1}}行将如下所示:

    ThreeColumnTable

    现在,那些 n 请求已经开始在+-----------------------------------------+ | ThreeColumnTable | column_c | (121 + n) | +-----------------------------------------+ 中执行插入。但由于它们都有自己独特的ThreeColumnTable值,所以无论插入的顺序如何,它们的插入都不会发生冲突!您可以先插入值column_c,然后插入121 + n。事实上,您甚至不需要122元组是唯一的,因为column_a, column_b, column_c将始终是唯一的。

    此解决方案适用于并行请求和串行请求。只有非常小的(甚至可以忽略不计)击中性能,这将是您试图达到的交易安全性,隔离级别和性能的最佳点。

    重要提示: 使用column_c将所有此类主键列的最后一个值存储在数据库的所有表中。由于此表将只包含与要手动增加的所有数据库中的键数一样多的行,因此它将仅包含有限且固定数量的行。这将是快速和竞争条件免费。

答案 4 :(得分:0)

您可以使用column_c的虚拟值来锁定其他插入的组合(column_a, column_b),这尤其可确保即使该组合的行尚未存在也会被锁定。

start transaction;

set @a = 1;
set @b = 1;

insert into `table` (column_a, column_b, column_c)
values (@a,@b,0)
on duplicate key update column_c = 0; -- , column_d = null, ...

select max(column_c) + 1 into @c
from `table` where column_a = @a and column_b = @b;

update `table` set column_c = @c
where column_a = @a and column_b = @b and column_c = 0;

select @c;

commit;

第一个insert将锁定确切的组合(column_a, column_b),但不会阻止其他值,因此您可以在第一个事务运行时插入其他组合。

它适用于任何事务级别,因为select max()将是正确的(并且正确的间隙锁定),即使另一个会话将更新相同组合的行(除了带有column_c = 0的锁定行)第一次插入后;但是,如果您使用READ UNCOMMITTEDSERIALIZABLE,则临时值(带column_c = 0)当然会很快显示给其他会话。如果这让您感到困扰,请使用更高级别(例如保持默认值)。

@c会根据需要包含您的上一个ID,@a@b将被您的值替换,并且不需要变量。

如果将该代码放在存储过程中,请记住MySQL不支持嵌套事务,因此在过程中启动(特别是提交)事务总是有风险的,所以你应该这样做程序之外的交易处理。

on duplicate key只是为了让它成为傻瓜证明。如果一切正常,您就不需要它,但即使有人(手动)添加了column_c = 0的无效行并将其留在那里,它也能确保代码正常工作。或者,如果您将代码放入过程中,有人调用它而不先启动事务,而另一个会话同时插入该组合,则可能导致重复键错误(对于update)因此可能会导致column_c = 0的剩余行(您当然可以在过程中的异常处理程序中删除)。如果发生这种情况,可能会有兴趣(通过哭泣的用户)获取信息,因此您可能想要删除on duplicate key(至少用于测试)。

答案 5 :(得分:0)

选项1

这应该是原子的,并且似乎插入了正确的值:

INSERT 
  INTO table_name (column_a, column_b, column_c)
SELECT 
    :column_a,
    :column_b,
    COALESCE((
       SELECT MAX(column_c) 
         FROM table_name
        WHERE column_a = :column_a
          AND column_b = :column_b          
    ),0) + 1;

:column_a:column_b是您的新值。

不幸的是,如果您想使用LAST_INSERT_ID()功能,它只会使用AUTO_INCREMENT值。

您可以添加代理主键:

CREATE TABLE `table_name` (
     `id` int(11) unsigned NOT NULL AUTO_INCREMENT
     `column_a` int(11) unsigned NOT NULL,
     `column_b` int(11) unsigned NOT NULL,
     `column_c` int(11) unsigned NOT NULL,
     PRIMARY KEY (`id`),
     UNIQUE INDEX (`column_a`, `column_b`, `column_c`)
) ENGINE=InnoDB;

并在上面运行相同的INSERT查询。现在,您的LAST_INSERT_ID()将引用新插入的行。

如果您确实添加了代理键,则可能需要重新评估是否仍需要column_c

选项2

您也可以通过在单个连接/事务/过程中使用用户变量来绕过添加代理键:

INSERT 
  INTO table_name (column_a, column_b, column_c)
SELECT 
    :column_a,
    :column_b,
    @c := COALESCE((
       SELECT MAX(column_c) 
         FROM table_name
        WHERE column_a = :column_a
          AND column_b = :column_b          
    ),0) + 1;

SELECT @c;

选项3

如果这是您在表中插入或更新这些列的唯一位置,则可以执行一些基于名称的手动锁定。在单个事务中使用GET_LOCK()模拟记录锁。

开始交易。

为要锁定的行选择特定名称。例如'insert_table_name_aaa_bbb'。其中'aaa'是column_a的值,'bbb'是column_b的值。

调用SELECT GET_LOCK('insert_table_name_aaa_bbb',30)锁定名称'insert_table_name_aaa_bbb' ..如果名称可用,它将返回1并设置锁定;如果30秒后锁定不可用,则返回0(第二个参数是超时)。

点击此处SELECTINSERT查询。

完成后使用DO RELEASE_LOCK('insert_table_name_aaa_bbb')

提交交易。

注意;在事务中再次调用GET_LOCK()将释放先前设置的锁。此外,此命名锁定仅适用于此方案或使用确切名称的情况。锁只适用于名称!

GET_LOCK() docs