I would like to increment a non-primary key value without the possibility of creating duplicates in the event that two queries check the MAX() of the non-primary key at the same time. I've found that a good way to accomplish this would be to use InnoDB's locking mechanism.
I have a table like this:
tbl
-----------------------
groupID | msgNum | msg
-----------------------
1 | 1 | text
1 | 2 | text
1 | 3 | text
2 | 1 | text
2 | 2 | text
I would like to insert a new row and to increment the msgNum for that row. What I'm worried about is that if I use MAX(msgNum) to calculate the next number, that two near-simultaneous queries would calculate MAX(msgNum) at the same time and then insert the same msgNum twice. So, I would like to lock the table, but only specifically lock the minimum possible, which would be to lock calculating the MAX(msgNum) for a specific groupID while also locking the ability to insert a new row of the specified groupID. Ideally, I would like to avoid locking reading from the table.
A possible solution would be this (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;
I think this solution should work, but I am not sure and after testing it I ran into an issue. Additionally, it's a difficult concept to test and I'd like to be sure so it would be better to know. What I'm not sure of is if the lock would prevent the INSERT query from beginning and thus preventing its calculation of MAX(msgNum).
I did perform an initial test:
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)
}
}
I got between 7 and 52 rows inserted, no duplicates, but the tests did not finish (with 80 rows), saying 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