原文地址:Retry logic for optimistic locking and transparent failover
原文作者:Franck Pachot
下面是一个小样例,用于演示必须实现的重试逻辑,以便处理数据库中的事务冲突或透明故障转移错误。目标是能够在笔记本电脑上进行一些实验,可惜,没有更好的解决方案。
配置 PostgreSQL 数据库
启动本地 PostgreSQL:
docker run --name pg -e POSTGRES_PASSWORD=franck -p 5432:5432 -d postgres
创建一个表(franck),插入10行数据:
PGUSER=postgres PGPASSWORD=franck PGHOST=localhost PGPORT=5432 psql -e <<SQL
create table franck(id int primary key, val int);
insert into franck select generate_series(1,10),0;
SQL
create table franck(id int primary key, val int)
CREATE TABLE
insert into franck select generate_series(1,10),0;
INSERT 0 10
获取YBDemo
我使用一个非常简单的程序在线程中运行查询,
下载YBDemo.jar,此jar包中包括PostgreSQL和YugabyteDB JDBC驱动程序以及Hikari.properties参数文件:
curl -Ls https://github.com/FranckPachot/ybdemo/releases/download/v0.0.1/YBDemo-0.0.1-SNAPSHOT-jar-with-dependencies.jar > YBDemo.jar
curl -Ls https://jdbc.postgresql.org/download/postgresql-42.3.2.jar > postgresql.jar
我在 Hikary.properties参数文件中设置了连接参数:
cat > hikari.properties <<'INI'
dataSourceClassName=org.postgresql.ds.PGSimpleDataSource
dataSource.url=jdbc:postgresql://localhost:5432/postgres?user=postgres&password=franck
INI
使用for循环,开启5个线程,对表的每一行进行更新操作。
for i in {1..5} ; do
echo "update franck set val=val+1 returning val+1"
done | java -jar YBDemo.jar
这是一个非常简单的示例,更新fracnk表的每一行,使值val=val+1,从多个线程运行,以显示事务冲突。当然,如果您的应用程序中有此内容,也可以使用自己的程序。但实验室的目标是分析简单的事情,以便充分理解。
Read committed
在PostgreSQL 中的默认隔离级别是 Read Commited(提交读)。您通过我的程序可以快速检查是否是提交读:
java -jar YBDemo.jar <<< "select current_setting('transaction_isolation')"
因此,如果您运行下面程序至少3个线程,您将看到一些事务冲突错误:
update franck set val=val+1 returning val+1#
以下为冲突报错
Thread-2 5 ms: 17735
Thread-0 5 ms: 17736
Thread-1 5 ms: 17737
update franck set val=val+1 returning val+1
Error in thread Thread-0 1016 ms SQLSTATE(40P01) - retry 0/10
org.postgresql.util.PSQLException: ERROR: deadlock detected
Detail: Process 180 waits for ShareLock on transaction 20923; blocked by process 182.
Process 182 waits for ShareLock on transaction 20926; blocked by process 180.
Hint: See server log for query details.
Where: while rechecking updated tuple (11,5) in relation "franck"
wait in thread Thread-0 15 ms after 0 retries
update franck set val=val+1 returning val+1
Error in thread Thread-1 1005 ms SQLSTATE(40P01) - retry 0/10
org.postgresql.util.PSQLException: ERROR: deadlock detected
Detail: Process 181 waits for ShareLock on transaction 20989; blocked by process 182.
Process 182 waits for ShareLock on transaction 20988; blocked by process 181.
Hint: See server log for query details.
Where: while rechecking updated tuple (9,21) in relation "franck"
wait in thread Thread-1 10 ms after 0 retries
我运行此示例是为了清楚地表明,即使在 Read Commit (提交读)隔离级别,使用一个简单的 SQL 语句时,也可能发生事务冲突,并且应用程序必须使用重试逻辑来处理它。
为什么选择重试逻辑?因为您不想停止应用程序,也不想忽略不成功的事务。顺便说一句,这是我的YBDemo.java的主要目标。
因此,无论隔离级别如何,SQLSTATE 40P01 (deadlock_detected) 都必须由高可用性应用程序重试。
Serializable
在Serializable隔离级别中,可以预期会出现更多的事务冲突,因为可以保证整个事务的读/写稳定性。
在参数文件hikari.properties中添加以下参数,以设置每个事务的隔离级别:
cat >> hikari.properties <<'INI'
connectionInitSql= set default_transaction_isolation=serializable;
INI
再次运行以下程序:
for i in {1..5} ; do
echo "update franck set val=val+1 returning val+1"
done | java -jar YBDemo.jar
可以看到像下面一样的错误。
Thread-3 2 ms: 153703
Thread-3 2 ms: 153704
Thread-3 2 ms: 153705
wait in thread Thread-0 2567 ms after 8 retries
Thread-3 2 ms: 153706
update franck set val=val+1 returning val+1
Error in thread Thread-0 2 ms SQLSTATE(40001) - retry 9/10
org.postgresql.util.PSQLException: ERROR: could not serialize access due to concurrent update
Thread-3 3 ms: 153707
Thread-3 2 ms: 153708
Thread-3 2 ms: 153709
Thread-3 2 ms: 153710
This one was at the limit because my YBDemo.java fails above 10 retries. This happened quickly:
在YBDemo.java中设置了10次失败重试,程序很快就结束了。
Error in thread Thread-2 2 ms SQLSTATE(40001) - retry 0/10
wait in thread Thread-2 10 ms after 0 retries
Error in thread Thread-2 2 ms SQLSTATE(40001) - retry 1/10
wait in thread Thread-2 29 ms after 1 retries
Error in thread Thread-2 2 ms SQLSTATE(40001) - retry 2/10
wait in thread Thread-2 41 ms after 2 retries
Error in thread Thread-2 2 ms SQLSTATE(40001) - retry 3/10
wait in thread Thread-2 86 ms after 3 retries
Error in thread Thread-2 2 ms SQLSTATE(40001) - retry 4/10
wait in thread Thread-2 164 ms after 4 retries
Error in thread Thread-2 1 ms SQLSTATE(40001) - retry 5/10
wait in thread Thread-2 328 ms after 5 retries
Error in thread Thread-2 1 ms SQLSTATE(40001) - retry 6/10
wait in thread Thread-2 641 ms after 6 retries
Error in thread Thread-2 3 ms SQLSTATE(40001) - retry 7/10
wait in thread Thread-2 1289 ms after 7 retries
Error in thread Thread-2 4 ms SQLSTATE(40001) - retry 8/10
wait in thread Thread-2 2567 ms after 8 retries
Error in thread Thread-2 3 ms SQLSTATE(40001) - retry 9/10
wait in thread Thread-2 5125 ms after 9 retries
Error in thread Thread-2 2 ms SQLSTATE(40001) - retry 10/10
我使用exponential backoff(指数补偿、指数退避)方式来重试,你可以在代码中看到怎样实现。最后一个等待了5秒钟。
YugabyteDB
在分布式数据库中,由乐观锁的规则造成的重试的错误,可能由于多种原因而发生。在电脑上,使用docker-compose工具,使用yb-lab参数文件,启动3个服务组成一个分布式集群。

我创建了演示表(docker yb-tserver-0服务重定向本地端口为:5433 ):
psql -p 5433 -U yugabyte -d yugabyte <<SQL
create table franck(id int primary key, val int);
insert into franck select generate_series(1,10),0;
SQL
并为其设置连接参数:
cat > hikari.properties <<INI
dataSourceClassName=com.yugabyte.ysql.YBClusterAwareDataSource
dataSource.url=jdbc:yugabytedb://localhost:5433/yugabyte?user=yugabyte&load-balance=false
connectionInitSql= set default_transaction_isolation=serializable;
INI
当然,我看到一些 serialization错误,但未达到最大重试次数:
Thread-3 17 ms: 1474
Thread-3 17 ms: 1475
Thread-3 17 ms: 1476
Thread-3 17 ms: 1477
Thread-3 18 ms: 1478
Thread-3 17 ms: 1479
Thread-3 18 ms: 1480
Thread-3 17 ms: 1481
update franck set val=val+1 returning val+1
Error in thread Thread-0 6541 ms SQLSTATE(40001) - retry 1/10
com.yugabyte.util.PSQLException: ERROR: Operation failed. Try again.: [Operation failed. Try again. (yb/tablet/running_transaction.cc:456): Transaction aborted: 9ced2fc8-06d2-490c-b400-732e53013a56 (pgsql error 40001)]
Thread-3 18 ms: 1482
wait in thread Thread-0 21 ms after 1 retries
Thread-3 18 ms: 1483
Thread-0 20 ms: 1484
Thread-0 16 ms: 1485
Thread-0 18 ms: 1486
Thread-0 20 ms: 1487
Thread-0 19 ms: 1488
Thread-0 19 ms: 1489
Thread-0 18 ms: 1490
Thread-0 17 ms: 1491
运行一小时后,我得到了两个线程,达到9次重试。
因此,当使用Serializable隔离级别时,错误SQLSTATE 40001 (serialization_failure)必须进行重试,并应针对您的系统调整最大重试次数(以及重试之间的等待时间)。
高可用性
在上一个运行时,使用docker-compose的启停功能,对yb-tserver-2服务进行重启,等待几秒钟后(因为在停止节点中的主节点的服务后,必须有一个从节点被选为新的主节点)。但仍然只显示 SQLSTATE 40001。
但是,在之前的测试中,我只连接到yb-tserver-0。集群负载均衡功能是,当连接到一个节点后,当我连接的节点关闭时,应用程序将会连接到其他节点来继续提供服务(这要归功于连接池和集群感知 JDBC 驱动程序)。暂时性错误也可以由应用程序处理。
Thread-0 22 ms: 59648^M
Thread-0 22 ms: 59649^M
Thread-0 29 ms: 59650^M
53171 [Thread-3] WARN com.zaxxer.hikari.pool.ProxyConnection - HikariPool-1 - Connection com.yugabyte.jdbc.PgConnection@61336f01 marked as broken because of SQLSTATE(08006), ErrorCode(0)^M
53171 [Thread-0] WARN com.zaxxer.hikari.pool.ProxyConnection - HikariPool-1 - Connection com.yugabyte.jdbc.PgConnection@3b373f9c marked as broken because of SQLSTATE(08006), ErrorCode(0)^M
53171 [Thread-2] WARN com.zaxxer.hikari.pool.ProxyConnection - HikariPool-1 - Connection com.yugabyte.jdbc.PgConnection@2abffd27 marked as broken because of SQLSTATE(08006), ErrorCode(0)^M
53171 [Thread-4] WARN com.zaxxer.hikari.pool.ProxyConnection - HikariPool-1 - Connection com.yugabyte.jdbc.PgConnection@39961dd8 marked as broken because of SQLSTATE(08006), ErrorCode(0)^M
53171 [Thread-1] WARN com.zaxxer.hikari.pool.ProxyConnection - HikariPool-1 - Connection com.yugabyte.jdbc.PgConnection@683b2ab4 marked as broken because of SQLSTATE(08006), ErrorCode(0)^M
com.yugabyte.util.PSQLException: An I/O error occurred while sending to the backend.^M
at com.yugabyte.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:382)^M
at com.yugabyte.jdbc.PgStatement.executeInternal(PgStatement.java:490)^M
at com.yugabyte.jdbc.PgStatement.execute(PgStatement.java:408)^M
at com.yugabyte.jdbc.PgStatement.executeWithFlags(PgStatement.java:329)^M
at com.yugabyte.jdbc.PgStatement.executeCachedSql(PgStatement.java:315)^M
at com.yugabyte.jdbc.PgStatement.executeWithFlags(PgStatement.java:291)^M
at com.yugabyte.jdbc.PgStatement.executeQuery(PgStatement.java:243)^M
at com.zaxxer.hikari.pool.ProxyStatement.executeQuery(ProxyStatement.java:110)^M
at com.zaxxer.hikari.pool.HikariProxyStatement.executeQuery(HikariProxyStatement.java)^M
at YBDemo.run(YBDemo.java:38)^M\
错误 SQLSTATE 08006 (connection_failure)可能被重试。无需等待,因为在 YugabyteDB数据库中,其他节点立即可用。
在日志中也可以看到这些错误信息:
yb-lab-yb-demo-write-1 | Error in thread Thread-1 1589ms SQLSTATE(XX000) - retry 0/5
yb-lab-yb-demo-write-1 | com.yugabyte.util.PSQLException: ERROR: no owned sequence found
yb-lab-yb-demo-write-1 | Where: Catalog Version Mismatch: A DDL occurred while processing this query. Try again.
当测试不同的故障转移情况时,我决定重试“XX000”。
重新连接还可能获得一个 SQLTransientConnectionException,该异常也会重试,而无需等待,因为连接池已经具有超时设置
很难提供一些通用代码来处理这个问题。如果这么简单,这将在数据库或驱动程序中自动进行。YugabyteDB已经出现过很多冲突自动重试的情况。但某些情况必须由应用程序处理:
- 您是否执行了一些非事务性操作,这些操作有待完成两次的风险(例如:发送电子邮件、通知…)
- 您确定在提交更改之前收到连接错误,还是需要在重试之前进行检查?
总结
任何 SQL 数据库(因为它是共享的,并且保证 ACID 属性以避免数据损坏)都可能引发事务冲突。某些情况可以自动重试。它们的唯一后果是延迟较高,但考虑到它发生的可能性较低,因此在需要扩展场景下,它比悲观锁定更好。对于数据库不知道的应用程序逻辑,必须重试其他一些情况,尤其是如果没有将所有数据库操作封装到存储过程中。




