MySQL:如何使用InnoDB锁定来增加非主要值,同时防止该值重复?

问题描述 投票:2回答:1

我想在两个查询同时检查非主键的MAX()的情况下增加非主键的值,而不能创建重复项。我发现完成此操作的一个好方法是使用InnoDB的锁定机制。

我有一张这样的桌子:

tbl
-----------------------
groupID | msgNum | msg
-----------------------
1       | 1      | text
1       | 2      | text
1       | 3      | text
2       | 1      | text
2       | 2      | text

我想插入新行并为该行增加msgNum。我担心的是,如果我使用MAX(msgNum)计算下一个数字,那么两个几乎同时执行的查询会同时计算MAX(msgNum),然后两次插入相同的msgNum。因此,我想锁定表,但仅专门锁定最小的可能性,这将是锁定计算特定MAX(msgNum)groupID,同时还锁定插入指定groupID的新行的功能。理想情况下,我想避免锁定表中的读数。

可能的解决方案是这个(SQL Fiddle):

START TRANSACTION;
SELECT * FROM tbl WHERE groupID=1 FOR UPDATE;
INSERT INTO tbl
(groupID,msgNum,msg) VALUES
(1,(SELECT IFNULL(MAX(msgNum)+1,0) FROM (SELECT * FROM tbl WHERE groupID=1) AS a),"text");
COMMIT;

我认为该解决方案应该有效,但是我不确定,在测试之后,我遇到了一个问题。此外,这是一个很难测试的概念,我想确定一下,以便更好地了解。我不确定的是锁是否会阻止INSERT查询的开始,从而阻止其对MAX(msgNum)的计算。

我确实进行了初始测试:

package main

import (
    "database/sql"
    "fmt"

    _ "github.com/go-sql-driver/mysql"
)

func runTest(sqlCon *sql.DB) {
    _, err := sqlCon.Exec(
        "START TRANSACTION",
    )
    if err != nil {
        fmt.Println(err.Error())
    }
    _, err = sqlCon.Exec(
        "SELECT * FROM tbl WHERE groupID=1 FOR UPDATE",
    )
    if err != nil {
        fmt.Println(err.Error())
    }
    _, err = sqlCon.Exec(
        "INSERT INTO tbl " +
            "(groupID,msgNum,msg) VALUES " +
            "(1,(SELECT IFNULL(MAX(msgNum)+1,0) FROM (SELECT * FROM tbl WHERE groupID=1) AS a),\"text\")",
    )
    if err != nil {
        fmt.Println(err.Error())
    }
    _, err = sqlCon.Exec(
        "COMMIT",
    )
    if err != nil {
        fmt.Println(err.Error())
    }
}

func main() {
    sqlCon, err := sql.Open("mysql", "user1:password@tcp(127.0.0.1:3306)/Tests")
    if err != nil {
        panic(err.Error())
    }
    sqlCon2, err := sql.Open("mysql", "user1:password@tcp(127.0.0.1:3306)/Tests")
    if err != nil {
        panic(err.Error())
    }

    for i := 0; i < 40; i++ {
        fmt.Println(i)
        go runTest(sqlCon)
        go runTest(sqlCon2)
    }

}

我插入了7至52行,没有重复,但是测试没有完成(有80行),说Error 1213: Deadlock found when trying to get lock; try restarting transaction

$ go run main.go
0
1
2
3
4
5
6
7
8
9
10
11
12
13
Error 1213: Deadlock found when trying to get lock; try restarting transaction
Error 1213: Deadlock found when trying to get lock; try restarting transaction
Error 1213: Deadlock found when trying to get lock; try restarting transaction
Error 1213: Deadlock found when trying to get lock; try restarting transaction
Error 1213: Deadlock found when trying to get lock; try restarting transaction
Error 1213: Deadlock found when trying to get lock; try restarting transaction
Error 1213: Deadlock found when trying to get lock; try restarting transaction
Error 1213: Deadlock found when trying to get lock; try restarting transaction
Error 1213: Deadlock found when trying to get lock; try restarting transaction
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
mysql select locking innodb sql-insert
1个回答
1
投票

我认为您不需要事务和显式锁定即可正常运行。我建议您使用一个查询,即select s最后一个值,将其递增,并立即inserts一个新行。

我将查询表达为insert into ... select语句:

insert into tbl(groupID, msgNum, msg)
select 1, coalesce(max(msgNum), 0) + 1, 'text'
from tbl
where groupID = 1

您将在启用自动提交的情况下运行此查询。通过在后台隐藏insert队列,数据库应该能够为您处理并发,因此不会产生死锁。


更笼统地说:我实际上不会尝试存储msgNum。这实际上是派生的信息,可以在需要时即时计算。您可以在表上仅具有一个自动递增的主键,并可以使用窗口函数(在MySQL 8.0中提供)来计算msgNum的视图]

create table tbl (
    id int auto_increment primary key,
    groupID int
    msg varchar(50)
);

create view myview as
select 
    groupID,
    row_number() over(partition by groupID order by id) msgNum,
    msg
from tbl

然后您可以使用常规的insert语句:

    insert into tbl(groupID, msg) values(1, 'text');

优势:

  • 数据库在后台为您管理主键

  • 您的insert查询非常简单和高效(它不需要像其他解决方案一样扫描表)]]

  • 视图为您提供了数据的最新视角,包括派生信息(msgNum),维护成本为0

© www.soinside.com 2019 - 2024. All rights reserved.