MySQL异步

ClownFish同时支持3个不同的 MySQL 客户端:

  • MySql.Data
    https://dev.mysql.com/downloads/connector/net/
    它是MySQL的官方客户端,历史较久,因此稳定性会比较好,但是它不支持TAP风格的异步模式。

  • MySqlConnector老版本
    0.x 版本,使用 MySql.Data.MySqlClient 命名空间

  • MySqlConnector新版本
    1.0 以上版本,使用 MySqlConnector 命名空间
    https://mysqlconnector.net/
    它虽然不是官方提供,但它却支持TAP风格的异步模式,而且拥有较好的性能。



切换选择

Nebula已引用 MySqlConnector 2.x ,因此项目中不需要再引用此包。
如果你需要使用 MySql.Data ,请手动添加引用,并按照下面介绍的方法来切换。

强烈建议:不要使用 MySql.Data,除非你要访问阿里 ADS ,这方面请阅读本文后续部分。

我们可以通过 环境变量 或者 ClownFish.App.config 来切换使用不同的客户端。

  • MySqlClientProviderSupport=1
    在内部注册:ProviderName=MySql.Data.MySqlClient 指向 MySql.Data

  • MySqlClientProviderSupport=2
    在内部注册:ProviderName=MySql.Data.MySqlClient 指向 MySqlConnector

  • MySqlClientProviderSupport=3
    同时支持 MySql.Data 和 MySqlConnector

    • ProviderName=MySql.Data.MySqlClient,指向 MySqlConnector
    • ProviderName=MySqlConnector,指向 MySqlConnector
    • ProviderName=MySql.Data,指向 MySql.Data
  • MySqlClientProviderSupport=0 或者 不指定此配置

    • 由框架自动判断,引用哪个客户端就使用哪个

说明:

  • 2类客户端的行为存在少量差别,具体可参考:https://mysql-net.github.io/AdoNetResults/
  • 在使用 MySql.Data 时,对于某个XxxxAsyn方法,其实它是以同步方式执行的,在切换到MySqlConnector后请做好全面测试,因为那些XxxxAsyn方法将以异步方式执行。
  • 建议分批次切换生产环境中应用程序,如果发现行为有异常,立即切换到默认设置。

配置示例:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <appSettings>
        <add key="MySqlClientProviderSupport" value="2" />
    </appSettings>    
</configuration>



确认设置生效

可以查看应用程序的控制台输出,确认包含:

Register DbClient Provider: MySql.Data.MySqlClient => ClownFish.Data.MultiDB.MySQL.MySqlConnectorClientProvider



MySqlClientProviderSupport=3

启动时会输出:

Register DbClient Provider: MySql.Data.MySqlClient => ClownFish.Data.MultiDB.MySQL.MySqlConnectorClientProvider
Register DbClient Provider: MySql.Data => ClownFish.Data.MultiDB.MySQL.MySqlDataClientProvider
Register DbClient Provider: MySqlConnector => ClownFish.Data.MultiDB.MySQL.MySqlConnectorClientProvider

在这种【双模式】下强制指定使用哪种客户端,示例代码如下:

public string Test1()
{
    // 默认使用  MySqlConnector 客户端
    using( DbContext dbContext = DbConnManager.CreateAppDb("dbconn-name") ) {
        return dbContext.CPQuery.Create("select now()").ExecuteScalar<DateTime>().ToTimeString();
    }
}

public string Test2()
{
    // 强制使用 MySqlConnector 客户端
    using( DbContext dbContext = DbConnManager.CreateAppDb("dbconn-name", false, "MySqlConnector") ) {
        return dbContext.CPQuery.Create("select now()").ExecuteScalar<DateTime>().ToTimeString();
    }
}

public string Test3()
{
    // 强制使用 MySql.Data 客户端
    using( DbContext dbContext = DbConnManager.CreateAppDb("dbconn-name", false, "MySql.Data") ) {
        return dbContext.CPQuery.Create("select now()").ExecuteScalar<DateTime>().ToTimeString();
    }
}



查看异步效果

示例代码如下:

public async Task<string> TestAsync()
{
    StringBuilder sb = new StringBuilder();
    sb.AppendLineRN("before OpenConnection: " + Thread.CurrentThread.ManagedThreadId);

    using( DbContext dbContext = DbConnManager.CreateAppDb("mysqltest") ) {
        sb.AppendLineRN("after  OpenConnection: " + Thread.CurrentThread.ManagedThreadId);

        //--------------------------------------------------------------
        var data = await dbContext.GetByKeyAsync<Customer>(90);
        sb.AppendLineRN("after  GetById(90): " + Thread.CurrentThread.ManagedThreadId);

        //--------------------------------------------------------------
        data = await dbContext.GetByKeyAsync<Customer>(91);
        sb.AppendLineRN("after  GetById(91): " + Thread.CurrentThread.ManagedThreadId);

        //--------------------------------------------------------------
        Task<Customer> task = dbContext.GetByKeyAsync<Customer>(92);
        sb.AppendLineRN("before await GetById(92): " + Thread.CurrentThread.ManagedThreadId);
        data = await task;
        sb.AppendLineRN("after  await GetById(92): " + Thread.CurrentThread.ManagedThreadId);

        //--------------------------------------------------------------
        task = dbContext.GetByKeyAsync<Customer>(93);
        sb.AppendLineRN("before await GetById(93): " + Thread.CurrentThread.ManagedThreadId);
        data = await task;
        sb.AppendLineRN("after  await GetById(93): " + Thread.CurrentThread.ManagedThreadId);

    }

    return sb.ToString();
}

使用MySqlConnector的服务端响应如下:

HTTP/1.1 200 OK
Transfer-Encoding: chunked
x-RequestId: 19aaa3c3d3e940359fa11061eda2d84f
x-HostName: f2de9a0d19ad
x-AppName: XDemo.WebSiteApp
x-dotnet: .NET 5.0.5
x-Nebula: 1.21.708.100
x-PreRequestExecute-ThreadId: 110
x-PostRequestExecute-ThreadId: 77
Content-Type: text/plain; charset=utf-8
Date: Tue, 13 Jul 2021 08:26:44 GMT
Server: Kestrel

before OpenConnection: 110
after  OpenConnection: 110
after  GetById(90): 55
after  GetById(91): 64
before await GetById(92): 64
after  await GetById(92): 53
before await GetById(93): 53
after  await GetById(93): 77

使用MySql.Data的服务端响应如下:

HTTP/1.1 200 OK
Transfer-Encoding: chunked
x-RequestId: 7e43d40dcc644fe881e87b2966a1d50a
x-HostName: f2de9a0d19ad
x-AppName: XDemo.WebSiteApp
x-dotnet: .NET 5.0.5
x-Nebula: 1.21.708.100
x-PreRequestExecute-ThreadId: 105
x-PostRequestExecute-ThreadId: 105
Content-Type: text/plain; charset=utf-8
Date: Tue, 13 Jul 2021 08:27:07 GMT
Server: Kestrel

before OpenConnection: 105
after  OpenConnection: 105
after  GetById(90): 105
after  GetById(91): 105
before await GetById(92): 105
after  await GetById(92): 105
before await GetById(93): 105
after  await GetById(93): 105


使用阿里的 ADS

ADS 这东西虽然一直号称与MySQL兼容,但并非完全兼容!

如果使用 MySqlConnector,你会发现:

  • 第一次能连接 ADS
  • 第二次就不能连接(出现异常)
  • 第三次可以连接
  • 第四次又不能连接(出现异常)
  • .......周而反复

具体异常内容大致是:

MySqlConnector.MySqlException: HResult=0x80004005,
  Message=[30000, 2023121110252517201620317003151105564] syntax error. SQL => xxxxxxxx
  Source=MySqlConnector
  StackTrace:
   在 MySqlConnector.Core.ServerSession.<ReceiveReplyAsync>d__107.MoveNext()
   在 System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable`1.ConfiguredValueTaskAwaiter.GetResult()
   在 MySqlConnector.Core.ServerSession.<TryResetConnectionAsync>d__99.MoveNext()
   在 MySqlConnector.Core.ConnectionPool.<GetSessionAsync>d__13.MoveNext()
   在 MySqlConnector.Core.ConnectionPool.<GetSessionAsync>d__13.MoveNext()
   在 System.Threading.Tasks.ValueTask`1.get_Result()
   在 MySqlConnector.MySqlConnection.<CreateSessionAsync>d__133.MoveNext()
   在 System.Threading.Tasks.ValueTask`1.get_Result()
   在 MySqlConnector.MySqlConnection.<OpenAsync>d__29.MoveNext()
   在 MySqlConnector.MySqlConnection.Open()

所以,你只能切换到 MySql.Data

然而,当你使用 MySql.Data 的新版本(8.0.26之后的版本),你会看到以下异常:

System.FormatException: The input string 'ON' was not in a correct format.
在 System.Number.ThrowFormatException[TChar](ReadOnlySpan`1 value)
   在 System.Convert.ToInt32(String value)
   在 MySql.Data.MySqlClient.Driver.LoadCharacterSets(MySqlConnection connection)
   在 MySql.Data.MySqlClient.Driver.Configure(MySqlConnection connection)
   在 MySql.Data.MySqlClient.MySqlConnection.Open()

原因是,新版本的MySql.Data在打开连接时,在MySql.Data.MySqlClient.Driver.LoadCharacterSets() 方法中增加了一段代码:

serverProps.TryGetValue("autocommit", out var value);
//-----------省略一些代码
if (Convert.ToInt32(value) == 0 && Version.isAtLeast(8, 0, 0))  

这样就会抛出异常(System.FormatException:Input string was not in a correct format.),因为 ON, OFF 不能转成数字!!

阿里云居然不认为这个是BUG ~~~

所以,解决办法是:使用 MySql.Data 8.0.26 之前的版本!

<PackageReference Include="MySql.Data" Version="8.0.25" />

如果觉得这个解决方法比较恶心,那就放弃 ADS 吧!!