并发与事务管理
正如“事务与并发控制”页面中所解释的那样,应用程序对所有数据库访问都使用事务阻塞。这使得 SmartEDB 事务管理器能够调度和处理所有数据库操作,无论是简单的只读访问还是修改数据库对象的读写操作。
事务块
SmartEDB 事务块由一组数据库操作组成,这些操作被包含在事务开始和提交或回滚之间。在 C# 应用程序中,通过调用 Connection 类的 startTransaction() 方法的两个版本之一来启动事务:第一个版本接受一个类型为 Database.TransactionType 的单个参数,该参数可以是以下值之一:
- Exclusive:仅适用于单进程、单线程的应用程序
- ReadOnly:用于只读数据库访问
- ReadWrite:用于执行数据库更新
- Update:用于阅读并可能更新数据库
第二个版本的不同之处在于,它允许为事务设置事务优先级和隔离级别。正如“事务优先级和调度”页面中所解释的那样,事务优先级可以是以下之一:
- AboveNormal
- Background
- Foreground
- Highest
- Idle
正如“隔离级别”页面中所解释的那样,隔离级别可以是以下之一:
- Default
- ReadCommitted
- RepeatableRead
- Serializable
当首次调用 startTransaction() 时,事务隔离级别将被设置为默认值,对于 MURSIW 是可串行化,对于 MVCC 是可重复读。(请注意,如果使用 MURSIW,则唯一可能的级别是可串行化。)
事务提交与回滚
要提交事务,请调用 Connection 方法 commitTransaction();要放弃自 startTransaction() 以来的任何数据库操作,请调用 rollbackTransaction()。commitTransaction() 方法有两个版本:第一个不需要参数,第二个允许指定两阶段提交的阶段(1 或 2)(见下文)。
事务管理器
根据“基本概念”页面,提供了三种事务管理器以满足不同应用需求和并发策略。选择合适的事务管理器对性能有重大影响。幸运的是,更改事务管理器只需在实例化数据库时更换加载的库。MURSIW、MVCC 的实现细节可通过链接查看,EXCL 事务管理器不支持 Java 应用。
在 Java 应用中,所有库(包括事务管理器库)在运行时动态加载。MURSIW 是默认事务管理器,无需额外操作即可加载。要加载 MVCC 库,需在 Database 构造函数中传递 MVCCTransactionManager 属性。通过添加 DebugSupport 属性可加载 MVCC 的调试版本;否则,默认加载发布版本。
例如,以下代码片段将加载 MVCC 的调试版本:
Database.Mode mode = Database.Mode.MVCCTransactionManager | Database.Mode.DebugSupport;
Database db = new Database(mode);
请注意,在开发期间建议使用 SmartEDB 库的调试版本,然后在最终发布时切换到发布模式的库。
设置事务优先级和调度策略
正如“事务优先级和调度”页面中所解释的那样,应用程序可以在运行时调整事务优先级和调度策略。事务优先级在调用 StartTransaction() 时指定。应用程序可以通过在传递给 Database 方法 open() 的 Database.Parameters.SchedPolicy 中设置所需的 Database.TransSchedPolicy 来显式定义 MURSIW 调度策略。
多版本并发控制冲突管理
当使用 MVCC 且隔离级别不是可串行化时,读写事务将并发执行。有时并发事务会修改相同的对象,从而产生事务冲突。事务管理器通过终止其中一个冲突事务并让另一个事务将其更新提交到数据库来解决这些冲突。当事务被终止时,应用程序会收到 MCO_E_CONFLICT 错误代码。应用程序有责任通过类似于以下逻辑来管理这种可能性:
conflicts = true;
do {
try
{
con.startTransaction(Database.TransactionType.ReadWrite);
...<update database>...
con.commitTransaction();
conflicts = false;
}
catch (DatabaseError dbe)
{
if ( dbe.errorCode != MCO_E_CONFLICT )
throw dbe;
}
} while ( conflicts );
请注意,当使用多版本并发控制(MVCC)时,应用程序必须能够容忍由于冲突而导致的事务回滚。如果冲突数量过高,可能会因需要重试事务而导致性能急剧下降。出现这种情况时,事务管理器会临时将隔离级别更改为可串行化。
两阶段提交
某些应用程序需要更精细的事务提交控制,具体为分两步提交事务。第一步(预提交)将数据写入数据库、插入新索引并检查唯一性限制,然后返回控制权给应用程序。第二步完成最终提交。典型应用场景包括:
- 多个 SmartEDB 数据库需在单一事务内同步更新。
- SmartEDB 事务作为全局事务的一部分,涉及其他数据库或外部存储。此时,应用程序在第一阶段和第二阶段之间协调 SmartEDB 与全局事务。
执行两阶段提交时,应用程序应依次调用两个提交阶段,而不是使用单个 commitTransaction() 方法。在第一个阶段完成后,应用程序只能启动第二个阶段或回滚事务,不能进行其他数据库操作。此过程如以下代码段所示:
...
con.startTransaction(Database.TransactionType.ReadWrite);
...
if ( (con.commitTransaction(1)) && GlobalTransaction() == SUCCESS ) )
{
con.commitTransaction(2);
}
else
{
con.rollbackTransaction();
}
请注意,在使用具有持久化数据库的 MVCC 事务管理器时,不支持两阶段提交 API。
伪嵌套事务
当应用程序功能可能单独调用或相互调用时,嵌套事务是必要的。SmartEDB 允许 Java 应用在当前事务提交或回滚前调用 startTransaction()。运行时维护一个内部计数器,每次调用 startTransaction() 递增,调用 commitTransaction() 或 rollbackTransaction() 递减。内部事务的提交仅减少计数器,不执行其他操作,直到外部事务提交或回滚,事务上下文保持有效。运行时在计数器归零时才实际提交事务。
如果内部事务调用 rollbackTransaction(),事务进入错误状态,最外层事务范围内的任何后续数据库修改调用将立即返回。
外部和内部事务自动分配更严格的事务类型,无需应用程序手动升级。每个事务代码块只需根据自身操作调用适当的 startTransaction()。注意,内部事务的 startTransaction() 可能会失败。
以下代码片段展示了嵌套事务的实现:
class BankTransaction
{
#pragma warning disable 0649
public uint4 from;
public uint4 to;
#pragma warning restore 0649
};
// Insert two BankTransaction objects
public static bool InsertTwo(Connection con, uint4 from1, uint4 to1, uint4 from2, uint4 to2)
{
try
{
con.StartTransaction(Database.TransactionType.ReadWrite);
// call nested transaction in InsertOne() to insert first object
InsertOne(con, from2, to2);
// insert second object
con.StartTransaction(Database.TransactionType.ReadWrite);
BankTransaction b2 = new BankTransaction();
b2.from = from1;
b2.to = to1;
con.Insert(b2);
con.CommitTransaction(); // commit second object
}
catch (DatabaseError dbe)
{
throw dbe;
}
return true;
}
// insert one BankTransaction record within a read-write transaction */
public static void InsertOne(Connection con, uint4 from, uint4 to )
{
try
{
// insert first object
con.StartTransaction(Database.TransactionType.ReadWrite);
BankTransaction b1 = new BankTransaction();
b1.from = from1;
b1.to = to1;
con.Insert(b1);
con.CommitTransaction(); // commit first object
}
catch (DatabaseError dbe)
{
throw dbe;
}
return true;
}
int main(int argc, char* argv[])
{
bool rc;
Connect con;
...
/* perform a simple nested transaction... */
uint4 from1 = 11, to1 = 16, from2 = 7, to2 = 17;
rc = InsertTwo(con, from1, to1, from2, to2);
...
}
如果 InsertTwo() 模块中的事务类型为只读,InsertOne() 中的嵌套事务会自动将事务类型提升为读写。这样,即使外部事务在只读模式下尝试实例化新对象通常会失败,它也能成功完成。
不幸的是,没有安全的方法来强制事务范围。因此,应用程序可能会因未关闭事务而无意中创建伪嵌套事务。必须小心确保事务正确阻塞。