并发与事务管理
应用程序对所有数据库访问都使用事务阻塞。这使得事务管理器能够安排和处理所有数据库操作,无论这些操作是简单的只读访问,还是修改数据库对象的读写操作。
事务阻塞
事务块由一组数据库操作组成,这些操作被包含在事务开始和提交或回滚之间。在 C 应用程序中,事务通过调用 mco_trans_start()
或 mco_trans_start_ex()
这两个函数中的一个来启动。这两个函数的区别仅在于后者允许为事务设置隔离级别。要提交事务,请调用 mco_trans_commit()
;要丢弃自 mco_trans_start()
以来的所有数据库操作,请调用 mco_trans_rollback()
。
事务管理器
正如“基本概念”页面所述,提供了三种事务管理器以满足不同应用程序的需求和并发策略。事务管理器的选择会对应用程序的性能产生重大影响。但幸运的是,更改事务管理器只需更改链接器指令并重新构建应用程序即可。请使用这些链接查看 MURSIW 和 MVCC 事务管理器的实现细节。要链接 MURSIW,请在开发时使用 mcotmursiw_debug
库,在应用程序的发布版本中使用 mcotmursiw
库。要链接 MVCC,请在开发时使用 mcotmvcc_debug
库,在应用程序的发布版本中使用 mcotmvcc
库。
EXCL(独占)事务管理器仅适用于单进程、单线程的应用程序,因此实际上并不管理并发。要链接到 EXCL,请在开发时使用 mcotexcl_debug
库,在应用程序的发布版本中使用 mcotexcl
库。
设置隔离级别
当调用 mco_trans_start()
时,事务隔离级别会被设置为 MCO_DEFAULT_ISOLATION_LEVEL
,默认情况下,对于 MURSIW 是 MCO_SERIALIZABLE
,对于 MVCC 是 MCO_REPEATABLE_READ
。(请注意,如果使用 MURSIW,则唯一可能的级别是 MCO_SERIALIZABLE
。)可以为数据库会话(连接)重新定义默认的事务隔离级别。在 C 应用程序中,这通过调用函数 mco_trans_set_default_isolation_level()
来完成,该函数会返回之前的默认隔离级别。应用程序可以通过调用函数 mco_trans_isolation_level()
来确定当前的隔离级别,并通过调用函数 mco_trans_get_supported_isolation_levels()
来检查当前运行的事务管理器支持哪些事务隔离级别。
设置事务优先级和调度策略
正如“事务优先级和调度”页面中所解释的那样,应用程序可以在运行时调整事务优先级和调度策略。事务优先级在对 mco_trans_start() 的调用中指定。应用程序可以通过在传递给 mco_db_open_dev()
的 mco_db_params_t
中的 trans_sched_policy
设置所需的 MCO_TRANS_SCHED_POLICY
标志来显式定义 MURSIW 调度策略。
多版本并发控制(MVCC)冲突管理
当 MVCC 与 MCO_SERIALIZABLE
以外的隔离级别一起使用时,MCO_READ_WRITE
事务将并发执行。有时并发事务会修改相同的对象,从而产生事务冲突。事务管理器通过中止其中一个冲突事务并允许另一个事务将其更新提交到数据库来解决这些冲突。当事务被中止时,应用程序会收到 MCO_E_CONFLICT
错误代码。应用程序有责任通过类似于以下逻辑来管理这种可能性:
do {
mco_trans_start( db, MCO_READ_WRITE, MCO_TRANS_FOREGROUND, &t);
...<update database>...
rc = mco_trans_commit(t);
} while ( rc == MCO_E_CONFLICT );
请注意,当使用多版本并发控制(MVCC)时,应用程序必须能够容忍由于冲突而导致的事务回滚。
如果冲突的数量过高,可能会由于需要重试事务而导致性能急剧下降。出现这种情况时,事务管理器会临时将隔离级别更改为 MCO_SERIALIZABLE
。应用程序可以通过调用 mco_trans_optimistic_threshold() 函数来设置禁用乐观控制的冲突阈值。
如果由于事务冲突而被中止的事务百分比超过了 max_conflicts_percent
,那么在 disable_period
个连续事务期间,事务隔离级别将更改为 SERIALIZABLE
以禁用乐观模式。SERIALIZABLE
允许一次只有一个 MCO_READ_WRITE
事务(消除了冲突的可能性),并且可以与 MCO_READ_ONLY
事务并行运行。默认情况下,乐观阈值设置为 100,这意味着“无论发生多少冲突,都永远不会禁用乐观模式”。
调整事务锁定
请注意,当使用 MVCC 时,在执行 B 树索引锁定操作时,可以调整活动写入事务的最大数量。此阈值由传递给 mco_db_open_dev()
的 mco_db_params_t 中的参数 index_optimistic_lock_threshold
设置。
提升 MVCC 性能
已证实,通过使用内部位图可以加快 MVCC 事务管理器的性能。默认情况下,此功能处于禁用状态。应用程序可以通过在传递给 mco_db_open_dev()
的 mco_db_params_t 中为数据库参数 mvcc_bitmap_size
指定非零值来启用此功能。(请注意,指定的值应为 2 的幂次方。)
两阶段提交
某些应用程序需要对事务提交处理进行更精细的控制;具体来说,分两步(阶段)提交事务。第一步将数据写入数据库,将新数据插入索引并检查索引限制(唯一性)(统称为“预提交”),然后将控制权交还给应用程序。第二步完成提交。
此类应用的一个示例是,多个数据库需要同步单个事务内执行的更新操作。另一个示例可能是事务提交包含在涉及其他数据库系统或外部存储的全局事务中。在这种情况下,应用程序会在第一阶段和第二阶段之间协调事务与全局事务。
为了便于这些及类似的使用场景,为 C 应用程序提供了以下两个 API 函数:mco_trans_commit_phase1()
和 mco_trans_commit_phase2()
。
要执行两阶段提交,应用程序需要依次调用提交阶段,而不是调用一个 mco_trans_commit()
。在第一个提交阶段返回后,应用程序除了启动第二个提交阶段或回滚事务外,不能对数据库执行任何操作。此过程在以下代码段中进行了说明:
mco_db_h db;
mco_trans_h t;
...
mco_trans_start(db, MCO_READ_WRITE, _&t);
...
if ( (mco_trans_commit_phase1(t) == MCO_S_OK) && global_transaction() == SUCCESS ) )
{
mco_trans_commit_phase2(t);
}
else
{
mco_trans_rollback(t);
}
请注意
在使用具有持久化数据库的 MVCC 事务管理器时,不支持两阶段提交 API。
事务升级
在数据库中导航以查找所需对象或对象组之后,有时应用程序需要更新所找到的对象。更新数据库需要 MCO_READ_WRITE
事务。为了使应用程序在使用 MURSIW 事务管理器时能够优化事务性能,可以通过调用 mco_trans_upgrade()
将 MCO_READ_ONLY
事务升级为 MCO_READ_WRITE
。此函数尝试将 MCO_READ_ONLY
事务提升到 MCO_READ_WRITE
访问级别。
使用多版本并发控制(MVCC),此调用总是成功的。
使用 MURSIW 时,升级要么成功,此时返回 MCO_S_OK
,要么失败并返回 MCO_E_UPGRADE_FAIL
错误代码。此错误代码表明运行时无法升级事务,因为另一个 MCO_READ_ONLY
事务已请求并获得了升级许可——MURSIW 一次仅允许一个 MCO_READ_WRITE
事务。
为了确保升级能够成功,应将事务作为 MCO_UPDATE
事务(“带有更新意图的读取”)启动,而不是像以下伪代码片段中演示的那样作为 MCO_READ_ONLY
启动:
mco_trans_h t; // 事务句柄
mco_trans_start(db, MCO_UPDATE, MCO_TRANS_FOREGROUND, &t);
read_data(t);
if (some_condition() == TRUE)
{
// 有必要对数据库进行修改。
rc = mco_trans_upgrade(t);
if ( MCO_S_OK == RC )
{
// 该事务现在是读写模式。
write_data(t);
rc = mco_trans_commit(t);
}
else
{
// 升级失败 - 采取适当措施
rc = mco_trans_rollback(t);
}
}
MURSIW 事务管理器使用队列来处理事务。当在 MCO_UPDATE
事务的上下文中调用 mco_trans_upgrade()
时,升级操作必定会成功。MURSIW 中升级事务的实际逻辑相当复杂;有关详细说明,请参阅此处。
获取事务类型和错误状态
有时,执行更新的应用程序模块可能会从应用程序的不同位置被调用。如果传递了事务句柄,此函数可能需要首先确定事务是否为 MCO_READ_WRITE
,然后再继续执行更新。mco_trans_type()
函数就用于此目的。
如果在事务处理过程中出现错误,该事务将进入错误状态,并且该事务内的后续操作将返回 MCO_E_TRANSACT
。在这种情况下,C 应用程序调用函数 mco_get_last_error()
以获取最初导致错误条件的操作的错误代码。
检查事务的内容
mco_trans_iterate()
函数为 C 应用程序提供了遍历事务所做所有修改的能力,读取被修改的对象,并确定应用了何种修改(新增、更新、删除)。该函数的签名是:
extern MCO_RET mco_trans_itertate (
mco_trans_h trans,
mco_trans_iterator_callback_t callback,
void* user_ctx
);
第二个参数是一个由应用程序定义的回调函数,用于检查每个对象,并确定其是否受外部事务等的影响。回调函数接收一个指向被修改对象的句柄、对象的 class_id
、修改操作的 opcode
以及一些应用程序特定的上下文 user_ctx
(应用程序需要传递给回调函数的任何内容)。回调函数将返回 MCO_S_OK
以表明应用程序可以继续遍历事务,或者返回任何其他值以表明存在问题,在这种情况下,应用程序将回滚事务。此函数在与两阶段提交一起使用时特别有用,如以下代码片段所示:
mco_trans_start(db, MCO_READ_WRITE, MCO_TRANS_FOREGROUND, &trans);
...
rc = mco_trans_commit_phase1(&trans);
if (rc == MCO_S_OK)
{
/* 提交到外部数据库 */
rc = mco_trans_iterate(&trans, &my_iterator_callback, my_iterator_context);
if (rc == MCO_S_OK)
{
/* 外部提交成功 */
mco_trans_commit_phase2(&trans);
}
else
{
mco_trans_rollback(&trans);
}
}
事务可以读取、插入、更新或删除单个对象或多个对象,甚至数千个对象,这取决于应用程序的需求。
在单个事务中处理对象块时,可能某个对象已被删除,但在提交事务之前再次访问了该对象。读取或更新操作,或者任何其他访问都将导致致命错误代码 MCO_ERR_OBJECT_HANDLE+N
,其中 N
是源代码中检测到无效句柄的确切位置的行号。
为了防止这种致命错误,C 应用程序可以调用函数 mco_is_object_deleted()
来确定对象是否在当前事务中已被删除。
伪嵌套事务
当两个不同的应用程序函数可能被独立调用或相互调用时,嵌套事务可能是必要的。为了支持这种需求,SmartEDB允许C 应用程序在当前事务提交或中止之前调用mco_trans_start()
或mco_trans_start_ex()
,并使用内部计数器每次递增。而在调用mco_trans_commit()
或mco_trans_rollback()
时递减。
内部事务的提交仅减少嵌套事务计数器,不会执行其他操作,且事务上下文在外部事务完成提交或回滚前保持有效。只有当计数器归零时,运行时才会实际提交事务。
如果内部事务调用了mco_trans_rollback()
,则该事务将进入错误状态,任何后续尝试在最外层事务范围内修改数据库的操作将立即返回错误。对象句柄将变得无效,再次使用这些句柄将引发错误。
外部和内部事务会自动分配更严格的事务类型,而无需应用程序显式升级事务类型。每个事务代码块应根据其自身操作的需求调用mco_trans_start()
并指定适当的事务类型。需要注意的是,内部事务的mco_trans_start()
可能会像mco_trans_upgrade()
一样失败。
以下代码片段展示了嵌套事务的实现:
/* “BankTransaction”类的模式定义*/
class BankTransaction
{
unsigned<4> from;
unsigned<4> to;
hash<from> hFrom[10000];
nonunique hash<to> hTo[10000];
};
/* 插入两条BankTransaction记录 */
int insert_two(mco_db_h db, uint4 from1, uint4 to1, uint4 from2, uint4 to2)
{
MCO_RET rc;
mco_trans_h t;
BamkTransaction b2;
rc = mco_trans_start(db, MCO_READ_WRITE, MCO_TRANS_FOREGROUND, &t);
if ( MCO_S_OK != rc )
return 0;
/* 在 insert_one() 中调用嵌套事务来插入第一个对象 */
insert_one(db, from2, to2 );
/* insert second object */
rc = BankTransaction_new(t, &b2);
if ( MCO_S_OK != rc )
{
mco_trans_rollback(t);
return 0;
}
/* 将值放入第一个新建对象中 */
BankTransaction_from_put(&b2, from1);
BankTransaction_to_put(&b2, to1);
/* 现在提交事务以完成第一个对象的插入操作。 */
return mco_trans_commit(t);
}
/* 在读写事务中插入一条BankTransaction记录 */
MCO_RET insert_one(mco_db_h db, uint4 from, uint4 to )
{
MCO_RET rc;
mco_trans_h t;
BankTransaction b1;
rc = mco_trans_start(db, MCO_READ_WRITE, MCO_TRANS_FOREGROUND, &t);
if (rc ) return 0;
rc = BankTransaction_new(t, &b1);
if ( MCO_S_OK != rc )
{
mco_trans_rollback(t);
return 0;
}
BankTransaction_from_put(&b1s, from);
BankTransaction_to_put(&b1, to);
return mco_trans_commit(t);
}
int main(int argc, char* argv[])
{
MCO_RET rc;
mco_db_h db;
...
/* 执行一个简单的嵌套事务。. */
uint4 from1 = 11, to1 = 16, from2 = 7, to2 = 17;
rc = insert_two(db, from1, to1, from2, to2);
...
}
如果模块insert_two()
中的事务类型为MCO_READ_ONLY
,那么在insert_one()
中启动的嵌套事务会自动将事务类型提升为MCO_READ_WRITE
。这样一来,即使在MCO_READ_ONLY
事务中尝试创建新对象(如代码行rc = Transaction_new(t, &trans)
)时原本会失败,外部事务仍然可以顺利完成。
遗憾的是,C 语言并没有提供一种安全的方式来强制事务的作用范围。因此,应用程序可能会不小心忘记关闭事务,从而无意中创建了伪嵌套事务。
调试未关闭的事务可能相当具有挑战性。为了帮助开发人员更好地跟踪和管理事务的启动与关闭,SmartEDB提供了两个额外的库。如果您遇到相关问题,可以通过联系技术支持人员获取这些库及其使用方法,以简化调试过程。