事务错误处理
本章介绍使用事务时可能会遇到的错误和处理办法。
死锁
如果应用程序遇到下面错误时,说明遇到了死锁问题:
ERROR 1213: Deadlock found when trying to get lock; try restarting transaction
当两个及以上的事务,双方都在等待对方释放已经持有的锁或因为加锁顺序不一致,造成循环等待锁资源,就会出现“死锁”。这里以 bookshop 数据库中的 books
表为示例演示死锁:
先给 books
表中写入 2 条数据:
INSERT INTO books (id, title, stock, published_at) VALUES (1, 'book-1', 10, now()), (2, 'book-2', 10, now());
在 TiDB 悲观事务模式下,用 2 个客户端分别执行以下语句,就会遇到死锁:
客户端-A | 客户端-B |
---|---|
BEGIN; | |
BEGIN; | |
UPDATE books SET stock=stock-1 WHERE id=1; | |
UPDATE books SET stock=stock-1 WHERE id=2; | |
UPDATE books SET stock=stock-1 WHERE id=2; -- 执行会被阻塞 | |
UPDATE books SET stock=stock-1 WHERE id=1; -- 遇到 Deadlock 错误 |
在客户端-B 遇到死锁错误后,TiDB 会自动 ROLLBACK
客户端-B 中的事务,然后客户端-A 中购买 id=2
的操作就会执行成功,再执行 COMMIT
即可完成购买的事务流程。
解决方案 1:避免死锁
为了应用程序有更好的性能,可以通过调整业务逻辑或者 Schema 设计,尽量从应用层面避免死锁。例如上面示例中,如果客户端-B 也和客户端-A 用同样的购买顺序,即都先买 id=1
的书,再买 id=2
的书,就可以避免死锁了:
客户端-A | 客户端-B |
---|---|
BEGIN; | |
BEGIN; | |
UPDATE books SET stock=stock-1 WHERE id=1; | |
UPDATE books SET stock=stock-1 WHERE id=1; -- 执行会被阻塞,当事务 A 完成后再继续执行 | |
UPDATE books SET stock=stock-1 WHERE id=2; | |
UPDATE books SET stock=stock-1 WHERE id=2; | |
COMMIT; | |
COMMIT; |
或者直接用 1 条 SQL 购买 2 本书,也能避免死锁,而且执行效率更高:
UPDATE books SET stock=stock-1 WHERE id IN (1, 2);
解决方案 2:减小事务粒度
如果每次购书都是一个单独的事务,也能避免死锁。但需要权衡的是,事务粒度太小不符合性能上的最佳实践。
解决方案 3:使用乐观事务
乐观事务模型下,并不会有死锁问题,但应用端需要加上乐观事务在失败后的重试逻辑,具体重试逻辑见应用端重试和错误处理。
解决方案 4:重试
正如错误信息中提示的那样,在应用代码中加入重试逻辑即可。具体重试逻辑见应用端重试和错误处理。
应用端重试和错误处理
尽管 TiDB 尽可能地与 MySQL 兼容,但其分布式系统的性质导致了某些差异,其中之一就是事务模型。
开发者用来与数据库通信的 Adapter 和 ORM 都是为 MySQL 和 Oracle 等传统数据库量身定制的,在这些数据库中,提交很少在默认隔离级别失败,因此不需要重试机制。对于这些客户端,当提交失败时,它们会因错误而中止,因为这在这些数据库中被呈现为罕见的异常。
与 MySQL 等传统数据库不同的是,在 TiDB 中,如果采用乐观事务模型,想要避免提交失败,需要在自己的应用程序的业务逻辑中添加机制来处理相关的异常。
下面的类似 Python 的伪代码展示了如何实现应用程序级的重试。 它不要求您的驱动程序或 ORM 来实现高级重试处理逻辑,因此可以在任何编程语言或环境中使用。
特别是,您的重试逻辑必须:
- 如果失败重试的次数达到
max_retries
限制,则抛出错误 - 使用
try ... catch ...
语句捕获 SQL 执行异常,当遇到下面这些错误时进行失败重试,遇到其它错误则进行回滚。详细信息请参考:错误码与故障诊断。Error 8002: can not retry select for update statement
:SELECT FOR UPDATE 写入冲突报错。Error 8022: Error: KV error safe to retry
:事务提交失败报错。Error 8028: Information schema is changed during the execution of the statement
:表的 Schema 结构因为完成了 DDL 变更,导致事务提交时报错。Error 9007: Write conflict
:写冲突报错,一般是采用乐观事务模式时,多个事务都对同一行数据进行修改时遇到的写冲突报错。
- 在 try 块结束时使用 COMMIT 提交事务:
while True:
n++
if n == max_retries:
raise("did not succeed within #{n} retries")
try:
connection.execute("your sql statement here")
connection.exec('COMMIT')
break
catch error:
if (error.code != "9007" && error.code != "8028" && error.code != "8002" && error.code != "8022"):
raise error
else:
connection.exec('ROLLBACK')
# Capture the error types that require application-side retry,
# wait for a short period of time,
# and exponentially increase the wait time for each transaction failure
sleep_ms = int(((1.5 ** n) + rand) * 100)
sleep(sleep_ms) # make sure your sleep() takes milliseconds