2016-05-22 15 views
0

Ich schreibe eine sehr einfache Webanwendung, die als Endpunkt für das Hochladen von Geldtransaktionen von Kunden dient und speichert sie in SQL Server DB. Es akzeptiert Anfragen mit nur 2 Parametern: userid: 'xxx', balancechange: -19.99. Wenn die Benutzer-ID in der App-Datenbank vorhanden ist, wird der Kontostand geändert. Wenn nicht, wird eine neue Zeile für diese ID erstellt.Korrigieren Sie Gleichzeitigkeitskontrolle in ASP.NET WebAPI + SQL

Der schwierige Teil in all dem ist, dass die Anzahl der Anfragen enorm ist und ich die App so implementieren muss, dass sie so schnell wie möglich funktioniert und Nebenläufigkeitsprobleme behebt (wenn zwei Anfragen für dieselbe ID gleichzeitig ankommen)).

Die App ist eine ASP.NET MVC WebAPI. Ich entschied mich für das gute alte ADO.NET für Geschwindigkeit zu verwenden, und das ist, was ich derzeit haben:

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(); 
     } 
    } 
} 

von einem Controller wie folgt aufgerufen:

[HttpPost] 
public IHttpActionResult UpdateBalance(string id, decimal balanceChange) 
{ 
    UpdateBalance(id, balanceChange); 
    return Ok(); 
} 

Das, was ich mit Concurrency concernred bin ist Steuerung mit lock (syncLock). Dies würde die App bei hoher Auslastung verlangsamen und es nicht zulassen, dass mehrere Instanzen der App auf verschiedenen Servern bereitgestellt werden. Wie kann man die Steuerung des Nebenläufers hier richtig implementieren?

Hinweis: Ich möchte eine schnelle und DB-unabhängige Art der Implementierung der Steuerung des gemeinsamen Zugriffs verwenden, da sich der aktuelle Speichermechanismus (SQL Server) in der Zukunft ändern kann.

+1

Addition ist assoziativ, also warum sind Sie besorgt über eine Änderung des Gleichgewichts in Reihenfolge? Und wenn Sie mehr Geschwindigkeit wünschen, sollten Sie dies in einer gespeicherten Prozedur anstelle von Inline-Strings (injection-anfällig) einkapseln. – Crowcoder

+0

Ihr SQL-Code ist für SQL-Injektionen geöffnet, da die Variable 'userId' eine Zeichenfolge ist. Verwenden Sie parametrisierte Abfragen. – jgauffin

+2

Ihre Sperren müssen pro Zeile und verteilt sein: Sie benötigen eine DB-Transaktion. Ich verstehe nicht, warum Sie versuchen, es mit einem so brüchigen Ansatz wie diesem umzusetzen. –

Antwort

0

Zuerst DB-unabhängiger Code:

Dazu werden Sie bei DbProviderFactory suchen. Was dies erlaubt, ist die Übergabe des Providernamens (MySql.Data.MySqlClient, System.Data.SqlClient), dann die Verwendung der abstrakten Klassen (DbConnection, DbCommand), die Sie mit Ihrer DB interagieren.

Zweitens über Transaktionen und paramaterized Anfragen:

Wenn Sie mit einer Datenbank arbeiten, können Sie immer Ihre Fragen paramaterized haben möchten. Wenn Sie String.Format() oder eine andere Art von String-Verkettung verwenden, öffnen Sie Ihre Abfrage bis zur Injektion.

Transaktionen gewährleisten alles oder nichts mit Ihren Abfragen und sie können auch die Tabelle sperren, sodass nur Abfragen innerhalb der Transaktion auf diese Tabellen zugreifen können. Transaktionen haben zwei Befehle: Commit, die die Änderungen (falls vorhanden) in der DB speichert, und Rollback, die alle Änderungen an der DB verwirft.

Im Folgenden wird davon ausgegangen, dass Sie bereits eine Instanz von DbProviderFactory in einer Klassenvariablen _factory haben.

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. 
    } 
} 
Verwandte Themen