面向对象的数据库编程与 db4o - 第 2 部分






3.93/5 (8投票s)
本文探讨了 db4o 的客户端-服务器和事务特性。
目录
引言
在本文的第 1 部分中,我讨论了如何使用 db4o 执行 CRUD 操作。(如果您还没有阅读第 1 部分,我建议您阅读,以了解 db4o 的基础知识以及代码示例中使用的对象模型。)
在这一部分中,我将探讨 db4o 在多线程环境和 n 层应用程序中非常有用的其他特性。这些特性包括客户端-服务器特性以及事务和并发支持。在开始之前,请注意所有代码示例都使用 db4o 7.2 的开发版本(可从此处下载)和 C# 3.0。
客户端-服务器部署
在第 1 部分中,我们看到了 db4o 数据库最简单的用法,即独立模式,其中我们通过 Db4oFactory
创建一个 IObjectContainer
类型的实例并与该实例进行交互。虽然这种使用模式足以用于单线程独立或嵌入式应用程序,但它不适用于多个客户端并发与同一数据库交互的应用程序,或数据库客户端和数据服务器位于不同机器上的应用程序。实际上,在独立模式下创建的 IObjectContainer
对象会独占锁定数据库文件,因此没有其他实例可以访问它,更不用说修改它了。幸运的是,db4o 通过其客户端-服务器特性提供了解决方案,该特性包括两种设置模式:嵌入式服务器和网络模式。
嵌入式服务器模式
在嵌入式服务器模式下,单个 IObjectServer
类型的实例用于创建多个 IObjectContainer
实例。所有这些对象都驻留在同一个进程中,并且通信不涉及 TCP/IP(这就是“嵌入式”的原因)。让我们看一些演示此设置的代码
IObjectServer server = null;
try
{
// Create the server instance
server = Db4oFactory.OpenServer(DB_PATH, 0);
// Create the first client via the server instance
using (IObjectContainer client1 = server.OpenClient())
{
Line line = client1.Query<line>(l => { return l.Color == Color.Green; })[0];
line.Color = Color.Black;
// Store... then try to access the line in another client
client1.Store(line);
using (IObjectContainer client2 = server.OpenClient())
{
// Nothing is found because the transaction is not committed
Assert.AreEqual(
0,
client2.Query<line>(l => { return l.Color == Color.Black; }).Count
);
}
// Now, commit
client1.Commit();
using (IObjectContainer client2 = server.OpenClient())
{
// The new line is found now
Assert.AreEqual(
1,
client2.Query<line>(l => { return l.Color == Color.Black; }).Count
);
}
}
}
finally
{
if (server != null)
server.Close();
}
上述代码显示我们可以有多个客户端访问同一个数据库文件。Db4oFactory.OpenServer()
方法的第二个参数是端口号,我们传递 0 来告诉 db4o 使用嵌入式服务器模式而不是网络模式。代码还显示了一个客户端所做的更改如何影响另一个客户端。具体来说,在 client1
提交其更改之前,client2
将完全看不到任何更新。(我们将在事务部分进一步讨论这一点。)
为了使两个客户端之间的交互易于理解,我只使用一个线程来启动这两个客户端。在实际应用程序中,您可能会创建多个线程,每个线程负责一个或多个数据库客户端。(事实上,如果没有这样的用例,那么您应该使用 db4o 的独立模式而不是嵌入式模式。)
网络模式
如果您的数据库部署在与数据库客户端不同的进程或机器中,则需要使用 db4o 的网络模式。使用此模式与嵌入式服务器模式有以下几点不同
- 您将使用
Db4oFactory.OpenClient()
而不是IObjectServer#OpenClient()
。这很有意义,因为您的客户端代码无法访问在服务器端创建的任何IObjectServer
。 - 您需要指定一个服务器侦听的有效端口,并将其作为
Db4oFactory.OpenClient()
的参数传递。 - 最后,服务器必须明确授予某些凭据的访问权限,并且客户端必须使用其中一个凭据才能访问服务器。
很简单,让我们看一些代码
IObjectServer server = null;
try
{
// Create a server socket and have it listen on port 9999
server = Db4oFactory.OpenServer(DB_PATH, 9999);
// Specify some permitted credentials
server.GrantAccess("buuid", "buupass");
server.GrantAccess("somebody", "somepassword");
try
{
// Try to connect using the invalid credential
IObjectContainer client = Db4oFactory.OpenClient("localhost",
9999, "buuid", "any");
Assert.Fail("Wrong password, must not go here");
}
catch (Exception e)
{
Assert.IsTrue(e is InvalidPasswordException);
}
using (IObjectContainer client1 = Db4oFactory.OpenClient("localhost", 9999,
"buuid", "buupass"))
using (IObjectContainer client2 = Db4oFactory.OpenClient("localhost", 9999,
"somebody", "somepassword"))
{
client1.Store(new Line(new CPoint(0, 0), new CPoint(10, 10), Color.YellowGreen));
client1.Commit();
// client2 will see the change
Assert.AreEqual(
1,
( from Line line in client2
where line.Color == Color.YellowGreen
select line
).Count()
);
}
}
finally
{
if (server != null)
server.Close();
}
同样,通过让服务器及其两个客户端在同一个线程上运行,代码大大简化了。如果您将服务器移动到另一个进程或机器,那么行为将保持不变。代码中唯一其他值得注意的地方是查询是用 LINQ 编写的,只是为了展示 db4o 7.2 中可用的 db4o 的 LINQ 提供程序。
带外信令
在网络模式下,有时您会希望客户端能够请求服务器执行 IObjectContainer
的任何 API 中不存在的任意数据相关操作。关闭服务器就是一个例子:虽然您可以调用 IObjectServer#Close()
来关闭数据库服务器,但无法通过 IObjectContainer
接口来做到这一点,而 IObjectContainer
接口是客户端代码唯一可访问的东西。虽然您可以通过在数据库组件前创建一个服务接口来实现此目的,但更省时的替代方法是利用 db4o 的带外信令特性。
为了让服务器响应来自客户端的自定义消息,您只需编写一个实现 IMessageRecipient
接口的类,该接口有一个方法:ProcessMessage(IMessageContext context, object message)
。(message
参数是您的客户端代码将发送到服务器的实际消息对象。)然后,您将通过将其作为参数传递给 IObjectServer#SetMessageRecipient()
方法来注册此接口的实例。(在我看来,db4o 应该有一个接收委托的此方法重载,这样开发人员就不必编写接口实现。)在客户端,您将使用从 IObjectContainer
检索到的 IMessageSender
实例来向服务器发送消息。
下面是一个执行此操作的示例代码
[TestClass]
public class OutOfBandTest : IMessageRecipient
{
public const string DB_PATH = "mypaintdb.db";
IObjectServer server = null;
[TestMethod]
public void TestCloseServer()
{
// Start the server and set initialize message recipient
server = Db4oFactory.OpenServer(DB_PATH, 9999);
server.GrantAccess("buuid", "buupass");
server.Ext().Configure().ClientServer().SetMessageRecipient(this);
using (IObjectContainer client = Db4oFactory.OpenClient("localhost",
9999, "buuid", "buupass"))
{
IMessageSender sender =
client.Ext().Configure().ClientServer().GetMessageSender();
// Send the shutdown message to server
sender.Send(new ShutdownServerMessage());
try
{
client.Store(new Line(null, null));
Assert.Fail("Server is already shutdown. Must not go here.");
}
catch (Exception e)
{
Assert.IsTrue(e is DatabaseClosedException);
}
}
}
public void ProcessMessage(IMessageContext context, object message)
{
if (server != null && message is ShutdownServerMessage)
{
server.Close();
}
}
class ShutdownServerMessage { }
}
这就是 db4o 客户端-服务器特性的工作原理。现在让我们讨论 db4o 对事务的支持。
事务和并发
事务支持
到目前为止,在我们的大多数示例中,我们都使用了 db4o 的隐式事务,该事务在通过 Db4oFactory.OpenFile()
或 IObjectServer#OpenClient()
创建对象容器时自动启动新事务,并在容器关闭(或处置)时提交更改。我们可以使用 IObjectContainer
的 Commit()
和 RollBack()
方法手动管理 db4o 事务,而不是让 db4o 自动控制事务。
现在,假设我们需要编写一个函数来更改数据库中所有现有线条形状的颜色,并且我们需要确保此操作是原子执行的,即要么所有线条都更新,要么没有线条更新;在这种情况下,显式事务管理绝对是必要的。让我们看看执行此操作的代码
IObjectContainer container = null;
try
{
container = Db4oFactory.OpenFile(DB_PATH);
IObjectSet set = container.QueryByExample(typeof(Line));
Assert.IsTrue(set.Count > 0);
// Change color of all lines
for (var i = 0; i < set.Count; i++)
{
((Line)set[i]).Color = Color.Purple;
container.Store(set[i]);
// After storing 2 lines, deliberately fail
if (i == 1)
{
throw new Exception();
}
}
container.Commit();
}
catch
{
container.Rollback();
// Strange enough, this asserts to true...
Assert.AreEqual(
2,
container.Query<line>(line => {return line.Color == Color.Purple;}).Count
);
}
finally
{
container.Close();
}
using (container = Db4oFactory.OpenFile(DB_PATH))
{
Assert.AreEqual(
0,
container.Query<line>(line => { return line.Color == Color.Purple; }).Count
);
}
您可以看到,我故意在两条线的颜色更改后抛出了一个异常。有趣的是,回滚后的断言代码表明实际上有两条线的颜色发生了变化。这难道不令人意外吗?因为回滚应该会清除事务中的所有更改。好吧,请记住在本文的第 1 部分中,我提到了 db4o 的对象缓存,它在对象容器的生命周期中维护对所有存储或检索到的对象的引用。回到我们的示例,当容器从数据库中获取线条形状列表时,它注意到某些线条已经存在于其缓存中,并返回这些缓存的引用,而不是创建并返回新实例。换句话说,虽然回滚操作成功并且数据库已正确更新(否则,我们将收到异常),但返回的对象是内存中已有的对象,而不是从数据库检索到的数据创建的新实例。要检索数据库副本,我们可以简单地在新容器中执行查询,如代码示例所示。另一种方法是在运行查询之前通过调用 IObjectContainer#Ext().Purge()
清除缓存。
现在我们已经了解了如何显式使用 Commit()
和 Rollback()
来确保一系列数据操作的原子性,接下来让我们看看 db4o 如何在客户端-服务器模式下多个并发访问同一数据库时确保数据完整性。回想一下嵌入式服务器部分的示例代码,client2
只能在 client1
调用 IObjectContainer#Commit()
方法后才能看到 client1
所做的更改。这正是 db4o 控制并发的方式:db4o 的所有事务都使用“读已提交”隔离级别。在此隔离级别下,一个客户端无法看到另一个客户端未提交的更改,因此客户端无法操作将要回滚的数据(例如,“脏读”)。另一方面,此隔离级别不足以防止“不可重复读”和“幻读”,并且我们必须在应用程序中显式编写代码来处理这些场景,因为 db4o 目前仅支持“读已提交”隔离级别。由于 db4o 内置的“读已提交”隔离级别已在嵌入式服务器的示例代码中演示过,因此我将不再提供另一个示例。相反,我们将看看如何在应用程序代码中使用 db4o 的信号量来控制事务隔离。
信号量
db4o 提供信号量,允许开发人员控制对应用程序代码关键部分的访问。信号量由名称标识,可以通过调用 IObjectContainer#Ext().SetSemaphore(string name, int waitForAvailability)
由对象容器获取,如果成功获取信号量则返回 true
,否则返回 false
。waitForAvailability
参数表示在信号量已被其他容器获取的情况下方法需要返回的毫秒数,0 表示立即返回。当对象容器成功获取信号量时,在通过调用 IObjectContainer#Ext().ReleaseSemaphore(string name)
释放信号量之前,其他容器无法获取它。使用信号量是一种悲观锁定策略,您甚至可以通过在通过对象容器执行任何数据操作之前显式获取信号量来模仿可序列化隔离级别。
现在,让我们编写一个客户端-服务器代码,它有两个客户端:一个检索所有线条形状并尝试更改它们的颜色,另一个在另一个线程上运行的客户端尝试更新特定线条的颜色。假设有一个要求,即当 client1
正在更新所有线条时,client2
不能执行任何更新。(请耐心听我说;在这个简单的绘图域中很难想到好的并发场景,而且我太懒了,不想再创建一个对象模型来演示信号量功能。)
IObjectServer server = null;
try
{
server = Db4oFactory.OpenServer(DB_PATH, 9999);
server.GrantAccess("buuid", "buupass");
// This client updates the color of all lines
Thread clientThread1 = new Thread(delegate()
{
using (IObjectContainer client1 = Db4oFactory.OpenClient("localhost",
9999, "buuid", "buupass"))
{
// Acquire the semaphore
if (client1.Ext().SetSemaphore(LOCK_KEY, 0))
{
IObjectSet set = client1.QueryByExample(typeof(Line));
foreach (Line line in set)
{
line.Color = Color.RosyBrown;
client1.Store(line);
Thread.Sleep(500);
}
}
}
});
// This client tries to delete the first line
Thread clientThread2 = new Thread(delegate()
{
using (IObjectContainer client2 = Db4oFactory.OpenClient("localhost",
9999, "buuid", "buupass"))
{
// Cannot acquire semaphore with this call
Assert.IsFalse(client2.Ext().SetSemaphore(LOCK_KEY, 0));
// After 5 seconds, client1 already releases the lock
// (there're only 4 Lines in the DB)
// thus, this call would succeed
Assert.IsTrue(client2.Ext().SetSemaphore(LOCK_KEY, 5000));
// Test another call - pass because client2 is already the lock owner
if (client2.Ext().SetSemaphore(LOCK_KEY, 0))
{
IObjectSet set = client2.QueryByExample(typeof(Line));
((Line)set[0]).Color = Color.Black;
client2.Store(set[0]);
}
}
});
// Start client 1, then sleep a bit to make sure the semaphore is acquired
// before starting client 2
clientThread1.Start();
Thread.Sleep(1000);
clientThread2.Start();
// Wait for the threads to terminate...
clientThread1.Join();
clientThread2.Join();
}
finally
{
if (server != null)
server.Close();
}
敏锐的读者会问:“嘿,client1
从未释放信号量,因为它没有调用 IObjectContainer#Ext().ReleaseSemaphore()
。client2
怎么能获取信号量呢?” 好问题。我故意这样做是为了演示 db4o 信号量的另一个有趣点:当客户端对象容器关闭时,服务器会检测到连接断开并释放该客户端之前获取的所有信号量。然而,这仅用于演示目的,我建议开发人员在应用程序代码中明确释放信号量,而不是依赖 db4o 的连接检测机制,因为在客户端崩溃的情况下,该机制可能具有时间不确定性。
结论
我们已经讨论了 db4o 的客户端-服务器特性以及事务和并发支持。但这并非结束,因为 db4o 仍然有许多有趣的特性值得研究。然而,本文介绍的这两个特性,连同第 1 部分中介绍的知识,应该足以让您在比独立或嵌入式应用程序更高级的场景中使用 db4o。希望您喜欢阅读这篇文章!