我正在编写一个非常简单的Web应用程序,用作从客户上传货币交易并将其保存在SQL Server数据库中的端点。它接受只有2个参数的请求:userid: 'xxx', balancechange: -19.99
。如果应用程序数据库中存在用户标识,则更改余额;如果不是 - 为此ID创建一个新行。
所有这一切中的困难部分是请求的数量巨大,我必须以尽可能快的方式实现应用程序并解决并发问题(如果同一ID的2个请求同时到达)
该应用程序是ASP.NET MVC WebAPI。我选择使用普通的旧ADO.NET来提高速度,这就是我目前所拥有的:
private static readonly object syncLock = new object();
public void UpdateBalance(string userId, decimal balance)
{
lock (syncLock)
{
using (var sqlConnection = new SqlConnection(this.connectionString))
{
var command = new SqlCommand($"SELECT COUNT(*) FROM Users WHERE Id = '{userId}'", sqlConnection);
if ((int)command.ExecuteScalar() == 0)
{
command = new SqlCommand($"INSERT INTO Users (Id, Balance) VALUES ('{userId}', 0)", sqlConnection);
command.ExecuteNonQuery();
}
command = new SqlCommand($"UPDATE Users SET Balance = Balance + {balance} WHERE Id = {userId}", sqlConnection);
command.ExecuteNonQuery();
}
}
}
从这样的控制器调用:
[HttpPost]
public IHttpActionResult UpdateBalance(string id, decimal balanceChange)
{
UpdateBalance(id, balanceChange);
return Ok();
}
我关心的是使用lock (syncLock)
的并发控制。这会在高负载下降低应用程序速度,并且不允许将应用程序的多个实例部署在不同的服务器上。有什么方法可以在这里正确实现并发控制?
注意:我想使用一种快速且独立于数据库的方式来实现并发控制,因为当前的存储机制(SQL Server)将来可能会发生变化。
答案 0 :(得分:0)
首先,与数据库无关的代码:
为此,您需要查看DbProviderFactory。这允许传递提供者名称(MySql.Data.MySqlClient
,System.Data.SqlClient
),然后使用与数据库交互的抽象类(DbConnection
,DbCommand
)。
其次,使用交易和参数化查询:
当您使用数据库时,您总是希望对您的查询进行参数化。如果您使用String.Format()
或任何其他类型的字符串连接,则打开查询直到注入。
事务确保您的查询全部或全部,并且它们还可以锁定表,以便只有事务中的查询才能访问这些表。事务有两个命令,Commit
将更改(如果有的话)保存到数据库,Rollback
将丢弃对数据库的任何更改。
以下假设您已在类变量DbProviderFactory
中拥有_factory
的实例。
public void UpdateBalance(string userId, decimal balanceChange)
{
//since we might need to execute two queries, we will create the paramaters once
List<DbParamater> paramaters = new List<DbParamater>();
DbParamater userParam = _factory.CreateParamater();
userParam.ParamaterName = "@userId";
userParam.DbType = System.Data.DbType.Int32;
userParam.Value = userId;
paramaters.Add(userParam);
DbParamater balanceChangeParam = _factory.CreateParamater();
balanceChangeParam.ParamaterName = "@balanceChange";
balanceChangeParam.DbType = System.Data.DbType.Decimal;
balanceChangeParam.Value = balanceChange;
paramaters.Add(balanceChangeParam);
//Improvement: if you implement a method to clone a DbParamater, you can
//create the above list in class construction instead of function invocation
//then clone the objects for the function.
using (DbConnection conn = _factory.CreateConnection()){
conn.Open(); //Need to open the connection before you start the transaction
DbTransaction trans = conn.BeginTransaction(System.Data.IsolationLevel.Serializable);
//IsolationLevel.Serializable locks the entire table down until the
//transaction is commited or rolled back.
try {
int changedRowCount = 0;
//We can use the fact that ExecuteNonQuery will return the number
//of affected rows, and if there are no affected rows, a
//record does not exist for the userId.
using (DbCommand cmd = conn.CreateCommand()){
cmd.Transaction = trans; //Need to set the transaction on the command
cmd.CommandText = "UPDATE Users SET Balance = Balance + @balanceChange WHERE Id = @userId";
cmd.Paramaters.AddRange(paramaters.ToArray());
changedRowCount = cmd.ExecuteNonQuery();
}
if(changedRowCount == 0){
//If no record was affected in the previous query, insert a record
using (DbCommand cmd = conn.CreateCommand()){
cmd.Transaction = trans; //Need to set the transaction on the command
cmd.CommandText = "INSERT INTO Users (Id, Balance) VALUES (@userId, @balanceChange)";
cmd.Paramaters.AddRange(paramaters.ToArray());
cmd.ExecuteNonQuery();
}
}
trans.Commit(); //This will persist the data to the DB.
}
catch (Exception e){
trans.Rollback(); //This will cause the data NOT to be saved to the DB.
//This is the default action if Commit is not called.
throw e;
}
finally {
trans.Dispose(); //Need to call dispose
}
//Improvement: you can remove the try-catch-finally block by wrapping
//the conn.BeginTransaction() line in a using block. I used a try-catch here
//so that you can more easily see what is happening with the transaction.
}
}