11

我不敢相信让某人向我展示一个简单的工作示例是如此困难。这让我相信每个人都只能像他们知道该怎么做一样说话,但实际上他们不知道。

我将帖子缩短为仅我希望示例执行的操作。也许这个帖子太长了,把人们吓跑了。

为了获得这个赏金,我正在寻找一个可以在 VS 2010 中复制并运行的工作示例。

示例需要做什么。

  1. 在 mssql 2008 中将版本的版本显示为我的域中的数据类型作为时间戳
  2. 显示 nhibernate 自动抛出“StaleObjectException”
  3. 向我展示这 3 个场景的工作示例

方案 1

用户 A 来到站点并编辑 Row1。用户 B 来(注意他可以看到第 1 行)并单击编辑第 1 行,用户 B 应该被拒绝编辑行,直到用户 A 完成。

方案 2

用户 A 来到站点并编辑 Row1。用户 B 30 分钟后来,点击编辑第 1 行。用户 B 应该能够编辑此行并保存。这是因为用户 A 编辑该行的时间过长,失去了编辑权限。

方案 3

用户 A 从离开中回来。他单击更新行按钮,应该会收到 StaleObjectException。

我正在使用 asp.net mvc 和流利的 nhibernate。寻找要在这些中完成的示例。


我试过的

我尝试构建自己的,但我无法让它抛出 StaleObjectException,也无法让版本号增加。我厌倦了打开 2 个单独的浏览器并加载了索引页面。两种浏览器都显示相同的版本号。

public class Default1Controller : Controller
{
    //
    // GET: /Default1/

    public ActionResult Index()
    {
        var sessionFactory = CreateSessionFactory();

        using (var session = sessionFactory.OpenSession())
        {
            using (var transaction = session.BeginTransaction())
            {
                var firstRecord = session.Query<TableA>().FirstOrDefault();
                transaction.Commit();
                return View(firstRecord);
            }

        }

    }

    public ActionResult Save()
    {
        var sessionFactory = CreateSessionFactory();
        using (var session = sessionFactory.OpenSession())
        {
            using (var transaction = session.BeginTransaction())
            {
                var firstRecord = session.Query<TableA>().FirstOrDefault();
                firstRecord.Name = "test2";
                transaction.Commit();
                return View();
            }
        }
    }

    private static ISessionFactory CreateSessionFactory()
    {
        return Fluently.Configure()
            .Database(MsSqlConfiguration.MsSql2008
                .ConnectionString(c => c.FromConnectionStringWithKey("Test")))
            .Mappings(m => m.FluentMappings.AddFromAssemblyOf<TableA>())
                             //  .ExposeConfiguration(BuidSchema)
            .BuildSessionFactory(); 
    }


    private static void BuidSchema(NHibernate.Cfg.Configuration config)
    {
        new NHibernate.Tool.hbm2ddl.SchemaExport(config).Create(false, true);
    }

}


public class TableA
{
    public virtual Guid Id { get; set; }
    public virtual string Name { get; set; }

    // Not sure what data type this should be for timestamp.
    // To eliminate changing to much started with int version
    // but want in the end timestamp.
    public virtual int Version { get; set; } 
}

public class TableAMapping : ClassMap<TableA>
{
    public TableAMapping()
    {
        Id(x => x.Id);
        Map(x => x.Name);
        Version(x => x.Version);
    }
}
4

7 回答 7

6

nhibernate 会停止检索该行吗?

不可以。锁只在事务的范围内放置,在 Web 应用程序中,事务在请求结束时结束。此外,事务隔离模式的默认类型是已提交读,这意味着一旦选择语句终止,就会释放读锁。如果您在同一个请求和事务中读取和编辑,您可以在手头的行上放置读写锁,这将阻止其他事务写入或读取该行。但是,这种类型的并发控制在 Web 应用程序中效果不佳。

或者用户 B 是否仍能看到该行但如果他试图保存它会崩溃?

如果使用 [乐观并发],就会发生这种情况。在 NHibernate 中,乐观并发通过添加一个版本字段来工作。保存/更新命令与更新所基于的版本一起发出。如果这与数据库表中的版本不同,则不会更新任何行并且 NHibernate 将抛出。

如果用户 A 说取消并且不编辑会发生什么。我必须自己释放锁还是可以设置超时来释放锁?

不,锁在请求结束时被释放。

总的来说,你最好的选择是选择 NHibernate 管理的版本字段的乐观并发。

于 2012-09-04T23:51:09.553 回答
2

它在代码中看起来如何?我是否在我的流利的 nhibernate 中设置以生成时间戳(不确定我是否会时间跨度数据类型)。

我建议使用版本列。如果您将 FluentNhibernate 与自动映射一起使用,那么如果您创建一个名为 Version 类型为 int/long 的列,默认情况下它将使用该列进行版本化,或者您可以使用映射中的 Version() 方法来执行此操作(类似时间戳)。

所以现在我以某种方式生成了时间戳,并且用户正在编辑一行(通过 gui)。我应该将时间戳存储在内存中吗?那么当用户从内存中提交调用时,该行的时间戳和ID并检查?

当用户开始编辑一行时,您检索它并存储当前版本(版本属性的值)。我建议将当前版本放在表单的隐藏字段中。当用户保存他的更改时,您可以手动检查数据库中的版本(检查它是否与隐藏字段中的版本相同),或者您可以将版本属性设置为隐藏字段中的值(如果您使用数据绑定,您可以自动执行此操作)。如果您设置了版本属性,那么当您尝试保存实体时,NHibernate 将检查您保存的版本是否与数据库中的版本匹配,如果不匹配则抛出异常。

NHibernate 将发出更新查询,例如:

更新 xyz SET ,版本 = 16 其中 Id = 1234 AND 版本 = 15

(假设您的版本是 15) - 在此过程中,它还将增加版本字段

如果是这样,这意味着业务逻辑正在跟踪“行锁定”,但理论上有人仍然可以去 Where(x => x.Id == id) 并随意抓取该行并进行更新。

如果其他人通过 NHibernate 更新该行,它将自动增加版本,因此当您的用户尝试使用错误的版本保存它时,您将收到一个异常,您需要决定如何处理(即尝试显示一些合并屏幕,或告诉用户使用新数据重试)

当行被更新时会发生什么?您是否将 null 设置为时间戳?

它会自动更新版本或时间戳(时间戳将更新为当前时间)

如果用户从未真正完成更新并离开会发生什么。每行如何再次解锁?

该行本身没有锁定,而是使用乐观并发,您假设没有人会同时更改同一行,如果有人这样做,那么您需要重试更新。

是否仍然存在竞争条件,或者这几乎不可能发生?我只是担心 2 个人尝试编辑同一行,他们都在他们的 gui 中看到它进行编辑,但实际上一个人最终会被拒绝,因为他们失去了竞争条件。

如果 2 个人尝试同时编辑同一行,如果您使用乐观并发,其中一个会丢失。好处是他们会知道存在冲突,而不是丢失他们的更改并认为它已更新,或者在不知道的情况下覆盖其他人的更改。

所以我做了这样的事情

var test = session.Query.Where(x => x.Id == id).FirstOrDefault(); // 发送给用户进行编辑。有版本控制。用户编辑并在 30 分钟后发回数据。

代码确实

测试.Id = vm.Id; test.ColumnA = vm.ColumnA; test.Version = vm.Version;

会话.更新(测试);session.Commit(); 所以上面的工作对吗?

如果其他人进入并更改了行,则上述内容将引发异常。这就是重点,所以您知道出现了并发问题。通常,您会向用户显示一条消息,说“其他人已更改此行”,其中包含新行以及他们的更改,因此用户必须选择哪些更改获胜。

但如果我这样做

测试.Id = vm.Id; test.ColumnA = vm.ColumnA;

session.Update(test);
session.Commit(); it would not commit right?

只要您没有重新加载测试就正确(即您做了 test = new Xyz(),而不是 test = session.Load() ),因为行上的时间戳不匹配

如果其他人通过 NHibernate 更新该行,它将自动增加版本,因此当您的用户尝试使用错误的版本保存它时,您将收到一个异常,您需要决定如何处理(即尝试显示一些合并屏幕,或告诉用户使用新数据重试)

我可以做到这一点,当记录被抓住这个检查。一开始我想保持简单,一次只有一个人可以编辑。其他人甚至无法访问记录进行编辑,而正在编辑它。

这不是乐观的并发。作为一个简单的答案,您可以添加一个 CheckOutDate 属性,您在有人开始编辑它时设置该属性,并在他们完成时将其设置为 null。然后,当他们开始编辑时,或者当您向他们展示要编辑的行时,您可以排除 CheckOutDate 比过去 10 分钟更新的所有行(那么您不需要计划任务来定期重置它)

该行本身没有锁定,而是使用乐观并发,您假设没有人会同时更改同一行,如果有人这样做,那么您需要重试更新。

我不确定你说的这意味着我能做什么

session.query.Where(x => x.id == id).FirstOrDefault(); 一整天,它会不断让我记录(认为它会不断增加版本)。

查询不会增加版本,只有对它的更新才会增加版本。

于 2012-09-13T17:33:33.627 回答
0

对您的问题的简短回答是您不能/不应该在具有 nhibernate 乐观(版本)和悲观(行锁)锁定的简单 Web 应用程序中执行此操作。您的交易仅在请求时才进行这一事实是您的限制因素。

您可以做的是创建另一个表和实体类,以及管理这些“锁”的映射。在最低级别,您需要正在编辑的对象的 Id 和执行编辑的用户的 Id,以及获取锁的日期时间。我会将正在编辑的对象的 Id 作为主键,因为您希望它是独占的...

当用户单击要编辑的行时,您可以尝试获取锁(在该表中创建一条带有 id 和当前日期时间的新记录)。如果另一个用户的锁已经存在,那么它将失败,因为您试图违反主键约束。

如果获取了锁,当用户单击保存时,您需要在执行实际保存之前检查他们是否仍然拥有有效的“锁”。然后,执行实际保存并删除锁定记录。

我还建议使用后台服务/进程定期清除这些锁并删除已过期或超过您的时间限制的锁。

这是我在 Web 环境中处理“锁”的规定方式。祝你好运!

于 2012-10-04T14:54:59.227 回答
0

你看过 ISaveOrUpdateEventListener 接口吗?

public class SaveListener : NHibernate.Event.ISaveOrUpdateEventListener
{

    public void OnSaveOrUpdate(NHibernate.Event.SaveOrUpdateEvent e)
    {
        NHibernate.Persister.Entity.IEntityPersister p = e.Session.GetEntityPersister(null, e.Entity);
        if (p.IsVersioned)
        {
            //TODO: check types etc...
            MyEntity m = (MyEntity) e.Entity;
            DateTime oldversion = (DateTime) p.GetVersion(m, e.Session.EntityMode);
            DateTime currversion = (DateTime) p.GetCurrentVersion(m.ID, e.Session);

            if (oldversion < currversion.AddMinutes(-30))
                throw new StaleObjectStateException("MyEntity", m.ID);
        }
    }

}

然后在您的配置中,注册它。

    private static void Configure(NHibernate.Cfg.Configuration cfg)
    {
        cfg.EventListeners.SaveOrUpdateEventListeners = new NHibernate.Event.ISaveOrUpdateEventListener[] {new SaveListener()};

    }



    public static ISessionFactory CreateSessionFactory()
    {
        return Fluently.Configure().Database(...).
                    .Mappings(...)
                    .ExposeConfiguration(Configure)                       
                    .BuildSessionFactory();
    }

并在 Mapping 类中对要版本化的属性进行版本化。

public class MyEntityMap: ClassMap<MyENtity>
{
    public MyEntityMap()
    {
        Table("MyTable");

        Id(x => x.ID);
        Version(x => x.Timestamp);
        Map(x => x.PropA);
        Map(x => x.PropB);

    }
}
于 2012-10-03T14:22:59.923 回答
0

我对 nHibernate 本身知之甚少,但是如果您准备在数据库上创建一些存储过程,它可以>排序<完成。

您将需要一个额外的数据列和对象模型中的两个字段来存储每一行​​的信息:

  • 除散列字段本身和 EditTimestamp 字段之外的所有字段值的“散列”(使用 SQL Server CHECKSUM 2008 及更高版本或 HASHBYTES 用于早期版本)。如果需要,可以使用 INSERT/UPDATE 触发器将其持久化到表中。
  • 日期时间类型的“编辑时间戳”。

更改您的程序以执行以下操作:

  • 'select' 过程应该包括一个类似于 'edit-timestamp < (Now - 30 minutes)' 的 where 子句,并且应该将 'edit-timestamp' 更新为当前时间。在更新行之前使用适当的锁定运行选择我正在考虑一个带有保持锁定的存储过程,例如这里 的使用持久日期/时间而不是像 GETDATE() 这样的东西。

示例(使用固定值):

BEGIN TRAN

DECLARE @now DATETIME 
SET @now = '2012-09-28 14:00:00'

SELECT *, @now AS NewEditTimestamp, CHECKSUM(ID, [Description]) AS RowChecksum
FROM TestLocks
WITH (HOLDLOCK, ROWLOCK)
WHERE ID = 3 AND EditTimestamp < DATEADD(mi, -30, @now)

/* Do all your stuff here while the record is locked */
UPDATE TestLocks
SET EditTimestamp = @now
WHERE ID = 3 AND EditTimestamp < DATEADD(mi, -30, @now)

COMMIT TRAN

如果您从该过程中返回一行,那么您“拥有”“锁”,否则,将不会返回任何行,也没有可编辑的内容。

  • “更新”过程应添加类似于“哈希 = 先前返回的哈希”的 where 子句

示例(使用固定值):

BEGIN TRAN

    DECLARE @RowChecksum INT
    SET @RowChecksum = -845335138

    UPDATE TestLocks
    SET [Description] = 'New Description'
    WHERE ID = 3 AND CHECKSUM(ID, [Description]) = @RowChecksum

    SELECT @@ROWCOUNT AS RowsUpdated

COMMIT TRAN

所以在你的场景中:

  1. 用户 A 编辑一行。当您从数据库返回此记录时,“编辑时间戳”已更新为当前时间,并且您有一行,因此您知道可以编辑。用户 B 不会得到一行,因为时间戳仍然太新。

  2. 用户 B 在 30 分钟后编辑该行。因为时间戳已经过去了 30 多分钟,所以他们得到了回击。字段的哈希值将与 30 分钟前用户 A 的哈希值相同,因为没有写入任何更新。

  3. 现在用户 B 更新。先前检索到的散列仍然与行中字段的散列匹配,因此更新语句成功,并且我们返回行计数以表明该行已更新。但是,用户 A 尝试下一步更新。因为描述字段的值发生了变化,散列值也发生了变化,因此 UPDATE 语句没有更新任何内容。我们得到“零行更新”的结果,因此我们知道该行已被更改或该行已被删除。

在所有这些锁进行的情况下,可能存在一些关于可伸缩性的问题,并且可以优化上述代码(例如,使用 UTC 可能会遇到时钟前进/后退的问题),但我编写这些示例只是为了解释它是如何工作的。

除此之外,如果不使用 select 事务中的数据库级行锁定,我看不出如何做到这一点。可能您可以通过 nHibernate 请求这些锁,但这超出了我对 nHibernate 的了解。

于 2012-09-28T14:19:26.377 回答
0

是的,可以用 nhibernate 锁定一行,但如果我理解得很好,你的场景是在 web 上下文中,那么这不是最佳实践。

如您所述,最好的做法是使用带有自动版本控制的乐观锁定。在页面打开时锁定一行并在页面卸载时释放它会很快导致死锁行(javascript问题,页面未正确终止......)。乐观锁定将使 NHibernate 在刷新包含由另一个会话修改的对象的事务时抛出异常。如果您想对相同的信息进行真正的并发修改,您可以尝试考虑一个将许多用户输入合并到同一个文档中的系统,但它是一个独立的系统,不受 ORM 管理。

您将不得不选择一种方式来处理 Web 环境中的会话。 http://nhibernate.info/doc/nh/en/index.html#transactions-optimistic

与高并发和高可伸缩性一致的唯一方法是带有版本控制的乐观并发控制。NHibernate 提供了三种可能的方法来编写使用乐观并发的应用程序代码。

于 2012-09-13T09:58:03.480 回答