如何在多会话/事务环境中安全地将行插入包含带有(手动)增量键的主复合键的表中。
如何获取column_c
的最新增量值,LAST_INSERT_ID()
不会返回所需的值。
我调查了SELECT FOR UPDATE ... INSERT
和INSERT INTO SELECT
,但无法决定使用哪个。
在交易安全(锁定),隔离级别和性能方面实现这一目标的最佳方法是什么。
更新 - 另一个问题
假设两个事务/会话尝试同时插入相同的column_a,column_b对(示例1,1)。我该怎么做;
按顺序执行插入查询。第一个插入(事务1)应该产生1,1, 1 的复合键,第二个(事务2)1,1, 2 。我需要某种锁定机制
检索插入的column_c值。我可能需要利用变量?
表格定义
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;
答案 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
是手动递增的。将其拉出并跟踪另一个表中此列的最后一个值。
解决方案的步骤:
column_c
的最后一个值的表。我们称这个表为LastUsedIdTable
。 LastUsedIdTable
只包含三列:
ThreeColumnTable
); column_c
); 121
)。LastUsedIdTable
上执行事务的存储过程。此proc将读取您案例中column_c
使用的最后一个值。增加它。将递增的值返回给您。 (当然,您每次都可以进行直接查询。存储过程是更好的选择。)column_c
的返回值,因为存储过程已将LastUsedIdTable
行的值增加到ThreeColumnTable
行。任何想要向ThreeColumnTable
添加另一行的人都会调用存储过程并获取非冲突和递增的值,即使您尚未在table
中插入先前的值。 演示解决方案的可行性:
为了保持演示的一般性,请考虑在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 UNCOMMITTED
或SERIALIZABLE
,则临时值(带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(第二个参数是超时)。
点击此处SELECT
和INSERT
查询。
完成后使用DO RELEASE_LOCK('insert_table_name_aaa_bbb')
。
提交交易。
注意;在事务中再次调用GET_LOCK()
将释放先前设置的锁。此外,此命名锁定仅适用于此方案或使用确切名称的情况。锁只适用于名称!