事务限制

本章将简单介绍 TiDB 中的事务限制。

隔离级别

TiDB 支持的隔离级别是 RC(Read Committed)与 SI(Snapshot Isolation),其中 SI 与 RR(Repeatable Read)隔离级别基本等价。

隔离级别

SI 可以克服幻读

TiDB 的 SI 隔离级别可以克服幻读异常 (Phantom Reads),但 ANSI/ISO SQL 标准中的 RR 不能。

所谓幻读是指:事务 A 首先根据条件查询得到 n 条记录,然后事务 B 改变了这 n 条记录之外的 m 条记录或者增添了 m 条符合事务 A 查询条件的记录,导致事务 A 再次发起请求时发现有 n+m 条符合条件记录,就产生了幻读。

例如:系统管理员 A 将数据库中所有学生的成绩从具体分数改为 ABCDE 等级,但是系统管理员 B 就在这个时候插入了一条具体分数的记录,当系统管理员 A 改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

SI 不能克服写偏斜

TiDB 的 SI 隔离级别不能克服写偏斜异常(Write Skew),需要使用 Select for update 语法来克服写偏斜异常。

写偏斜异常是指两个并发的事务读取了不同但相关的记录,接着这两个事务各自更新了自己读到的数据,并最终都提交了事务,如果这些相关的记录之间存在着不能被多个事务并发修改的约束,那么最终结果将是违反约束的。

举个例子,假设你正在为医院写一个医生轮班管理程序。医院通常会同时要求几位医生待命,但底线是至少有一位医生在待命。医生可以放弃他们的班次(例如,如果他们自己生病了),只要至少有一个同事在这一班中继续工作。

现在出现这样一种情况,Alice 和 Bob 是两位值班医生。两人都感到不适,所以他们都决定请假。不幸的是,他们恰好在同一时间点击按钮下班。下面用程序来模拟一下这个过程。

  • Java
  • Golang

Java 程序示例如下:

1. package com.pingcap.txn.write.skew;

3. import com.zaxxer.hikari.HikariDataSource;

5. import java.sql.Connection;
6. import java.sql.PreparedStatement;
7. import java.sql.ResultSet;
8. import java.sql.SQLException;
9. import java.util.concurrent.CountDownLatch;
10. import java.util.concurrent.ExecutorService;
11. import java.util.concurrent.Executors;
12. import java.util.concurrent.Semaphore;

14. public class EffectWriteSkew {
15. public static void main(String[] args) throws SQLException, InterruptedException {
16. HikariDataSource ds = new HikariDataSource();
17. ds.setJdbcUrl("jdbc:mysql://localhost:4000/test?useServerPrepStmts=true&cachePrepStmts=true");
18. ds.setUsername("root");

20. // prepare data
21. Connection connection = ds.getConnection();
22. createDoctorTable(connection);
23. createDoctor(connection, 1, "Alice", true, 123);
24. createDoctor(connection, 2, "Bob", true, 123);
25. createDoctor(connection, 3, "Carol", false, 123);

27. Semaphore txn1Pass = new Semaphore(0);
28. CountDownLatch countDownLatch = new CountDownLatch(2);
29. ExecutorService threadPool = Executors.newFixedThreadPool(2);

31. threadPool.execute(() -> {
32. askForLeave(ds, txn1Pass, 1, 1);
33. countDownLatch.countDown();
34. });

36. threadPool.execute(() -> {
37. askForLeave(ds, txn1Pass, 2, 2);
38. countDownLatch.countDown();
39. });

41. countDownLatch.await();
42. }

44. public static void createDoctorTable(Connection connection) throws SQLException {
45. connection.createStatement().executeUpdate("CREATE TABLE `doctors` (" +
46. "    `id` int(11) NOT NULL," +
47. "    `name` varchar(255) DEFAULT NULL," +
48. "    `on_call` tinyint(1) DEFAULT NULL," +
49. "    `shift_id` int(11) DEFAULT NULL," +
50. "    PRIMARY KEY (`id`)," +
51. "    KEY `idx_shift_id` (`shift_id`)" +
52. "  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin");
53. }

55. public static void createDoctor(Connection connection, Integer id, String name, Boolean onCall, Integer shiftID) throws SQLException {
56. PreparedStatement insert = connection.prepareStatement(
57. "INSERT INTO `doctors` (`id`, `name`, `on_call`, `shift_id`) VALUES (?, ?, ?, ?)");
58. insert.setInt(1, id);
59. insert.setString(2, name);
60. insert.setBoolean(3, onCall);
61. insert.setInt(4, shiftID);
62. insert.executeUpdate();
63. }

65. public static void askForLeave(HikariDataSource ds, Semaphore txn1Pass, Integer txnID, Integer doctorID) {
66. try(Connection connection = ds.getConnection()) {
67. try {
68. connection.setAutoCommit(false);

70. String comment = txnID == 2 ? "    " : "" + "/* txn #{txn_id} */ ";
71. connection.createStatement().executeUpdate(comment + "BEGIN");

73. // Txn 1 should be waiting until txn 2 is done.
74. if (txnID == 1) {
75. txn1Pass.acquire();
76. }

78. PreparedStatement currentOnCallQuery = connection.prepareStatement(comment +
79. "SELECT COUNT(*) AS `count` FROM `doctors` WHERE `on_call` = ? AND `shift_id` = ?");
80. currentOnCallQuery.setBoolean(1, true);
81. currentOnCallQuery.setInt(2, 123);
82. ResultSet res = currentOnCallQuery.executeQuery();

84. if (!res.next()) {
85. throw new RuntimeException("error query");
86. } else {
87. int count = res.getInt("count");
88. if (count >= 2) {
89. // If current on-call doctor has 2 or more, this doctor can leave
90. PreparedStatement insert = connection.prepareStatement( comment +
91. "UPDATE `doctors` SET `on_call` = ? WHERE `id` = ? AND `shift_id` = ?");
92. insert.setBoolean(1, false);
93. insert.setInt(2, doctorID);
94. insert.setInt(3, 123);
95. insert.executeUpdate();

97. connection.commit();
98. } else {
99. throw new RuntimeException("At least one doctor is on call");
100. }
101. }

103. // Txn 2 is done. Let txn 1 run again.
104. if (txnID == 2) {
105. txn1Pass.release();
106. }
107. } catch (Exception e) {
108. // If got any error, you should roll back, data is priceless
109. connection.rollback();
110. e.printStackTrace();
111. }
112. } catch (SQLException e) {
113. e.printStackTrace();
114. }
115. }
116. }

在 Golang 中,首先,封装一个用于适配 TiDB 事务的工具包 util,随后编写以下代码:

1. package main

3. import (
4. "database/sql"
5. "fmt"
6. "sync"

8. "github.com/pingcap-inc/tidb-example-golang/util"

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

13. func main() {
14. openDB("mysql", "root:@tcp(127.0.0.1:4000)/test", func(db *sql.DB) {
15. writeSkew(db)
16. })
17. }

19. func openDB(driverName, dataSourceName string, runnable func(db *sql.DB)) {
20. db, err := sql.Open(driverName, dataSourceName)
21. if err != nil {
22. panic(err)
23. }
24. defer db.Close()

26. runnable(db)
27. }

29. func writeSkew(db *sql.DB) {
30. err := prepareData(db)
31. if err != nil {
32. panic(err)
33. }

35. waitingChan, waitGroup := make(chan bool), sync.WaitGroup{}

37. waitGroup.Add(1)
38. go func() {
39. defer waitGroup.Done()
40. err = askForLeave(db, waitingChan, 1, 1)
41. if err != nil {
42. panic(err)
43. }
44. }()

46. waitGroup.Add(1)
47. go func() {
48. defer waitGroup.Done()
49. err = askForLeave(db, waitingChan, 2, 2)
50. if err != nil {
51. panic(err)
52. }
53. }()

55. waitGroup.Wait()
56. }

58. func askForLeave(db *sql.DB, waitingChan chan bool, goroutineID, doctorID int) error {
59. txnComment := fmt.Sprintf("/* txn %d */ ", goroutineID)
60. if goroutineID != 1 {
61. txnComment = "\t" + txnComment
62. }

64. txn, err := util.TiDBSqlBegin(db, true)
65. if err != nil {
66. return err
67. }
68. fmt.Println(txnComment + "start txn")

70. // Txn 1 should be waiting until txn 2 is done.
71. if goroutineID == 1 {
72. <-waitingChan
73. }

75. txnFunc := func() error {
76. queryCurrentOnCall := "SELECT COUNT(*) AS `count` FROM `doctors` WHERE `on_call` = ? AND `shift_id` = ?"
77. rows, err := txn.Query(queryCurrentOnCall, true, 123)
78. if err != nil {
79. return err
80. }
81. defer rows.Close()
82. fmt.Println(txnComment + queryCurrentOnCall + " successful")

84. count := 0
85. if rows.Next() {
86. err = rows.Scan(&count)
87. if err != nil {
88. return err
89. }
90. }
91. rows.Close()

93. if count < 2 {
94. return fmt.Errorf("at least one doctor is on call")
95. }

97. shift := "UPDATE `doctors` SET `on_call` = ? WHERE `id` = ? AND `shift_id` = ?"
98. _, err = txn.Exec(shift, false, doctorID, 123)
99. if err == nil {
100. fmt.Println(txnComment + shift + " successful")
101. }
102. return err
103. }

105. err = txnFunc()
106. if err == nil {
107. txn.Commit()
108. fmt.Println("[runTxn] commit success")
109. } else {
110. txn.Rollback()
111. fmt.Printf("[runTxn] got an error, rollback: %+v\n", err)
112. }

114. // Txn 2 is done. Let txn 1 run again.
115. if goroutineID == 2 {
116. waitingChan <- true
117. }

119. return nil
120. }

122. func prepareData(db *sql.DB) error {
123. err := createDoctorTable(db)
124. if err != nil {
125. return err
126. }

128. err = createDoctor(db, 1, "Alice", true, 123)
129. if err != nil {
130. return err
131. }
132. err = createDoctor(db, 2, "Bob", true, 123)
133. if err != nil {
134. return err
135. }
136. err = createDoctor(db, 3, "Carol", false, 123)
137. if err != nil {
138. return err
139. }
140. return nil
141. }

143. func createDoctorTable(db *sql.DB) error {
144. _, err := db.Exec("CREATE TABLE IF NOT EXISTS `doctors` (" +
145. "    `id` int(11) NOT NULL," +
146. "    `name` varchar(255) DEFAULT NULL," +
147. "    `on_call` tinyint(1) DEFAULT NULL," +
148. "    `shift_id` int(11) DEFAULT NULL," +
149. "    PRIMARY KEY (`id`)," +
150. "    KEY `idx_shift_id` (`shift_id`)" +
151. "  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin")
152. return err
153. }

155. func createDoctor(db *sql.DB, id int, name string, onCall bool, shiftID int) error {
156. _, err := db.Exec("INSERT INTO `doctors` (`id`, `name`, `on_call`, `shift_id`) VALUES (?, ?, ?, ?)",
157. id, name, onCall, shiftID)
158. return err
159. }

SQL 日志:

1. /* txn 1 */ BEGIN
2. /* txn 2 */ BEGIN
3. /* txn 2 */ SELECT COUNT(*) as `count` FROM `doctors` WHERE `on_call` = 1 AND `shift_id` = 123
4. /* txn 2 */ UPDATE `doctors` SET `on_call` = 0 WHERE `id` = 2 AND `shift_id` = 123
5. /* txn 2 */ COMMIT
6. /* txn 1 */ SELECT COUNT(*) AS `count` FROM `doctors` WHERE `on_call` = 1 and `shift_id` = 123
7. /* txn 1 */ UPDATE `doctors` SET `on_call` = 0 WHERE `id` = 1 AND `shift_id` = 123
8. /* txn 1 */ COMMIT

执行结果:

1. mysql> SELECT * FROM doctors;
2. +----+-------+---------+----------+
3. | id | name  | on_call | shift_id |
4. +----+-------+---------+----------+
5. |  1 | Alice |       0 |      123 |
6. |  2 | Bob   |       0 |      123 |
7. |  3 | Carol |       0 |      123 |
8. +----+-------+---------+----------+

在两个事务中,应用首先检查是否有两个或以上的医生正在值班;如果是的话,它就假定一名医生可以安全地休班。由于数据库使用快照隔离,两次检查都返回 2,所以两个事务都进入下一个阶段。Alice 更新自己的记录休班了,而 Bob 也做了一样的事情。两个事务都成功提交了,现在没有医生值班了。违反了至少有一名医生在值班的要求。下图(引用自《Designing Data-Intensive Application》)说明了实际发生的情况:

Write Skew

现在更改示例程序,使用 SELECT FOR UPDATE 来克服写偏斜问题:

  • Java
  • Golang

Java 中使用 SELECT FOR UPDATE 来克服写偏斜问题的示例如下:

1. package com.pingcap.txn.write.skew;

3. import com.zaxxer.hikari.HikariDataSource;

5. import java.sql.Connection;
6. import java.sql.PreparedStatement;
7. import java.sql.ResultSet;
8. import java.sql.SQLException;
9. import java.util.concurrent.CountDownLatch;
10. import java.util.concurrent.ExecutorService;
11. import java.util.concurrent.Executors;
12. import java.util.concurrent.Semaphore;

14. public class EffectWriteSkew {
15. public static void main(String[] args) throws SQLException, InterruptedException {
16. HikariDataSource ds = new HikariDataSource();
17. ds.setJdbcUrl("jdbc:mysql://localhost:4000/test?useServerPrepStmts=true&cachePrepStmts=true");
18. ds.setUsername("root");

20. // prepare data
21. Connection connection = ds.getConnection();
22. createDoctorTable(connection);
23. createDoctor(connection, 1, "Alice", true, 123);
24. createDoctor(connection, 2, "Bob", true, 123);
25. createDoctor(connection, 3, "Carol", false, 123);

27. Semaphore txn1Pass = new Semaphore(0);
28. CountDownLatch countDownLatch = new CountDownLatch(2);
29. ExecutorService threadPool = Executors.newFixedThreadPool(2);

31. threadPool.execute(() -> {
32. askForLeave(ds, txn1Pass, 1, 1);
33. countDownLatch.countDown();
34. });

36. threadPool.execute(() -> {
37. askForLeave(ds, txn1Pass, 2, 2);
38. countDownLatch.countDown();
39. });

41. countDownLatch.await();
42. }

44. public static void createDoctorTable(Connection connection) throws SQLException {
45. connection.createStatement().executeUpdate("CREATE TABLE `doctors` (" +
46. "    `id` int(11) NOT NULL," +
47. "    `name` varchar(255) DEFAULT NULL," +
48. "    `on_call` tinyint(1) DEFAULT NULL," +
49. "    `shift_id` int(11) DEFAULT NULL," +
50. "    PRIMARY KEY (`id`)," +
51. "    KEY `idx_shift_id` (`shift_id`)" +
52. "  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin");
53. }

55. public static void createDoctor(Connection connection, Integer id, String name, Boolean onCall, Integer shiftID) throws SQLException {
56. PreparedStatement insert = connection.prepareStatement(
57. "INSERT INTO `doctors` (`id`, `name`, `on_call`, `shift_id`) VALUES (?, ?, ?, ?)");
58. insert.setInt(1, id);
59. insert.setString(2, name);
60. insert.setBoolean(3, onCall);
61. insert.setInt(4, shiftID);
62. insert.executeUpdate();
63. }

65. public static void askForLeave(HikariDataSource ds, Semaphore txn1Pass, Integer txnID, Integer doctorID) {
66. try(Connection connection = ds.getConnection()) {
67. try {
68. connection.setAutoCommit(false);

70. String comment = txnID == 2 ? "    " : "" + "/* txn #{txn_id} */ ";
71. connection.createStatement().executeUpdate(comment + "BEGIN");

73. // Txn 1 should be waiting until txn 2 is done.
74. if (txnID == 1) {
75. txn1Pass.acquire();
76. }

78. PreparedStatement currentOnCallQuery = connection.prepareStatement(comment +
79. "SELECT COUNT(*) AS `count` FROM `doctors` WHERE `on_call` = ? AND `shift_id` = ? FOR UPDATE");
80. currentOnCallQuery.setBoolean(1, true);
81. currentOnCallQuery.setInt(2, 123);
82. ResultSet res = currentOnCallQuery.executeQuery();

84. if (!res.next()) {
85. throw new RuntimeException("error query");
86. } else {
87. int count = res.getInt("count");
88. if (count >= 2) {
89. // If current on-call doctor has 2 or more, this doctor can leave
90. PreparedStatement insert = connection.prepareStatement( comment +
91. "UPDATE `doctors` SET `on_call` = ? WHERE `id` = ? AND `shift_id` = ?");
92. insert.setBoolean(1, false);
93. insert.setInt(2, doctorID);
94. insert.setInt(3, 123);
95. insert.executeUpdate();

97. connection.commit();
98. } else {
99. throw new RuntimeException("At least one doctor is on call");
100. }
101. }

103. // Txn 2 is done. Let txn 1 run again.
104. if (txnID == 2) {
105. txn1Pass.release();
106. }
107. } catch (Exception e) {
108. // If got any error, you should roll back, data is priceless
109. connection.rollback();
110. e.printStackTrace();
111. }
112. } catch (SQLException e) {
113. e.printStackTrace();
114. }
115. }
116. }

Golang 中使用 SELECT FOR UPDATE 来克服写偏斜问题的示例如下:

1. package main

3. import (
4. "database/sql"
5. "fmt"
6. "sync"

8. "github.com/pingcap-inc/tidb-example-golang/util"

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

13. func main() {
14. openDB("mysql", "root:@tcp(127.0.0.1:4000)/test", func(db *sql.DB) {
15. writeSkew(db)
16. })
17. }

19. func openDB(driverName, dataSourceName string, runnable func(db *sql.DB)) {
20. db, err := sql.Open(driverName, dataSourceName)
21. if err != nil {
22. panic(err)
23. }
24. defer db.Close()

26. runnable(db)
27. }

29. func writeSkew(db *sql.DB) {
30. err := prepareData(db)
31. if err != nil {
32. panic(err)
33. }

35. waitingChan, waitGroup := make(chan bool), sync.WaitGroup{}

37. waitGroup.Add(1)
38. go func() {
39. defer waitGroup.Done()
40. err = askForLeave(db, waitingChan, 1, 1)
41. if err != nil {
42. panic(err)
43. }
44. }()

46. waitGroup.Add(1)
47. go func() {
48. defer waitGroup.Done()
49. err = askForLeave(db, waitingChan, 2, 2)
50. if err != nil {
51. panic(err)
52. }
53. }()

55. waitGroup.Wait()
56. }

58. func askForLeave(db *sql.DB, waitingChan chan bool, goroutineID, doctorID int) error {
59. txnComment := fmt.Sprintf("/* txn %d */ ", goroutineID)
60. if goroutineID != 1 {
61. txnComment = "\t" + txnComment
62. }

64. txn, err := util.TiDBSqlBegin(db, true)
65. if err != nil {
66. return err
67. }
68. fmt.Println(txnComment + "start txn")

70. // Txn 1 should be waiting until txn 2 is done.
71. if goroutineID == 1 {
72. <-waitingChan
73. }

75. txnFunc := func() error {
76. queryCurrentOnCall := "SELECT COUNT(*) AS `count` FROM `doctors` WHERE `on_call` = ? AND `shift_id` = ?"
77. rows, err := txn.Query(queryCurrentOnCall, true, 123)
78. if err != nil {
79. return err
80. }
81. defer rows.Close()
82. fmt.Println(txnComment + queryCurrentOnCall + " successful")

84. count := 0
85. if rows.Next() {
86. err = rows.Scan(&count)
87. if err != nil {
88. return err
89. }
90. }
91. rows.Close()

93. if count < 2 {
94. return fmt.Errorf("at least one doctor is on call")
95. }

97. shift := "UPDATE `doctors` SET `on_call` = ? WHERE `id` = ? AND `shift_id` = ?"
98. _, err = txn.Exec(shift, false, doctorID, 123)
99. if err == nil {
100. fmt.Println(txnComment + shift + " successful")
101. }
102. return err
103. }

105. err = txnFunc()
106. if err == nil {
107. txn.Commit()
108. fmt.Println("[runTxn] commit success")
109. } else {
110. txn.Rollback()
111. fmt.Printf("[runTxn] got an error, rollback: %+v\n", err)
112. }

114. // Txn 2 is done. Let txn 1 run again.
115. if goroutineID == 2 {
116. waitingChan <- true
117. }

119. return nil
120. }

122. func prepareData(db *sql.DB) error {
123. err := createDoctorTable(db)
124. if err != nil {
125. return err
126. }

128. err = createDoctor(db, 1, "Alice", true, 123)
129. if err != nil {
130. return err
131. }
132. err = createDoctor(db, 2, "Bob", true, 123)
133. if err != nil {
134. return err
135. }
136. err = createDoctor(db, 3, "Carol", false, 123)
137. if err != nil {
138. return err
139. }
140. return nil
141. }

143. func createDoctorTable(db *sql.DB) error {
144. _, err := db.Exec("CREATE TABLE IF NOT EXISTS `doctors` (" +
145. "    `id` int(11) NOT NULL," +
146. "    `name` varchar(255) DEFAULT NULL," +
147. "    `on_call` tinyint(1) DEFAULT NULL," +
148. "    `shift_id` int(11) DEFAULT NULL," +
149. "    PRIMARY KEY (`id`)," +
150. "    KEY `idx_shift_id` (`shift_id`)" +
151. "  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin")
152. return err
153. }

155. func createDoctor(db *sql.DB, id int, name string, onCall bool, shiftID int) error {
156. _, err := db.Exec("INSERT INTO `doctors` (`id`, `name`, `on_call`, `shift_id`) VALUES (?, ?, ?, ?)",
157. id, name, onCall, shiftID)
158. return err
159. }

SQL 日志:

1. /* txn 1 */ BEGIN
2. /* txn 2 */ BEGIN
3. /* txn 2 */ SELECT COUNT(*) AS `count` FROM `doctors` WHERE on_call = 1 AND `shift_id` = 123 FOR UPDATE
4. /* txn 2 */ UPDATE `doctors` SET on_call = 0 WHERE `id` = 2 AND `shift_id` = 123
5. /* txn 2 */ COMMIT
6. /* txn 1 */ SELECT COUNT(*) AS `count` FROM `doctors` WHERE `on_call` = 1 FOR UPDATE
7. At least one doctor is on call
8. /* txn 1 */ ROLLBACK

执行结果:

1. mysql> SELECT * FROM doctors;
2. +----+-------+---------+----------+
3. | id | name  | on_call | shift_id |
4. +----+-------+---------+----------+
5. |  1 | Alice |       1 |      123 |
6. |  2 | Bob   |       0 |      123 |
7. |  3 | Carol |       0 |      123 |
8. +----+-------+---------+----------+

对 savepoint 和嵌套事务的支持

Spring 支持的 PROPAGATION_NESTED 传播行为会启动一个嵌套的事务,它是当前事务之上独立启动的一个子事务。嵌套事务开始时会记录一个 savepoint,如果嵌套事务执行失败,事务将会回滚到 savepoint 的状态。嵌套事务是外层事务的一部分,它将会在外层事务提交时一起被提交。下面案例展示了 savepoint 机制:

1. mysql> BEGIN;
2. mysql> INSERT INTO T2 VALUES(100);
3. mysql> SAVEPOINT svp1;
4. mysql> INSERT INTO T2 VALUES(200);
5. mysql> ROLLBACK TO SAVEPOINT svp1;
6. mysql> RELEASE SAVEPOINT svp1;
7. mysql> COMMIT;
8. mysql> SELECT * FROM T2;
9. +------+
10. |  ID   |
11. +------+
12. |  100 |
13. +------+

事务限制 - 图3注意

TiDB 从 v6.2.0 版本开始支持 savepoint 特性。因此低于 v6.2.0 版本的 TiDB 不支持 PROPAGATION_NESTED 传播行为。基于 Java Spring 框架的应用如果使用了 PROPAGATION_NESTED 传播行为,需要在应用端做出调整,将嵌套事务的逻辑移除。

大事务限制

基本原则是要限制事务的大小。TiDB 对单个事务的大小有限制,这层限制是在 KV 层面。反映在 SQL 层面的话,简单来说一行数据会映射为一个 KV entry,每多一个索引,也会增加一个 KV entry。所以这个限制反映在 SQL 层面是:

  • 最大单行记录容量为 120MB(TiDB v5.0 及更高的版本可通过 tidb-server 配置项 performance.txn-entry-size-limit 调整,低于 TiDB v5.0 的版本支持的单行容量为 6MB)。
  • 支持的最大单个事务容量为 10GB(TiDB v4.0 及更高版本可通过 tidb-server 配置项 performance.txn-total-size-limit 调整,低于 TiDB v4.0 的版本支持的最大单个事务容量为 100MB)。

另外注意,无论是大小限制还是行数限制,还要考虑事务执行过程中,TiDB 做编码以及事务额外 Key 的开销。在使用的时候,为了使性能达到最优,建议每 100 ~ 500 行写入一个事务。

自动提交的 SELECT FOR UPDATE 语句不会等锁

自动提交下的 SELECT FOR UPDATE 目前不会加锁。效果如下图所示:

TiDB中的情况

这是已知的与 MySQL 不兼容的地方。

可以通过使用显式的 BEGIN;COMMIT; 解决该问题。