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