事务管理 #
事务原理 #
虽然 SphereEx-DBPlusEngine 希望能够完全兼容所有的分布式事务场景,并在性能上达到最优,但在 CAP 定理所指导下,分布式事务必然有所取舍。SphereEx-DBPlusEngine 希望能够将分布式事务的选择权交给使用者,在不同的场景用使用最适合的分布式事务解决方案。由于应用的场景不同,需要开发者能够合理的在性能与功能之间权衡各种分布式事务。强一致的事务与柔性事务的 API 和功能并不完全相同,在它们之间并不能做到自由的透明切换。在开发决策阶段,就不得不在强一致的事务和柔性事务之间抉择,使得设计和开发成本被大幅增加。基于 XA 的强一致事务使用相对简单,但是无法很好的应对互联网的高并发或复杂系统的长事务场景;柔性事务则需要开发者对应用进行改造,接入成本非常高,并且需要开发者自行实现资源锁定和反向补偿。
事务分类 #
SphereEx-DBPlusEngine 提供了三种事务模式:LOCAL,XA,BASE,以应对不同的场景。
- LOCAL:适用于对数据一致性要求不高的场景。
- XA:提供了原子性的保证,保证了数据不丢,不保证快照读。适用于对一致性要求相对高,没有快照读要求的场景。在对一致性要求较高的场景,较好的选择是使用 XA 的 Narayana 实现。如果底层存储节点的数据库隔离级是 Serializable ,可以达到强一致性的语义。
- BASE:在一致性和性能之间做了权衡,具体参考 Seata 官网。
使用方法 #
SphereEx-DBPlusEngine 中可通过配置文件(global.yaml)完成配置。主要是完成事务处理方式和分布式事务处理机制的配置。具体如下:
- defaultType
使用LOCAL事务
transaction:
defaultType: LOCAL
使用XA事务
transaction:
defaultType: XA
使用BASE事务
transaction:
defaultType: BASE
- providerType
仅对 defaultType=XA 情况下适用。SphereEx-DBPlusEngine 支持多种分布式事务处理器:Atomikos、Narayana。建议用户选择使用 Narayana。
transaction:
defaultType: XA
providerType: Narayana
注意事项 #
本地事务 #
支持项
- 完全支持非跨库事务,例如:仅分表,或分库但是路由的结果在单库中;
- 完全支持因逻辑异常导致的跨库事务。例如:同一事务中,跨两个库更新。更新完毕后,抛出空指针,则两个库的内容都能够回滚。
不支持项
不支持因网络、硬件异常导致的跨库事务。例如:同一事务中,跨两个库更新,更新完毕后、未提交之前,第一个库宕机,则只有第二个库数据提交,且无法回滚。
XA 事务 #
支持项
- 支持数据分片后的跨库事务;
- 两阶段提交保证操作的原子性和数据的强一致性;
- 服务宕机重启后,提交/回滚中的事务可自动恢复;
- 支持同时使用 XA 和非 XA 的连接池。
- 支持通过数据库和文件方式存储事务恢复日志。
不支持项
- 服务宕机后,在其它机器上恢复提交/回滚中的数据;
- MySQL 事务块内,SQL 执行出现异常,执行 Commit,数据保持一致;
- 配置 XA 事务后,存储单元名称最大长度不超过45个字符。
- 当使用 DB 方式存储 XA 事务时,若存储 DB 不可用,会影响事务的执行和恢复。
XA 事务所需的权限:
- MySQL 8:MySQL 8 数据库需要授予用户
XA_RECOVER_ADMIN
权限,否则 XA 事务管理器执行 XA RECOVER 语句时会报错,通过执行GRANT XA_RECOVER_ADMIN TO currentUser
进行授权; - Oracle:Oracle 数据库需要授予用户
SYS.DBA_PENDING_TRANSACTIONS
查询权限,通过执行GRANT SELECT ON SYS.DBA_PENDING_TRANSACTIONS TO currentUser
进行授权。
- MySQL 8:MySQL 8 数据库需要授予用户
柔性事务 #
支持项
- 支持数据分片后的跨库事务;
- 支持 RC 隔离级别;
- 通过 undo 快照进行事务回滚;
- 支持服务宕机后的,自动恢复提交中的事务。
不支持项
不支持除 RC 之外的隔离级别。
待优化项
SphereEx-DBPlusEngine 和 SEATA 重复 SQL 解析。
分布式事务实践 #
Narayana 使用方法 #
针对常见的分布式事务需求,SphereEx-DBPlusEngine 推荐使用 Narayana 实现的分布式事务,具体使用方法如下:
由于 Narayana 配置比较繁琐,DBPlusEngine-Proxy 提供了自动化配置功能,现在用户无需手动配置 jbossts-properties.xml 配置文件,DBPlusEngine 会自动根据用户在 global.yaml 里指定的 Narayana 事务配置,生成对应的 jbossts-properties.xml 配置文件。
- global.yaml 当使用 Narayana 作为 XA 事务管理器时,且没有在 props 里配置额外的属性。集群模式下,会自动配置为使用 DB 方式存储 XA Recovery 信息;单机模式下,会自动配置为使用文件方式存储 XA Recovery 信息。
transaction:
defaultType: XA
providerType: Narayana
当使用 Narayana 作为 XA 事务管理器时,支持在 props 里配置事务超时时间,单位秒,默认不配置为 180 秒。
transaction:
defaultType: XA
providerType: Narayana
props:
defaultTimeout: 300 # 配置事务超时时间 300 秒
当使用 Narayana 作为 XA 事务管理器时,并且配置了使用 DB 方式存储 XA Recovery 信息,则 DBPlusEngine-Proxy 支持将故障的 Proxy 实例上尚未恢复的事务转移到其他 Proxy 上进行恢复。
配置如下:
transaction:
defaultType: XA
providerType: Narayana
props:
recoveryStoreUrl: jdbc:mysql://127.0.0.1:3306/jbossts?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true# mysql8 使用 com.mysql.cj.jdbc.MysqlDataSourcerecoveryStoreDataSource: com.mysql.jdbc.jdbc2.optional.MysqlDataSource
recoveryStoreUser: databaseUser
recoveryStorePassword: databasePwd
当使用 DB 方式存储 XA Recovery 信息时,需要提前创建好对应的存储事务恢复日志的数据库,比如上面的 jbossts
数据库。Narayana 会自动在该库下创建所需的表,包括 CommunicationJBossTSTxTable
和 ActionJBossTSTxTable
表。
在 XA 事务执行期间,Narayana 会向 ActionJBossTSTxTable
表记录事务日志,在事务执行完成后删除事务日志记录。期间如果 RM 或 TM 发生故障,会依赖 ActionJBossTSTxTable
表存储的事务日志进行事务恢复。所以需要保证事务恢复数据库的可用性,如果发生可用性问题,会导致事务执行失败,影响业务。
使用文件方式存储 XA Recovery 信息的配置如下:
transaction:
defaultType: XA
providerType: Narayana
props:
recoveryStoreType: File
注意,在集群模式下,使用文件方式存储 XA Recovery 信息会导致 Proxy 实例之间无法共享事务恢复日志,因此不推荐使用文件方式存储 XA Recovery 信息。
通过 XA 语句控制的分布式事务 #
通过 XA START 可以手动开启 XA 事务,注意该事务完全由用户管理,DBPlusEngine 只负责将语句转发至后端数据库;
服务宕机后,需要通过 XA RECOVER 获取未提交或回滚的事务,也可以在 COMMIT 时使用 ONE PHASE 跳过 PERPARE。
MySQL [(none)]> use test1 │MySQL [(none)]> use test2
Reading table information for completion of table and column names │Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A │You can turn off this feature to get a quicker startup with -A
│
Database changed │Database changed
MySQL [test1]> XA START '61c052438d3eb'; │MySQL [test2]> XA START '61c0524390927';
Query OK, 0 rows affected (0.030 sec) │Query OK, 0 rows affected (0.009 sec)
│
MySQL [test1]> update test set val = 'xatest1' where id = 1; │MySQL [test2]> update test set val = 'xatest2' where id = 1;
Query OK, 1 row affected (0.077 sec) │Query OK, 1 row affected (0.010 sec)
│
MySQL [test1]> XA END '61c052438d3eb'; │MySQL [test2]> XA END '61c0524390927';
Query OK, 0 rows affected (0.006 sec) │Query OK, 0 rows affected (0.008 sec)
│
MySQL [test1]> XA PREPARE '61c052438d3eb'; │MySQL [test2]> XA PREPARE '61c0524390927';
Query OK, 0 rows affected (0.018 sec) │Query OK, 0 rows affected (0.011 sec)
│
MySQL [test1]> XA COMMIT '61c052438d3eb'; │MySQL [test2]> XA COMMIT '61c0524390927';
Query OK, 0 rows affected (0.011 sec) │Query OK, 0 rows affected (0.018 sec)
│
MySQL [test1]> select * from test where id = 1; │MySQL [test2]> select * from test where id = 1;
+----+---------+ │+----+---------+
| id | val | │| id | val |
+----+---------+ │+----+---------+
| 1 | xatest1 | │| 1 | xatest2 |
+----+---------+ │+----+---------+
1 row in set (0.016 sec) │1 row in set (0.129 sec)
MySQL [test1]> XA START '61c05243994c3'; │MySQL [test2]> XA START '61c052439bd7b';
Query OK, 0 rows affected (0.047 sec) │Query OK, 0 rows affected (0.006 sec)
│
MySQL [test1]> update test set val = 'xarollback' where id = 1; │MySQL [test2]> update test set val = 'xarollback' where id = 1;
Query OK, 1 row affected (0.175 sec) │Query OK, 1 row affected (0.008 sec)
│
MySQL [test1]> XA END '61c05243994c3'; │MySQL [test2]> XA END '61c052439bd7b';
Query OK, 0 rows affected (0.007 sec) │Query OK, 0 rows affected (0.014 sec)
│
MySQL [test1]> XA PREPARE '61c05243994c3'; │MySQL [test2]> XA PREPARE '61c052439bd7b';
Query OK, 0 rows affected (0.013 sec) │Query OK, 0 rows affected (0.019 sec)
│
MySQL [test1]> XA ROLLBACK '61c05243994c3'; │MySQL [test2]> XA ROLLBACK '61c052439bd7b';
Query OK, 0 rows affected (0.010 sec) │Query OK, 0 rows affected (0.010 sec)
│
MySQL [test1]> select * from test where id = 1; │MySQL [test2]> select * from test where id = 1;
+----+---------+ │+----+---------+
| id | val | │| id | val |
+----+---------+ │+----+---------+
| 1 | xatest1 | │| 1 | xatest2 |
+----+---------+ │+----+---------+
1 row in set (0.009 sec) │1 row in set (0.083 sec)
MySQL [test1]> XA START '61c052438d3eb';
Query OK, 0 rows affected (0.030 sec)
MySQL [test1]> update test set val = 'recover' where id = 1;
Query OK, 1 row affected (0.072 sec)
MySQL [test1]> select * from test where id = 1;
+----+---------+
| id | val |
+----+---------+
| 1 | recover |
+----+---------+
1 row in set (0.039 sec)
MySQL [test1]> XA END '61c052438d3eb';
Query OK, 0 rows affected (0.005 sec)
MySQL [test1]> XA PREPARE '61c052438d3eb';
Query OK, 0 rows affected (0.020 sec)
MySQL [test1]> XA RECOVER;
+----------+--------------+--------------+---------------+
| formatID | gtrid_length | bqual_length | data |
+----------+--------------+--------------+---------------+
| 1 | 13 | 0 | 61c052438d3eb |
+----------+--------------+--------------+---------------+
1 row in set (0.010 sec)
MySQL [test1]> XA RECOVER CONVERT XID;
+----------+--------------+--------------+------------------------------+
| formatID | gtrid_length | bqual_length | data |
+----------+--------------+--------------+------------------------------+
| 1 | 13 | 0 | 0x36316330353234333864336562 |
+----------+--------------+--------------+------------------------------+
1 row in set (0.011 sec)
MySQL [test1]> XA COMMIT 0x36316330353234333864336562;
Query OK, 0 rows affected (0.029 sec)
MySQL [test1]> XA RECOVER;
Empty set (0.011 sec)