2016-05-22 8 views
0

私は、顧客から金額取引をアップロードしてSQL Server DBに保存するためのエンドポイントとして機能する非常に簡単なWebアプリケーションを作成しています。 2つのパラメータ(userid: 'xxx', balancechange: -19.99)でリクエストを受け入れます。ユーザーIDがappデータベースに存在する場合、残高は変更されます。そうでない場合は、このIDに対して新しい行が作成されます。ASP.NET WebAPI + SQLの同時実行制御を正しく実装する

難しい点はリクエスト数が膨大で、できるだけ速く動作するようにアプリケーションを実装しなければならないことです(同じ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(); 
} 

私はconcernredてる事は同時実行でありますlock (syncLock)を使用して制御します。これは、負荷が高い状態でアプリを遅くし、アプリの複数のインスタンスを異なるサーバーにデプロイすることを許可しません。ここでは、並行処理制御を適切に実装する方法は何ですか?

注:現在のストレージメカニズム(SQL Server)が将来変更される可能性があるため、高速でDBに依存しない並行性制御の実装方法を使用したいと思います。

+1

追加はアソシエイティブなので、バランスの変更が順番に行われるのはなぜですか?さらに高速化が必要な場合は、インライン(注入の傾向がある)文字列ではなく、ストアドプロシージャでカプセル化する必要があります。 – Crowcoder

+0

'userId'変数が文字列であるため、SQLコードはSQLインジェクションのためにオープンされています。パラメータ化されたクエリを使用します。 – jgauffin

+2

あなたのロックは行ごとに分散していなければならず、DBトランザクションが必要です。なぜ私はあなたがそのような脆弱なアプローチを使用して再実装しようとしているのか分かりません。 –

答えて

0

まず、DBに依存しないコード:このため

、あなたはDbProviderFactory見たいと思うでしょう。これによりプロバイダ名(MySql.Data.MySqlClientSystem.Data.SqlClient)を渡し、抽象クラス(DbConnection,DbCommand)を使用してDBと対話します。

第二に、使用してトランザクションとparamaterizedクエリ:データベースで作業しているとき

、あなたは常にあなたのクエリがparamaterizedていたいです。 String.Format()または他のタイプの文字列連結を使用する場合は、クエリをインジェクションまで開きます。

トランザクションでは、問合せのすべてまたはすべてが確実に行われ、トランザクション内の問合せのみがこれらの表にアクセスできるように表をロックダウンすることもできます。トランザクションには、DBに変更があればそれを保存するCommitという2つのコマンドと、DBへの変更を破棄するRollbackというコマンドがあります。

以下は、クラス変数_factoryにすでにDbProviderFactoryというインスタンスがあることを前提としています。

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. 
    } 
} 
関連する問題