Guten Morgen,
Da ich nun endlich auch mal einen Artikel verfassen wollte,
dachte ich lange über ein geeignetes Thema nach.
Aber achtung... Ich wollte nicht einfach ein trockenes Tutorial schreiben.
Also entschuldigt, wenn da zu viel nebensächliches steht.
Ich kam auf das Thema Entwurfsmuster und so wollte ich nun über Singleton
berichten. Zunächst folgt die Theorie, dann geht es zur Implementierung.
Ausgangssituation
Wir haben eine Web-Applikation, welche auf eine MySQL-Datenbank zugreift.
Dabei wird
MySQL Connector/Net 5.2 verwendet.
Bei jedem Zugriff auf die Datenbank wird eine neue Verbindung zu MySQL hergestellt. Wir haben das so implementiert und veröffentlichen unsere Seite.
Es klappt alles wunderbar. Doch schon am zweiten Tag finden immer
mehr Benutzer unsere Seite und speichern, aktualisieren und laden ihre Daten.
Nun, damit werden wir wohl noch fertig.
Aber nun sind es mittlerweile 500 Benutzer, die ihre Daten dort (mehr oder minder) gleichzeitig speichern möchten.
Daran hätten wir nicht gedacht, und schon haben wir ein kleines Problem mit der Geschwindigkeit. So eine Verbindung herzustellen kann eine performancelastige Angelegenheit sein. Das möchten wir den Benutzern (und auch unserem lieben Server) natürlich nicht zumuten. Aber auch nicht uns selbst, denn dies wirkt sich natürlich auch negativ auf die Wartbarkeit des Codes aus.
Eine mögliche Lösung
Wie wäre es also, wenn wir eine gewisse Anzahl von Verbindungen gleich beim Starten der Web-Applikation zur Verfügung stellen? Ich spreche von einem ConnectionPool. Dies tun wir mit 200 Verbindungen. Somit sind diese bereits hergestellt, wenn der Benutzer auf seine Daten zugreifen möchte. Möchte er Daten speichern, holt er sich eine Verbindung vom ConnectionPool. Nach dem Speichern wird die Verbindung wieder zurückgegeben und sie steht für andere Benutzer zur Verfügung.
Ein einsames Objekt: Singleton
Wir werden diesen ConnectionPool als Singleton implementieren, da wohl mehrere Web-Applikationen darauf zugreifen werden.
Doch was ist eigentlich ein Singleton? Singleton ist ein Entwurfsmuster. Dabei wird eine Klasse so erstellt, dass es nur eine Instanz von ihr gibt.
Das Prinzip ist recht einfach: Wir sorgen dafür, dass niemand eine Instanz der Klasse erstellen darf. Mit Ausnahme der Klasse selbst. Das erreichen wir mit einem privaten Konstruktor. Auf diesen hat eben nur die Klasse selbst Zugriff. Zudem besitzt sie eine private Referenz-Variable auf eine Instanz von sich selbst. Das ist eben diese eine Instanz, welche zur Verfügung steht.
Damit sich auch jemand diese Instanz von der Klasse holen kann, stellen wir eine statische Methode GetInstance() bereit. Beispiel:
C#-Code: |
private static SingletonBeispiel instance;
private SingletonBeispiel()
{
}
public static SingletonBeispiel GetInstance()
{
if(instance == null)
{
instance = new SingletonBeispiel();
}
return instance;
}
|
Und schon haben wir unser erstes Singleton erstellt. Das kann zwar jetzt noch nicht wirklich etwas, aber das werden wir ja nun anhand des ConnectionPools ändern.
Der ConnectionPool
Wir erstellen also eine Klasse mit dem Namen ConnectionPool.
Die benötigt zunächst einmal die maximale Anzahl an Verbindungen, eine Liste für die verfügbaren Verbindungen, sowie eine Liste für die vergebenen Verbindungen. Hinzu kommen die Daten für die Verbindungen.
Da wir den ConnectionPool als Singleton implementieren, brauchen
wir natürlich noch eine Referenz für die Instanz.
C#-Code: |
private int maxConnections;
private List<MySqlConnection> freeConnections;
private List<MySqlConnection> busyConnections;
private string user;
private string password;
private string database;
private static ConnectionPool instance;
|
(Seitengedanken)
Moment... Wozu brauchen wir eine Liste für die vergebenen Verbindungen?
Bei der Vergabe einer Verbindung können wir doch einfach diese aus der Liste mit den verfügbaren Verbindungen entfernen und dann wieder hinzufügen?
Ja, das können wir. Allerdings werden wir sichergehen. Denn was ist, wenn wir die Web-Applikation beenden müssen? Dann möchten wir sämtliche Verbindungen schliessen.
Nun holt sich der Programmierer in seinem Code eine Verbindung und führt alle benötigten Operationen damit aus. Gut, nur noch die Verbindung zurückgeben.
Aber moment, da läuft eine hübsche Frau vorne durch und der Programmierer wird abgelenkt! Er vergisst doch tatsächlich, seine Verbindung auch wieder schön brav an den Pool zurückzugeben. Er startet und beendet die Web-Applikation wieder. Moment mal... Die Verbindung, die der Programmierer nicht wieder zurückgegeben hat, ist noch offen! Und zwar, weil der ConnectionPool keine Referenz mehr auf die Verbindung hatte. Deshalb haben wir eine zusätzliche Liste mit den vergebenen Verbindungen. Und weil so nicht irgendwelche Verbindungen in den ConnectionPool geschleust werden können. Wir wollen da ja nicht irgendwelchen komischen Mist reinwerfen lassen!
Auch unser Programmierer hat nun daran gedacht. Seine Verbindungen werden nun sicher geschlossen und es wird kein fremder Mist eingeschleust. Jetzt darf er endlich ohne schlechtes Gewissen der hübschen Frau von vorhin nachlaufen.
(/Seitengedanken)
Wir können nun unsere Verbindungen in den Listen lagern und auch angeben, wieviele Verbindungen denn zu Beginn hergestellt werden sollen.
Also schreiben wir doch erst einmal den Konstruktor. Darin werden
die Daten gesetzt und die Verbindungen hergestellt.
C#-Code: |
private ConnectionPool(string user, string password, string database, int maxConnections)
{
this.user = user;
this.password = password;
this.database = database;
this.maxConnections = maxConnections;
this.freeConnections = new List<MySqlConnection>;();
this.busyConnections = new List<MySqlConnection>;();
this.OpenConnections();
}
private void Init()
{
MySqlConnectionStringBuilder sb = new MySqlConnectionStringBuilder();
sb.Password = this.password;
sb.UserID = this.user;
sb.Database = this.database;
for(int i = 0; i < this.maxConnections; i++)
{
MySqlConnection conn = new MySqlConnection(sb.ConnectionString);
conn.Open();
this.freeConnections.Add(conn);
}
}
|
So, beim Instanzieren unseres ConnectionPools werden also nun die Verbindungen hergestellt. Doch dazu muss man den Pool überhaupt noch instanzieren können! Wir schreiben nun die Methode GetInstance(), über die die Instanz des ConnectionPools verfügbar wird.
C#-Code: |
public static ConnectionPool GetInstance()
{
if(instance == null)
{
Properties.Settings.Default.Reload();
instance = new ConnectionPool(
Properties.Settings.Default.DatabaseUser,
Properties.Settings.Default.DatabasePassword,
Properties.Settings.Default.DatabaseName,
200);
}
return instance;
}
|
Wunderbar. Wir können nun die Instanz des ConnectionPools holen.
Beim ersten Zugriff wird er automatisch initialisiert. Damit wir die Verbindungen nun holen und zurückgeben können, benötigen wir zwei weitere Methoden: GetConnection() und PutConnection().
C#-Code: |
public MySqlConnection GetConnection()
{
if(freeConnections.Count == 0)
throw new InvalidOperationException("Keine weiteren Verbindungen frei.");
MySqlConnection tmpConn = freeConnections[0];
busyConnections.Add(tmpConn);
freeConnections.Remove(tmpConn);
if(tmpConn.State != ConnectionState.Open)
{
tmpConn.Open();
}
return tmpConn;
}
|
Mit dieser Methode wird eine Verbindung geholt. Zuerst wird überprüft, ob überhaupt noch freie Verbindungen zur Verfügung stehen. Dann wird die Verbindung zur Liste der vergebenen Listen hinzugefügt und zurückgegeben. Vorher wird geprüft, ob die Verbindung noch steht. Wenn nicht, wird sie wieder hergestellt.
Nun zur PutConnection()-Methode:
C#-Code: |
public void PutConnection(MySqlConnection conn)
{
if(!busyConnections.Contains(conn))
throw new ArgumentException("Verbindung gehört nicht zum ConnectionPool.";);
freeConnections.Add(conn);
busyConnections.Remove(conn);
}
|
Es wird überprüft, ob sich die Verbindung in der Liste der vergebenen Verbindungen befindet. Es könnte ja sein, dass jemand eine fremde Verbindung einschleusen möchte. Ansonsten wird die Verbindung wieder der Liste der verfügbaren Verbindungen hinzugefügt.
Nun wären wir auch schon fast fertig. Fehlt nur noch eine Methode zum Schliessen aller Verbindungen.
C#-Code: |
public void CloseAll()
{
foreach(MySqlConnection conn in freeConnections)
{
conn.Close();
}
foreach(MySqlConnection conn in busyConnections)
{
conn.Close();
}
}
|
So werden sämtliche Verbindungen geschlossen. Auch diejenigen, die noch
vergeben sind oder beim Zurückgeben vergessen wurden.
Das wäre es dann auch schon.