ServiceStack / FluentNHibernate / MySQL - 两个并发请求使用的相同连接

本文关键字:请求 连接 并发 MySQL FluentNHibernate ServiceStack 两个 | 更新日期: 2023-09-27 18:35:29

我们似乎遇到了一个奇怪的问题,其中对我们服务的两个并发请求实际上使用相同的数据库连接。

我们的设置是ServiceStack + NHibernate+ FluentNHibernate+ MySQL。我已经设置了一个小测试来重现问题:

public class AppHost : AppHostBase
{
    private ISessionFactory _sessionFactory;
    public AppHost() : base("Lala Service", typeof(AppHost).Assembly)
    {
    }
    public override void Configure(Container container)
    {
        _sessionFactory = Fluently.Configure()
            .Database(MySQLConfiguration.Standard.ConnectionString(conn =>
                conn.Server("localhost").Username("lala").Password("lala").Database("lala")))
            .Mappings(mappings => mappings.AutoMappings.Add(
                AutoMap.Assembly(GetType().Assembly).Where(t => t == typeof(Lala))
                       .Conventions.Add(DefaultLazy.Never(), DefaultCascade.All())))
        .BuildSessionFactory();
        container.Register(c => _sessionFactory.OpenSession()).ReusedWithin(ReuseScope.Request);
    }
}

public class Lala
{
    public int    ID   { get; set; }
    public string Name { get; set; }
}
[Route("/lala")]
public class LalaRequest
{
}
public class LalaReseponse
{
}

public class LalaService : Service
{
    private ISession _session;
    public ISession Session1
    {
        get { return _session; }
        set { _session = value; }
    }
    public LalaReseponse Get(LalaRequest request)
    {
        var lala = new Lala
        {
            Name = Guid.NewGuid().ToString()
        };
        _session.Persist(lala);
        _session.Flush();
        lala.Name += " XXX";
        _session.Flush();
        return new LalaReseponse();
    }
}

我通过 Ajax 同时点击了这项服务 10 次,如下所示:

    <script type="text/javascript">
        for (i = 0; i < 10; i++) {
            console.log("aa");
            $.ajax({
                url:      '/lala',
                dataType: 'json',
                cache:    false
            });
        }
    </script>

结果一致为:

  1. 打开的连接数<10。
  2. 并非所有记录都已更新。
  3. 有时 - 抛出StaleObjectStateException - 如果我删除记录。

这背后的原因是连接被两个并发请求重用,然后 LAST_INSERT_ID() 给出错误行的 ID,因此两个请求正在更新同一行。

简而言之:这完全是一团糟,它显然在请求之间共享数据库连接。

问题是:为什么?我应该如何配置内容,以便每个请求从连接池获得自己的连接?

ServiceStack / FluentNHibernate / MySQL - 两个并发请求使用的相同连接

终于解决了,真是浪费一天!

问题的根源是NHibernate的连接释放模式:

11.7. 连接释放模式

NHibernate关于 ADO.NET 的遗留(1.0.x)行为 连接管理是 ISession 将获得连接 当第一次需要它时,然后保持该连接,直到 会议已关闭。NHibernate引入了连接的概念 释放模式,用于告知会话如何处理其 ADO.NET 连接。 ... 不同的释放模式由 NHibernate.ConnectionReleaseMode:

  • OnClose - 本质上是上述遗留行为。这 NHibernate 会话在首次需要执行时获取连接 某些数据库访问并保留该连接直到会话 已关闭。

  • 交易后 - 说在 NHibernate.ITransaction已完成。

使用配置参数hibernate.connection.release_mode 以指定要使用的发布模式。

  • after_transaction - 说使用 ConnectionReleaseMode.AfterTransaction. 请注意,使用 ConnectionReleaseMode.AfterTransaction,如果会话被视为 处于自动提交模式(即未启动任何事务)连接 将在每次操作后释放

这与MySQL .NET/Connector的默认连接池纠缠在一起,实际上意味着连接在并发请求之间交换,因为一个请求将连接释放回池,另一个请求获取连接。

但是,我认为NHibernate在释放并重新获取连接后调用LAST_INSERT_ID()的事实是一个错误。它应该在同一"操作"中调用LAST_INSERT_ID()

无论如何,解决方案:

  1. 使用交易,这是我们通常所做的,或者
  2. 如果由于某种原因不能或不想在特定上下文中使用事务(这就是今天使用的情况),请将连接释放模式设置为"关闭时"。使用FluentNHibernate,这将是:

    .ExposeConfiguration(cfg =>
        cfg.SetProperty("connection.release_mode", "on_close"));
    

    从这里开始,即使没有事务,连接也会绑定到会话。