每个客户端的已分离实体或会话的更改跟踪
本文关键字:会话 跟踪 分离 客户端 实体 | 更新日期: 2023-09-27 18:22:07
我开发了一个具有持久客户端连接(非基于请求)的服务器。当我在内存中跟踪每个连接的客户端状态时,如果每次需要访问此类客户端数据时都加载实体,那将是很奇怪的。
所以我有我的分离实体,当我需要执行任何更改时,我不会直接应用它们,而是将这些更改和分离实体作为请求传递给GameDb类。它对该实体执行更改,然后从数据库加载相同的实体,对会话拥有的实体再次执行相同的更改,以便NH可以跟踪这些更改。
我可以使用Merge,但它要慢得多,因为NH应该加载所有实体数据(包括可能未修改的惰性集合)来检查每个属性的更改。在我的情况下,性能至关重要。
一个例子:
public void GameDb.UpdateTradeOperation(UserOperation operation, int incomeQuantity, decimal price)
{
if (operation == null) throw new ArgumentNullException("operation");
if (operation.Id == 0) throw new ArgumentException("operation is not persisted");
_operationLogic.UpdateTradeOperation(operation, incomeQuantity, price);
try
{
_factory.Execute(
s =>
{
var op = s.Get<UserOperation>(operation.Id);
_operationLogic.UpdateTradeOperation(op, incomeQuantity, price);
if (op.User.BalanceFrozen != operation.User.BalanceFrozen)
throw new Exception("Inconsistent balance");
}); // commits transaction if no exceptions thrown
}
catch (Exception e)
{
throw new UserStateCorruptedException(operation.User, null, e);
}
}
这种方法带来了一些过于复杂的问题,因为我需要将每个更改应用两次,并检查结果状态是否相等。如果我可以使用NH会话来监视实体更改,那会更容易。但不建议NH会议长期开放,我可以有数千个这样长寿的开放会议。
它还迫使我分裂我的实体和共同的逻辑。问题是,GameDb类不知道它是从哪个上下文调用的,也不能为其操作请求任何额外的数据(例如当前价格或客户端套接字无效计时器或许多其他东西),或者它可能需要有条件地(根据自己的决定)向客户端发送一些数据。当然,我可以将一堆委托传递给GameDb方法,但在我看来这不是一个好的解决方案。
我可以使用Session.Lock附加未更改的分离实体吗?这样我就不需要执行两次更改了?我应该使用什么锁定模式?
我能用一个更好的方法吗?如果我为每个客户端保留一个打开的会话,但快速提交或回滚事务,它还会打开很多连接吗?事务完成后,会话是否会保持实体状态?
我在每个客户端会话的长期使用中会遇到什么样的并发问题:
- 如果我仅从其自己的线程光纤(或锁)操作每个用户实体
- 如果我从"错误"会话(从该会话的线程)请求另一个只读用户配置文件
我认为您需要做的是使用二级缓存,并为每个连接的客户端存储实体的Id,而不是将实体存储在内存中。
当客户端连接时,您可以使用存储的Id获取实体,这甚至不会在后续请求中访问数据库,因为它将从二级缓存中获取实体,并且您不需要担心更改跟踪。
http://ayende.com/blog/3976/nhibernate-2nd-level-cache
我尝试使用Session.Lock(LockMode.None)将分离的实体重新附加到新的会话中,它成功了。它将一个对象添加到会话中,使其保持干净和不变。我可以修改它,它将在下一次事务提交时存储到数据库中。
这比合并要好,因为Nhibernate不需要查看所有属性来找出更改的内容。
如果我更改了至少一个属性,它会更新整个对象(我指的是所有属性,但没有集合和实体链接,如果它们没有被触摸的话)。我在实体映射中设置DynamicUpdate=true,现在它只更新已更改的属性。
如果我在当前会话之外更改被取消连接实体的任何属性,则对session.Lock的下一次调用将引发异常(尤其是如果我更改集合内容,则异常状态为"重新关联的对象具有脏集合")。我在会话之外做这些更改,因为我不需要保存它们(一些带有引用的东西)。
很奇怪,但当我给Lock打两次电话时,它就完美地工作了!
try
{
s.Lock(DbEntity, LockMode.None); // throws
}
catch
{
s.Lock(DbEntity, LockMode.None); // everything ok
}
同样对于集合:在我找到上面的解决方案之前,我将它们投射到IPersistentCollection并使用ClearDirty()。
那么并发呢?我的代码表明,每个线程纤程只更新其用户,除此纤程外,任何人都无权对实体进行写访问。
所以模式是:
- 我打开一个会话,获取一个实体并将其存储在内存中的某个位置
- 当我需要阅读它的属性时,我可以在任何时候快速阅读
- 当我想修改它时,我打开一个新的会话并对它执行Lock()。在应用更改后,我提交事务并关闭会话