性懲りもなく4回目。
@kazuk さんが「無駄な処理をしない事と、重い処理をバックグラウンドにする事」ということで、
削除処理を毎回するんじゃなくてある程度間隔あけて+別スレッドでやるべきだろJKとおっしゃられました。
というわけで @takepara さんのHighPerformanceSessionStateProviderに追加してみました。
↓結果
少しパフォーマンスよくなりました。ただMAXも2秒とかなので、平均して落ち着いた感。エラーもなくなってますね。
クライアントは↓
多少ばらつきありますがネットワーク的に遠いところ(日本)なのでまぁこんな感じですかね。ゴミセッションの削除がはしったと思われるタイミングでもそんなに重くなってないのが特徴です。
とまぁこんな感じでかなりまともに使えるセッションプロバイダになった気がします。
ロールやメンバシップの部分はASP.NET Univarsal Providersのが良しなに利用できると思うので組み合わせるといいんではないでしょうか。
最終的なソースはこちら↓
// create: 2011.08.23
// author: @takepara (http://tinyurl.com/3kebwlu) , @kazuk (http://tinyurl.com/3r5vjkq)
using System;
using System.Configuration;
using System.Data.Common;
using System.Reflection;
using System.Web;
using System.Web.Providers;
using System.Web.Providers.Entities;
using System.Threading;
namespace UniversalProviders
{
public class HighPerformanceSessionStateProvider : DefaultSessionStateProvider
{
private string _connectionStringName;
private bool _initialized = false;
private DateTime _lastSessionExpireInvoked = DateTime.MinValue;
private object _lockSessionExpire = new object();
private bool _working = false;
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
_connectionStringName = config["connectionStringName"];
base.Initialize(name, config);
}
private void ExecuteSql(ConnectionStringSettings connectionStringSettings, Action<DbProviderFactory, DbCommand> functor)
{
var providerName = connectionStringSettings.ProviderName;
var factory = DbProviderFactories.GetFactory(providerName);
using (var connection = factory.CreateConnection())
{
connection.ConnectionString = connectionStringSettings.ConnectionString;
connection.Open();
var command = connection.CreateCommand();
functor(factory, command);
command.ExecuteNonQuery();
connection.Close();
}
}
private void CreateSessionIndex(ConnectionStringSettings connectionStringSettings)
{
ExecuteSql(connectionStringSettings, (factory, command) =>
{
command.CommandText = @"IF not EXISTS (SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID(N'[dbo].[Sessions]') AND name = N'IX_Sessions')
begin
CREATE NONCLUSTERED INDEX [IX_Sessions] ON [dbo].[Sessions]
(
[Expires] ASC
)
end
";
});
}
private void RemoveExpiredSessions(
ConnectionStringSettings connectionStringSettings)
{
if( _working ) return; // やってる人が居るならやらんでいいでしょ
_working = true;
try {
ThreadPool.QueueUserWorkItem( (object state)=>
{
try
{
ExecuteSql(connectionStringSettings, (factory, command) =>
{
command.CommandText = "delete Sessions where Expires < @0";
var parameter = factory.CreateParameter();
parameter.ParameterName = "@0";
parameter.Value = DateTime.UtcNow;
command.Parameters.Add(parameter);
});
}
finally { _working = false; }
});
} catch { _working = false; throw; }
}
private MethodInfo GetCreateSessionEntities()
{
var modelHelper =
Assembly.GetAssembly(typeof(Session)).GetType("System.Web.Providers.Entities.ModelHelper");
return modelHelper.GetMethod("CreateSessionEntities",
BindingFlags.NonPublic | BindingFlags.Static);
}
public override void InitializeRequest(HttpContext context)
{
var connectionStringSettings = ConfigurationManager.ConnectionStrings[_connectionStringName];
if (!_initialized)
{
var initializer = GetCreateSessionEntities();
initializer.Invoke(null, new object[] { connectionStringSettings });
CreateSessionIndex(connectionStringSettings);
_initialized = true;
}
if( _lastSessionExpireInvoked < DateTime.UtcNow - TimeSpan.FromSeconds(30) )
{
if( Monitor.TryEnter( _lockSessionExpire ) ) {
// ロックに入れたスレッドでだけ処理する、入れなかったスレッドでは処理しない
DateTime old = _lastSessionExpireInvoked;
_lastSessionExpireInvoked = DateTime.UtcNow;
try
{
try
{
RemoveExpiredSessions(connectionStringSettings);
}
catch
{
// 失敗した時はタイマ値を戻すことで別のスレッドにやってもらう
_lastSessionExpireInvoked = old;
throw;
}
}
finally
{
// 絶対にMonitorを出る
Monitor.Exit( _lockSessionExpire );
}
}
}
}
}
}

