Laden...

[Artikel] Singleton (mit ConnectionPools)

Erstellt von Asmodiel vor 15 Jahren Letzter Beitrag vor 15 Jahren 28.699 Views
Asmodiel Themenstarter:in
19 Beiträge seit 2007
vor 15 Jahren
[Artikel] Singleton (mit ConnectionPools)

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:


// Dies ist die Instanz der Singleton-Klasse
private static SingletonBeispiel instance;

/// <summary>
/// Privater Konstruktor des SingletonBeispiels. Nur aus SingletonBeispiel selbst kann
/// dieser aufgerufen werden.
/// </summary>
private SingletonBeispiel()
{
	// Evtl. Code im Konstruktor
}

/// <summary>
/// Gibt die Instanz der Klasse zurück.
/// </summary>
/// <returns>Instanz von SingletonBeispiel</returns>
public static SingletonBeispiel GetInstance()
{
	// Falls noch keine Instanz erstellt wurde, tun wir das jetzt
	if(instance == null)
	{
		// Hier darf der Konstruktor aufgerufen werden. Ist schliesslich in dieser Klasse
		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.


private int maxConnections;
private List<MySqlConnection> freeConnections;
private List<MySqlConnection> busyConnections;

// Datenbank-Verbindungsdaten
private string user;
private string password;
private string database;

// Instanz von sich selbst für Singleton-Implementierung
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.


private ConnectionPool(string user, string password, string database, int maxConnections)
{
	// Verbindungsdaten setzen
	this.user = user;
	this.password = password;
	this.database = database;

	// Anzahl der Verbindungen setzen
	this.maxConnections = maxConnections;

	// Listen instanzieren
	this.freeConnections = new List<MySqlConnection>;();
	this.busyConnections = new List<MySqlConnection>;();

	// Verbindungen herstellen
	this.OpenConnections();
}

/// <summary>
/// Stellt die Verbindungen zur MySQL-Datenbank her.
/// </summary>
private void Init()
{
	// ConnectionString mit den Verbindungsdaten erstellen
	MySqlConnectionStringBuilder sb = new MySqlConnectionStringBuilder();
	sb.Password = this.password;
	sb.UserID = this.user;
	sb.Database = this.database;

	// Verbindungen herstellen...
	for(int i = 0; i < this.maxConnections; i++)
	{
		MySqlConnection conn = new MySqlConnection(sb.ConnectionString);
		conn.Open();

		// ... und zur Liste der verfügbaren Verbindungen hinzufügen
		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.


/// <summary>
/// Gibt die Instanz des ConnectionPools zurück.
/// </summary>
/// <returns>Instanz des ConnectionPools</returns>
public static ConnectionPool GetInstance()
{
	if(instance == null)
	{
		// Die Verbindungsdaten sind in einem Settings-File gespeichert.
		// Dieses laden wir neu:
		Properties.Settings.Default.Reload();

		// Die Instanz wird mit den Verbindungsdaten erzeugt.
		// Wir stellen 200 Verbindungen zur Verfügung.
		instance = new ConnectionPool(
		Properties.Settings.Default.DatabaseUser,
		Properties.Settings.Default.DatabasePassword,
		Properties.Settings.Default.DatabaseName,
		200);
	}

	// Instanz zurückgeben
	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().


/// <summary>
/// Holt eine Verbindung aus dem ConnectionPool.
/// </summary>
/// <returns>Verbindung zur Datenbank</returns>
public MySqlConnection GetConnection()
{
	// Falls keine Verbindungen mehr verfügbar sind, wird eine
	// Ausnahme ausgelöst
	if(freeConnections.Count == 0)
		throw new InvalidOperationException("Keine weiteren Verbindungen frei.");

	// Die zu vergebende Verbindung wird zwischengespeichert, ...
	MySqlConnection tmpConn = freeConnections[0];
	// ... der Liste der vergebenen Verbindungen hinzugefügt ...
	busyConnections.Add(tmpConn);
	// ... und aus der Liste mit den Verfügbaren entfernt.
	freeConnections.Remove(tmpConn);

	// Prüfen, ob die Verbindung überhaupt noch hergestellt ist
	if(tmpConn.State != ConnectionState.Open)
	{
		// Wenn nicht: Verbindung erneut öffnen
		tmpConn.Open();
	}

	// Verbindung zurückgeben
	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:

/// <summary>
/// Gibt eine Verbindung wieder frei.
/// </summary>
/// <param name="conn">Verbindung, die freigegeben werden soll.</param>
public void PutConnection(MySqlConnection conn)
{
	// Überprüfen, ob die Verbindung überhaupt zum Pool gehört
	if(!busyConnections.Contains(conn))
		throw new ArgumentException("Verbindung gehört nicht zum ConnectionPool.";);

	// Verbindung freigeben
	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.


/// <summary>
/// Schliesst alle Verbindungen.
/// </summary>
public void CloseAll()
{
	// Verfügbare Verbindungen schliessen
	foreach(MySqlConnection conn in freeConnections)
	{
		conn.Close();
	}

	// Vergebene Verbindungen schliessen
	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.

**:::

Gelöschter Account
vor 15 Jahren

interessanter artikel. gut erklärt und auch gut ausgeschmückt aber ein paar punkte möchte ich hier anmerken.

du bist dir aber schon bewusst das du in diesem fall (da der connectionstring immer gleich ist) 200 mal ein und die selbe connection hast? zudem hat ein connectionpool eigendlich die aufgabe bestehende oder irgendwann erzeugte connections wiederverwertbar zu machen und genau das macht auch der .net databaseconnectionpool intern, den du im übrigen auch hier verwendest.

mein wissen bezieht sich zwar auf sqlconnections aber ich denke das dürte keinen unterschied zu mysqlconnections machen.

und wenn du schon einen artikel zu singleton machst, wären auch die variationen brauchbar.

z.b.
threadsafe singleton
threadsafe singleton mit lazy instance

hier mal ein link:http://www.yoda.arachsys.com/csharp/singleton.html

X
2.051 Beiträge seit 2004
vor 15 Jahren

Hallo Asmodiel,

nett, dass du so ein artikel schreibst. allerdings muss du noch einige fehler beheben. allein das erste bsp. ist nicht lauffähig, denn die instance soll auch static sein.

ich glaube, dass Golo Roden in seinem blog so einige artikel zu Singleton verfasst hat, die sehr hilfreich sind. (http://www.des-eisbaeren-blog.de) würde dir empfehlen die auch mal zu lesen.

140 Beiträge seit 2007
vor 15 Jahren

Vorsicht -> Multithreading, gerade bei Web-Anwendungen.

Viel Erfolg (mit wenig Aufwand),
Sisyphus

G
68 Beiträge seit 2005
vor 15 Jahren

Also ich finde den Artikel gar nicht mal so schlecht. Es hat zwar einige Ecken und Kanten die man noch ausbessern könnte, aber so im Ganzen ist er Doch ganz gut. Habe bereits "Asmodiel" darauf hingewiesen.

Und als Vorschlag an die Leute die Sachen vermissen (im Besonderen @ JAck30lena): Eine einzelne Person kann nie alles wissen. Besonders wenn man die .NET Libs sowie die ganzen 3rd Party Libs ansieht die es da draussen so gibt... Also, falls Ihr euch mit weiteren Details auskennt, könnt ihr doch eine "Erweiterung" zum Artikel schreiben - bissl in der Art wie Wikipedia ja auch funktioniert.

So oder so - schönes Weekend Euch allen bereits,
Gregor

Gelöschter Account
vor 15 Jahren

Und als Vorschlag an die Leute die Sachen vermissen (im Besonderen @ JAck30lena): Eine einzelne Person kann nie alles wissen. Besonders wenn man die .NET Libs sowie die ganzen 3rd Party Libs ansieht die es da draussen so gibt...

singleton ist ein designpattern und entweder man kennt es oder man kennt es nciht. siehe hierzu auch GoF

artikel über singleton ohne dessen wichitgsten varianten ist ein punkt den ich mit den zuvo geposteten link bereits erschlagen habe. in dem link wird genau erklärt wie man es macht und wie man es nciht macht. mit libs und frameworks hat das ncihts zu tun, da stimme ich dir zu, das man da nciht alles kennen kann aber designpatterns sind ein muss. siehe auch :Braucht man das GOF-Buch? (Design Patterns)