ノンブロッキング Db 処理が必要な一般的な理由は次のとおりです。
- 各データベース操作に時間がかかるため
- 処理するデータが多く、並列処理によってスループットが向上する可能性があるためです。(つまり、ボトルネックがデータベースに移動するだけではないと想定しています)
あなたの投稿が示唆するようSqlConnections
に、共有SqlConnection
やSqlCommand
スレッド間でのリソースの管理は良い考えではないため、考慮が必要です。へのアクセスを同期することSqlConnection
は、並列化の利点を台無しにするため、望ましくありません。
問題 1. に対する簡単な解決策は、各スレッドに独自の を強制的に確立させることですがSqlConnection
、これは高いデータベース スループットにはつながりません。
Task.Factory.StartNew(() =>
{
using (var conn = new SqlConnection(connectionString))
using (var cmd = conn.CreateCommand())
{
conn.Open();
SetupCmd(cmd);
SaveStat(cmd, statToSave);
}
});
バックグラウンド書き込みの代替手段が存在します (ケース 1.)。たとえば、1 つ以上の寿命の長いライター スレッドを持ち、キューをリッスンし ConcurrentQueue
ます。ライタースレッドは、全体を通して単一のオープンを維持します。BlockingCollection
GetConsumingEnumerable
SqlConnection
2. のような大量の状況では、SqlConnection
との再利用はSqlCommands
パフォーマンスに不可欠です。次に、データを複数のスレッド (TPL を使用している場合はタスク) に分割する必要があります。Parallel.ForEachがほとんどの面倒な作業を行ってくれます。以下では、オーバーロード withを使用してandlocalInit
を確立し、それを各本体に渡し、タスクの最後に を呼び出します (の 0..N 回の反復の後)。 Body - デフォルトのパーティショナーが使用されるため、TPL は、必要なタスクの数と、各タスクの本体に渡される項目の数を決定します)。これにより、スレッド ローカル ストレージの使用と同様のパラダイムが可能になります。SqlConnection
SqlCommand
localFinally
localInit
1 つの注意点 - 処理が大量の挿入操作のみの場合、SqlBulkCopyの方が全体的に優れたアプローチになる可能性があります。
TPL を使用したいくつかのオプションを次に示します。
create table StatsData
(
x int ,
y decimal(20,5),
name nvarchar(50)
)
そしてモデル:
public class StatsData
{
public int X { get; private set; }
public double Y { get; private set; }
public string Name { get; private set; }
public StatsData(int x, double y, string name)
{
X = x;
Y = y;
Name = name;
}
}
次のクラスは、2 つの非同期オプションを提供します (ポイント 1 と 2 用)。
public class Dispatcher
{
// Helpers - refactoring
private static void SetupCmd(SqlCommand cmd)
{
cmd.CommandText = "insert into dbo.statsdata(x, y, Name) values (@x, @y, @Name);";
cmd.CommandType = CommandType.Text;
cmd.Parameters.Add("@x", SqlDbType.Int);
cmd.Parameters.Add("@y", SqlDbType.Decimal);
cmd.Parameters.Add("@Name", SqlDbType.NVarChar, 30);
}
private static void SaveStat(SqlCommand cmd, StatsData statToSave)
{
cmd.Parameters["@x"].Value = statToSave.X;
cmd.Parameters["@y"].Value = statToSave.Y;
cmd.Parameters["@Name"].Value = statToSave.Name;
cmd.ExecuteNonQuery();
}
// 1. Save 1 stat at a time on a background task. Use for low / intermittent volumes
public void SaveStatAsynch(string connectionString, StatsData statToSave)
{
Task.Factory.StartNew(() =>
{
using (var conn = new SqlConnection(connectionString))
using (var cmd = conn.CreateCommand())
{
conn.Open();
SetupCmd(cmd);
SaveStat(cmd, statToSave);
}
});
}
// 2. For background writing of large volumes of stats. Uses the default partitioner in parallel foreach
public void SaveStatsParallel(string connectionString, IEnumerable<StatsData> statsToSave)
{
Parallel.ForEach(
statsToSave,
// localInit. Return value is passed to each body invocation
() =>
{
var conn = new SqlConnection(connectionString);
var cmd = conn.CreateCommand();
SetupCmd(cmd);
conn.Open();
return new
{
Conn = conn,
Cmd = cmd
};
},
// Body, 0..N per Task decided by TPL
(stat, loopState, initData) =>
{
SaveStat(initData.Cmd, stat);
return initData;
},
// Disposables
(initData) =>
{
initData.Cmd.Dispose();
initData.Conn.Dispose();
}
);
}
使用例:
const string connString = @"Server=.\SqlExpress;DataBase=StatsDb;Integrated Security=true";
// Create some dummy data
var statsToSave =
Enumerable
.Range(0, 10000)
.Select(i => new StatsData(i, i*Math.PI, string.Format("Stat #{0}", i)));
// Insert this in parallel on background tasks / threads as determined by the TPL
new Dispatcher().SaveStatsParallel(connString, statsToSave);