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 吧!!