392 lines
15 KiB
Plaintext
392 lines
15 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Seminarplatz-Vergabe\n",
|
|
"\n",
|
|
"In folgendem Problem geht es darum, dass eine Gruppe von Studierenden einen oder mehrere Seminarplätze in einem Kurs bekommen sollen. Es 8 Seminargruppen mit insgesamt 40 Plätzen, diese sind in der Tabelle `seminar_sessions` abgelegt.\n",
|
|
"\n",
|
|
"Bei einer Anmeldung einer einzelnen Person wird die Anzahl der Plätze in der Tabelle `seminar_sessions` um 1 reduziert und ein Eintrag in der Tabelle `seminar_registrations` erstellt. Dies darf natürlich nur passieren, wenn noch Plätze verfügbar sind.\n",
|
|
"\n",
|
|
"Die Anzahl der vergebenen Plätze in der Tabelle `seminar_registrations` und die Anzahl der verfügbaren Plätze in der Tabelle `seminar_sessions` müssen konsistent sein, also in der Summe 40 ergeben.\n",
|
|
"\n",
|
|
"In einem Lasttest mit mehreren Threads zeigt sich: Die Anzahl der vergebenen Plätze in der Tabelle `seminar_registrations` und die Anzahl der verfügbaren Plätze in der Tabelle `seminar_sessions` sind nicht konsistent! \n",
|
|
"\n",
|
|
"### Ihre Aufgabe:\n",
|
|
"Führen Sie geeignete Transaktionen ein, um die Konsistenz zu gewährleisten!\n",
|
|
"\n",
|
|
"Implementierungen Sie dazu zunächst die Funktionen, die mit TODO gekennzeichnet sind, und passen Sie die Funktion `reserve_seat` an, um die Konsistenz zu gewährleisten.\n",
|
|
"\n",
|
|
"Am Code des Lasttests müssen Sie nichts ändern, er dient nur dazu, das Problem zu demonstrieren.\n",
|
|
"\n",
|
|
"### Bemerkungen:\n",
|
|
"\n",
|
|
"- nicht jeder Durchlauf verursacht Inkonsistenzen, manchmal müssen Sie den Code mehrmals ausführen, um Inkonsistenzen zu finden\n",
|
|
"- auch Deadlocks können auftreten, diese müssen auch vermieden werden\n",
|
|
"- die User-Tabelle wurde weggelassen, da sie für das Problem nicht relevant ist\n",
|
|
"- an manchen Stellen ist der Code absichtlich etwas ineffizient gehalten, um die Inkonsistenzen zu begünstigen (z.B. würden Probleme mit Inkonsistenzen seltener auftreten, wenn die Query `UPDATE seminar_sessions SET seats = seats-1` lauten würde).\n",
|
|
"\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 16,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import psycopg2\n",
|
|
"import psycopg2.extras"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 17,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Datenbankverbindung herstellen\n",
|
|
"def get_connection():\n",
|
|
" conn = psycopg2.connect(\n",
|
|
" host=\"localhost\",\n",
|
|
" port=5432,\n",
|
|
" dbname=\"postgres\",\n",
|
|
" user=\"postgres\",\n",
|
|
" password=\"postgres\",\n",
|
|
" )\n",
|
|
"\n",
|
|
" conn.autocommit = False\n",
|
|
" return conn"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 18,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Tabelle erzeugen - immer wenn diese Zelle ausgeführt wird, wird die Tabelle neu erzeugt\n",
|
|
"conn = get_connection()\n",
|
|
"cursor = conn.cursor()\n",
|
|
"cursor.execute(\"\"\"\n",
|
|
" DROP TABLE IF EXISTS seminar_registrations CASCADE;\n",
|
|
" DROP TABLE IF EXISTS seminar_sessions;\n",
|
|
"\n",
|
|
" CREATE TABLE IF NOT EXISTS seminar_sessions (\n",
|
|
" id SERIAL PRIMARY KEY,\n",
|
|
" seats INTEGER NOT NULL\n",
|
|
" );\n",
|
|
"\n",
|
|
" INSERT INTO seminar_sessions (seats) VALUES\n",
|
|
" (4), (4), (5), (5), (4), (10), (5), (3);\n",
|
|
"\n",
|
|
"\n",
|
|
" CREATE TABLE IF NOT EXISTS seminar_registrations (\n",
|
|
" user_id INTEGER NOT NULL,\n",
|
|
" session_id INTEGER NOT NULL,\n",
|
|
" FOREIGN KEY (session_id) REFERENCES seminar_sessions(id)\n",
|
|
" );\n",
|
|
"\"\"\")\n",
|
|
"conn.commit()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 19,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Session 1: 4 free seats\n",
|
|
"Session 2: 4 free seats\n",
|
|
"Session 3: 5 free seats\n",
|
|
"Session 4: 5 free seats\n",
|
|
"Session 5: 4 free seats\n",
|
|
"Session 6: 10 free seats\n",
|
|
"Session 7: 5 free seats\n",
|
|
"Session 8: 3 free seats\n",
|
|
"freie Plätze 40\n",
|
|
"belegte Plätze 0\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"# Hilfsfunktion: Alle Seminar-Sessions ausgeben\n",
|
|
"\n",
|
|
"# Task: Alle Seminar-Sessions auf der Console ausgeben\n",
|
|
"def show_sessions():\n",
|
|
" with (\n",
|
|
" get_connection() as conn,\n",
|
|
" conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur,\n",
|
|
" ):\n",
|
|
" cur.execute(\"SELECT id, seats FROM seminar_sessions ORDER BY id\")\n",
|
|
" for row in cur.fetchall():\n",
|
|
" print(f\"Session {row['id']}: {row['seats']} free seats\")\n",
|
|
"\n",
|
|
"# Anzahl aller freien Plätze bestimmen\n",
|
|
"# Task: Anzahl freier Plätze bestimmen (Summe über die Spalte \"seats\")\n",
|
|
"def count_free_seats():\n",
|
|
" with get_connection() as conn, conn.cursor() as cur:\n",
|
|
" cur.execute(\"SELECT COALESCE(SUM(seats), 0) FROM seminar_sessions\")\n",
|
|
" return cur.fetchone()[0]\n",
|
|
"\n",
|
|
"# Anzahl der belegten Plätze bestimmen\n",
|
|
"# Task: Anzahl belegter Plätze bestimmen (Anzahl der Einträge in der Tabelle seminar_registrations)\n",
|
|
"def count_occupied_seats():\n",
|
|
" with get_connection() as conn, conn.cursor() as cur:\n",
|
|
" cur.execute(\"SELECT COUNT(*) FROM seminar_registrations\")\n",
|
|
" return cur.fetchone()[0]\n",
|
|
"\n",
|
|
"# Testaufrufe\n",
|
|
"\n",
|
|
"# show_sessions()\n",
|
|
"show_sessions()\n",
|
|
"print(\"freie Plätze\", count_free_seats())\n",
|
|
"print(\"belegte Plätze\", count_occupied_seats())"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 20,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Diese Funktion reserviert einen Platz in einer Transaktion.\n",
|
|
"def reserve_seat(conn, user_id, session_id):\n",
|
|
" \"\"\"Versucht, einen Seminarplatz für den gegebenen Benutzer in der angegebenen Session zu reservieren.\n",
|
|
" Gibt True zurück, wenn die Reservierung erfolgreich war, sonst False. Die Operation wird in einer\n",
|
|
" transaktionalen Einheit ausgeführt, um Inkonsistenzen zu vermeiden.\"\"\"\n",
|
|
" with conn:\n",
|
|
" with conn.cursor() as cur:\n",
|
|
" cur.execute(\"SELECT seats FROM seminar_sessions WHERE id = %s FOR UPDATE\", (session_id,))\n",
|
|
" seats = cur.fetchone()[0]\n",
|
|
" if seats <= 0:\n",
|
|
" return False\n",
|
|
" cur.execute(\"UPDATE seminar_sessions SET seats = seats - 1 WHERE id = %s\", (session_id,))\n",
|
|
" cur.execute(\n",
|
|
" \"INSERT INTO seminar_registrations (user_id, session_id) VALUES (%s, %s)\",\n",
|
|
" (user_id, session_id)\n",
|
|
" )\n",
|
|
" return True\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 21,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import random\n",
|
|
"\n",
|
|
"# 20 zufällige Anmeldungen durchführen - diese Funktion wird später als Thread gestartet\n",
|
|
"def random_seat_reservation(n=20):\n",
|
|
" # jeder Thread hat seine eigene Verbindung\n",
|
|
" conn = get_connection()\n",
|
|
"\n",
|
|
" # n mal einen zufälligen Platz für eine zufällige User-ID reservieren\n",
|
|
" for _ in range(n):\n",
|
|
" random_user_id = random.randint(1, 100)\n",
|
|
" random_session_id = random.randint(1, 8)\n",
|
|
"\n",
|
|
" if reserve_seat(conn, random_user_id, random_session_id):\n",
|
|
" print(\"User\", random_user_id, \"hat sich für Session\", random_session_id, \"angemeldet\")\n",
|
|
" else:\n",
|
|
" print(\"Session\", random_session_id, \"ist ausgebucht\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 22,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"Vor dem Lasttest:\n",
|
|
"--------------------\n",
|
|
"Freie Plätze: 40\n",
|
|
"Belegte Plätze: 0\n",
|
|
"Gesamt: 40\n",
|
|
"Platzanzahl ist konsistent!\n",
|
|
"--------------------\n",
|
|
"User 25 hat sich für Session 3 angemeldet\n",
|
|
"User 25 hat sich für Session 4 angemeldet\n",
|
|
"User 31 hat sich für Session 8 angemeldet\n",
|
|
"User 91 hat sich für Session 2 angemeldet\n",
|
|
"User 66 hat sich für Session 1 angemeldet\n",
|
|
"User 61 hat sich für Session 3 angemeldet\n",
|
|
"User 53 hat sich für Session 7 angemeldet\n",
|
|
"User 70 hat sich für Session 4 angemeldet\n",
|
|
"User 98 hat sich für Session 3 angemeldet\n",
|
|
"User 42 hat sich für Session 5 angemeldet\n",
|
|
"User 22 hat sich für Session 7 angemeldet\n",
|
|
"User 71 hat sich für Session 3 angemeldet\n",
|
|
"User 50 hat sich für Session 2 angemeldet\n",
|
|
"User 26 hat sich für Session 1 angemeldet\n",
|
|
"User 53 hat sich für Session 5 angemeldet\n",
|
|
"User 54 hat sich für Session 4 angemeldet\n",
|
|
"User 72 hat sich für Session 1 angemeldet\n",
|
|
"User 3 hat sich für Session 3 angemeldet\n",
|
|
"User 95 hat sich für Session 7 angemeldet\n",
|
|
"User 83 hat sich für Session 4 angemeldet\n",
|
|
"Session 3 ist ausgebucht\n",
|
|
"User 90 hat sich für Session 7 angemeldet\n",
|
|
"User 78 hat sich für Session 1 angemeldet\n",
|
|
"User 14 hat sich für Session 6 angemeldet\n",
|
|
"User 84 hat sich für Session 7 angemeldet\n",
|
|
"Session 1 ist ausgebucht\n",
|
|
"User 75 hat sich für Session 8 angemeldet\n",
|
|
"Session 7 ist ausgebucht\n",
|
|
"Session 3 ist ausgebucht\n",
|
|
"Session 7 ist ausgebucht\n",
|
|
"User 97 hat sich für Session 4 angemeldet\n",
|
|
"User 93 hat sich für Session 6 angemeldet\n",
|
|
"User 28 hat sich für Session 2 angemeldet\n",
|
|
"User 92 hat sich für Session 8 angemeldet\n",
|
|
"User 88 hat sich für Session 5 angemeldet\n",
|
|
"Session 7 ist ausgebucht\n",
|
|
"User 64 hat sich für Session 2 angemeldet\n",
|
|
"User 14 hat sich für Session 6 angemeldet\n",
|
|
"Session 7 ist ausgebucht\n",
|
|
"User 74 hat sich für Session 5 angemeldet\n",
|
|
"Session 3 ist ausgebucht\n",
|
|
"Session 1 ist ausgebucht\n",
|
|
"Session 7 ist ausgebucht\n",
|
|
"Session 2 ist ausgebucht\n",
|
|
"Session 7 ist ausgebucht\n",
|
|
"Session 5 ist ausgebucht\n",
|
|
"Session 4 ist ausgebucht\n",
|
|
"Session 1 ist ausgebucht\n",
|
|
"Session 7 ist ausgebucht\n",
|
|
"Session 7 ist ausgebucht\n",
|
|
"Session 4 ist ausgebucht\n",
|
|
"Session 3 ist ausgebucht\n",
|
|
"Session 2 ist ausgebucht\n",
|
|
"Session 7 ist ausgebucht\n",
|
|
"Session 3 ist ausgebucht\n",
|
|
"Session 7 ist ausgebucht\n",
|
|
"Session 3 ist ausgebucht\n",
|
|
"Session 8 ist ausgebucht\n",
|
|
"Session 8 ist ausgebucht\n",
|
|
"Session 3 ist ausgebucht\n",
|
|
"User 49 hat sich für Session 6 angemeldet\n",
|
|
"Session 3 ist ausgebucht\n",
|
|
"Session 1 ist ausgebucht\n",
|
|
"User 23 hat sich für Session 6 angemeldet\n",
|
|
"Session 2 ist ausgebucht\n",
|
|
"Session 1 ist ausgebucht\n",
|
|
"Session 7 ist ausgebucht\n",
|
|
"Session 2 ist ausgebucht\n",
|
|
"Session 8 ist ausgebucht\n",
|
|
"Session 2 ist ausgebucht\n",
|
|
"Session 8 ist ausgebucht\n",
|
|
"Session 5 ist ausgebucht\n",
|
|
"User 9 hat sich für Session 6 angemeldet\n",
|
|
"Session 3 ist ausgebucht\n",
|
|
"Session 5 ist ausgebucht\n",
|
|
"Session 4 ist ausgebucht\n",
|
|
"Session 7 ist ausgebucht\n",
|
|
"User 13 hat sich für Session 6 angemeldet\n",
|
|
"Session 2 ist ausgebucht\n",
|
|
"Session 1 ist ausgebucht\n",
|
|
"Session 2 ist ausgebucht\n",
|
|
"Session 1 ist ausgebucht\n",
|
|
"Session 8 ist ausgebucht\n",
|
|
"Session 7 ist ausgebucht\n",
|
|
"Session 5 ist ausgebucht\n",
|
|
"User 73 hat sich für Session 6 angemeldet\n",
|
|
"Session 8 ist ausgebucht\n",
|
|
"Session 8 ist ausgebucht\n",
|
|
"Session 3 ist ausgebucht\n",
|
|
"Session 1 ist ausgebucht\n",
|
|
"Session 2 ist ausgebucht\n",
|
|
"Session 4 ist ausgebucht\n",
|
|
"Session 5 ist ausgebucht\n",
|
|
"Session 4 ist ausgebucht\n",
|
|
"Session 3 ist ausgebucht\n",
|
|
"Session 2 ist ausgebucht\n",
|
|
"Session 1 ist ausgebucht\n",
|
|
"Session 7 ist ausgebucht\n",
|
|
"Session 7 ist ausgebucht\n",
|
|
"Session 5 ist ausgebucht\n",
|
|
"Nach dem Lasttest:\n",
|
|
"--------------------\n",
|
|
"Freie Plätze: 2\n",
|
|
"Belegte Plätze: 38\n",
|
|
"Gesamt: 40\n",
|
|
"Platzanzahl ist konsistent!\n",
|
|
"--------------------\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"import threading\n",
|
|
"\n",
|
|
"# Diese Funktion gibt Informationen über die Belegung der Seminare aus\n",
|
|
"def print_info():\n",
|
|
" print('-'*20)\n",
|
|
"\n",
|
|
" free_seats = count_free_seats()\n",
|
|
" occupied_seats = count_occupied_seats()\n",
|
|
"\n",
|
|
" print(\"Freie Plätze:\", free_seats)\n",
|
|
" print(\"Belegte Plätze:\", occupied_seats)\n",
|
|
" print(\"Gesamt:\", free_seats + occupied_seats)\n",
|
|
"\n",
|
|
" if free_seats + occupied_seats == 40:\n",
|
|
" print(\"Platzanzahl ist konsistent!\")\n",
|
|
" else:\n",
|
|
" print(\"Platzanzahl ist inkonsistent!\")\n",
|
|
"\n",
|
|
" print('-'*20)\n",
|
|
"\n",
|
|
"# -------------------------------------------------------------------------\n",
|
|
"# Mini-Lasttest, 5 Threads starten, die jeweils 20 Reservierungen vornehmen\n",
|
|
"# -------------------------------------------------------------------------\n",
|
|
"\n",
|
|
"print(\"Vor dem Lasttest:\")\n",
|
|
"print_info()\n",
|
|
"\n",
|
|
"threads = [ threading.Thread(target=random_seat_reservation) for _ in range(5)]\n",
|
|
"\n",
|
|
"# Threads starten\n",
|
|
"for t in threads:\n",
|
|
" t.start()\n",
|
|
"\n",
|
|
"# Auf das Ende der Threads warten\n",
|
|
"for t in threads:\n",
|
|
" t.join()\n",
|
|
"\n",
|
|
"\n",
|
|
"print(\"Nach dem Lasttest:\")\n",
|
|
"print_info()"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "code (3.13.2)",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 3
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython3",
|
|
"version": "3.13.3"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 2
|
|
}
|