PostgreSQL: المعاملات والتحكم في التزامن في PostgreSQL
آخر تحديث: 2026-08-26
1. ما ستتعلمه
- فهم خصائص ACID الأربعة وكيف تنفذها PostgreSQL
- استخدام
BEGIN/COMMIT/ROLLBACKللتحكم في المعاملات - استخدام
SAVEPOINTللتراجع الجزئي داخل المعاملة - فهم الاختلافات بين مستويات العزل الثلاثة في PostgreSQL وكيفية الاختيار
- إتقان آلية التحكم في التزامن متعدد الإصدارات MVCC
- استخدام أقفال الصفوف، أقفال الجداول، و Advisory Locks
- فهم اكتشاف ومعالجة الجمود (Deadlock)
- مقارنة استرا��يجيات القفل المتشائم مقابل القفل المتفائل
- استخدام الالتزام على مرحلتين (2PC) للمعاملات الموزعة
2. القصة
Alice مسؤولة عن نظام الطلبات في منصة تجارة إلكترونية. خلال عرض ترويجي، كان لدى عنصر محدود الإصدار وحدة واحدة فقط متبقية في المخزون، وقام مستخدمان بتقديم طلب في نفس الوقت تقريباً:
- قرأ المستخدم A المخزون كـ 1 واستعد للخصم
- قرأ المستخدم B أيضاً المخزون كـ 1 واستعد للخصم
بدون حماية المعاملات، يمكن لكلاهما تقديم الطلب بنجاح، مما يدفع المخزون إلى -1 ويسبب بيعاً زائداً. تحتاج Alice إلى عزل المعاملات في PostgreSQL، MVCC، وآليات قفل الصفوف لضمان أن مستخدماً واحداً فقط ينجح في خصم المخزون بينما يتم حظر الآخر أو يحصل على خطأ — ضماناً لاتساق البيانات.
3. المفهوم: ACID وأساسيات المعاملات
(1) خصائص ACID الأربعة
| الخاصية | الإنجليزية | المعنى | تنفيذ PostgreSQL |
|---|---|---|---|
| الذرية | Atomicity | المعاملة كل شيء أو لا شيء: تنجح بالكامل أو تتراجع بالكامل | WAL (سجل الكتابة المسبقة) |
| الاتساق | Consistency | قاعدة البيانات تحقق قيودها قبل وبعد المعاملة | القيود، المشغلات، فحوصات الأنواع |
| العزل | Isolation | المعاملات المتزامنة لا تتداخل مع بعضها | MVCC + الأقفال |
| المتانة | Durability | البيانات المودعة لا تفقد | WAL + fsync |
(1) ▶ مثال: ذرية ACID — التحويل إما ينجح بالكامل أو يتراجع بالكامل
BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 500 WHERE account_id = 2;
COMMIT;
Output:
-- SQL statement executed successfully
(2) عبارات التحكم في المعاملات
| العبارة | الغرض | SQL المقابل |
|---|---|---|
| بدء المعاملة | فتح معاملة صراحةً | BEGIN أو START TRANSACTION |
| إيداع المعاملة | استمرار جميع التغييرات | COMMIT |
| التراجع عن المعاملة | التراجع عن جميع التغييرات | ROLLBACK |
(2) ▶ مثال: التراجع عن معاملة لاستعادة البيانات
BEGIN;
DELETE FROM orders WHERE order_id = 999;
-- عذراً، حذف خاطئ!
ROLLBACK;
-- تم استعادة البيانات
Output:
-- SQL statement executed successfully
(3) وضع الالتزام التلقائي
PostgreSQL تفعل الالتزام التلقائي افتراضياً — كل عبارة SQL تودع تلقائياً كمعاملة خاصة بها.
| الوضع | السلوك | إعداد psql |
|---|---|---|
| الالتزام التلقائي ON | كل عبارة تودع تلقائياً | افتراضي |
| الالتزام التلقائي OFF | يتطلب COMMIT يدوياً | \set AUTOCOMMIT off |
(3) ▶ مثال: تعطيل الالتزام التلقائي والإيداع يدوياً
\set AUTOCOMMIT off
DELETE FROM orders WHERE order_status = 'cancelled';
-- تحقق قبل الإيداع
SELECT count(*) FROM orders WHERE order_status = 'cancelled';
COMMIT;
Output:
# command executed successfully
(4) SAVEPOINT التراجع الجزئي
تعيين نقطة حفظ داخل المعاملة بحيث يمكنك التراجع فقط إلى تلك النقطة بدلاً من المعاملة بالكا��ل.
(4) ▶ مثال: استخدام SAVEPOINT للتراجع الجزئي
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
SAVEPOINT sp1;
UPDATE accounts SET balance = balance - 200 WHERE account_id = 1;
-- التحديث الثاني كان خاطئاً، تراجع إلى نقطة الحفظ
ROLLBACK TO SAVEPOINT sp1;
-- التحديث الأول لا يزال نشطاً، يمكن الإيداع
COMMIT;
Output:
-- SQL statement executed successfully
| العبارة | الغرض |
|---|---|
SAVEPOINT sp_name |
إنشاء نقطة حفظ |
ROLLBACK TO SAVEPOINT sp_name |
التراجع إلى نقطة حفظ |
RELEASE SAVEPOINT sp_name |
تحرير نقطة حفظ (التراجعات اللاحقة لا يمكنها الإشارة إليها) |
4. المفهوم: مستويات عزل المعاملات
(1) مستويات العزل الثلاثة في PostgreSQL
تنفذ PostgreSQL ثلاثة مستويات عزل فقط (READ UNCOMMITTED تعين إلى READ COMMITTED):
| مستوى العزل | قراءة متسخة | قراءة غير قابلة للتكرار | قراءة وهمية | تنفيذ PG |
|---|---|---|---|---|
| READ COMMITTED | مستحيلة | ممكنة | ممكنة | المستوى الافتراضي؛ كل استعلام يرى آخر لقطة مودعة |
| REPEATABLE READ | مستحيلة | مستحيلة | ممكنة (PG تمنعها فعلياً) | المعاملة ترى اللقطة عند بدايتها |
| SERIALIZABLE | مستحيلة | مستحيلة | مستحيلة | الأكثر صرامة؛ تكتشف تعارضات التسلسل |
(5) ▶ مثال: تعيين مستوى العزل
BEGIN ISOLATION LEVEL READ COMMITTED;
-- أو
BEGIN ISOLATION LEVEL REPEATABLE READ;
-- أو
BEGIN ISOLATION LEVEL SERIALIZABLE;
Output:
-- SQL statement executed successfully
(2) READ COMMITTED مقابل REPEATABLE READ عملياً
(6) ▶ مثال: READ COMMITTED — استعلامان في نفس المعاملة يعيدان نتائج مختلفة
-- الجلسة 1
BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT balance FROM accounts WHERE account_id = 1; -- يعيد 1000
-- الجلسة 2 (اتصال آخر)
UPDATE accounts SET balance = 800 WHERE account_id = 1;
COMMIT;
-- العودة إلى الجلسة 1
SELECT balance FROM accounts WHERE account_id = 1; -- يعيد 800 (يرى التغيير المودع)
COMMIT;
Output:
count
-------
5
(1 row)
(7) ▶ مثال: REPEATABLE READ — استعلامان في نفس المعاملة يعيدان نتائج متسقة
-- الجلسة 1
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE account_id = 1; -- يعيد 1000
-- الجلسة 2 (اتصال آخر)
UPDATE accounts SET balance = 800 WHERE account_id = 1;
COMMIT;
-- العودة إلى الجلسة 1
SELECT balance FROM accounts WHERE account_id = 1; -- لا يزال يعيد 1000
COMMIT;
Output:
count
-------
5
(1 row)
(3) مستوى العزل SERIALIZABLE
SERIALIZABLE هو المستوى الأكثر صرامة؛ PostgreSQL تستخدم عزل اللقطة القابل للتسلسل (SSI) لاكتشاف تعارضات التسلسل.
(8) ▶ مثال: اكتشاف تعارض SERIALIZABLE
-- الجلسة 1
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT balance FROM accounts WHERE account_id = 1;
-- الجلسة 2
BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
COMMIT;
-- العودة إلى الجلسة 1
UPDATE accounts SET balance = balance + 50 WHERE account_id = 1;
-- ERROR: could not serialize access due to concurrent update
Output:
count
-------
5
(1 row)
| اختيار مستوى العزل | السيناريو | المستوى الموصى به |
|---|---|---|
| معظم تطبيقات الويب | READ COMMITTED | افتراضي، يوازن الأداء والاتساق |
| التقارير/التدقيق التي تحتاج لقطة متسقة | REPEATABLE READ | قراءات نفس المعاملة تبقى متسقة |
| المعاملات المصرفية/المالية الأساسية | SERIALIZABLE | الأكثر صرامة، يقايض الأداء بالأمان |
5. المفهوم: MVCC التحكم في التزامن متعدد الإصدارات
(1) المبدأ الأساسي لـ MVCC
MVCC (التحكم في التزامن متعدد الإصدارات) هو جوهر التحكم في التزامن في PostgreSQL. كل معاملة ترى لقطة من البيانات عند نقطة زمنية؛ القراءات لا تحظر الكتابات، والكتابات لا تحظر القراءات.
كل إصدار صف (tuple) يحتوي أربعة حقول مخفية:
| الحقل | المعنى |
|---|---|
xmin |
معرف المعاملة التي أدرجت الصف |
xmax |
معرف المعاملة التي حذفت/حدثت الصف (0 يعني لا يزال صالحاً) |
رؤية xmin |
معاملة معرفها < اللقطة الحالية يمكنها رؤية هذا الصف |
رؤية xmax |
معاملة معرفها >= اللقطة الحالية لا يمكنها رؤية هذا الحذف |
(9) ▶ مثال: عرض معلومات إصدار الصف
SELECT xmin, xmax, * FROM products WHERE product_id = 1;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(2) تدفق التزامن في القراءة/الكتابة MVCC
sequenceDiagram
participant R as قارئ(Tx1)
participant W as كاتب(Tx2)
participant T as جدول
R->>T: SELECT (لقطة عند بدء Tx1)
T-->>R: يعيد الإصدار V1 (xmin=100, xmax=0)
W->>T: UPDATE (ينشئ إصداراً جديداً)
T-->>T: V1 xmax=200, V2 xmin=200 xmax=0
R->>T: SELECT مرة أخرى (نفس اللقطة)
T-->>R: لا يزال يعيد V1 (Tx2 لم يودع)
W->>T: COMMIT
Note over T: V2 الآن مرئي للمعاملات الجديدة
R->>T: SELECT مرة أخرى (نفس اللقطة)
T-->>R: لا يزال يعيد V1 (REPEATABLE READ)
(10) ▶ مثال: قراءات MVCC لا تحظر الكتابات
-- الجلسة 1: قراءة طويلة الأمد
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM orders WHERE order_date = '2025-01-01';
-- هذا الاستعلام لا يحظر الكتّاب
-- الجلسة 2: كتابة متزامنة (لا حظر من الجلسة 1)
UPDATE orders SET order_status = 'shipped' WHERE order_id = 100;
COMMIT;
Output:
UPDATE 3
(3) تكلفة MVCC: الصفوف الميتة و VACUUM
تحت MVCC، UPDATE لا تعدل الصف الأصلي — تنشئ إصداراً جديداً. الصف القديم يصبح صفاً ميتاً ويحتاج تنظيف VACUUM.
| العملية | سلوك MVCC | الصفوف الميتة |
|---|---|---|
| INSERT | تنشئ صفاً جديداً | لا شيء |
| DELETE | توسم xmax للصف القديم | تنتج صفاً ميتاً واحداً |
| UPDATE | توسم xmax للصف القديم + تنشئ صفاً جديداً | تنتج صفاً ميتاً واحداً |
(11) ▶ مثال: عرض عدد الصفوف الميتة
SELECT relname, n_live_tup, n_dead_tup, last_vacuum, last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(12) ▶ مثال: تشغيل VACUUM يدوياً
VACUUM orders; -- استعادة المساحة، لا تحظر القراءة
VACUUM FULL orders; -- إعادة كتابة كاملة للجدول، تقفل الجدول
VACUUM ANALYZE orders; -- استعادة + تحديث الإحصائيات
Output:
-- SQL statement executed successfully
| نوع VACUUM | مستوى القفل | استعادة المساحة | السرعة |
|---|---|---|---|
VACUUM |
SHARE | توسم قابلة لإعاد�� الاستخدام، لا تعيد للقرص | سريعة |
VACUUM FULL |
ACCESS EXCLUSIVE | تعيد بالكامل للقرص | بطيئة، تقفل الجدول |
VACUUM ANALYZE |
SHARE | نفس VACUUM + تحديث الإحصائيات | سريعة |
6. المفهوم: الأقفال
(1) أقفال مستوى الصف
PostgreSQL تكتسب تلقائياً قفل صف عند تعديل صف؛ المعاملات الأخرى يجب أن تنتظر.
| نوع قفل الصف | كيفية الاكتساب | التعارض |
|---|---|---|
| FOR UPDATE | SELECT ... FOR UPDATE |
قفل حصري، يحظر FOR UPDATE / FOR NO KEY UPDATE أخرى |
| FOR NO KEY UPDATE | SELECT ... FOR NO KEY UPDATE |
لا يحظر FOR KEY SHARE |
| FOR SHARE | SELECT ... FOR SHARE |
يحظر FOR UPDATE / FOR NO KEY UPDATE |
| FOR KEY SHARE | SELECT ... FOR KEY SHARE |
الأضعف، يحظر FOR UPDATE فقط |
(13) ▶ مثال: SELECT FOR UPDATE يمنع البيع الزائد
BEGIN;
SELECT stock FROM products WHERE product_id = 1 FOR UPDATE;
-- stock = 1، الصف مقفل
UPDATE products SET stock = stock - 1 WHERE product_id = 1;
COMMIT;
-- إذا حاولت معاملة أخرى FOR UPDATE على نفس الصف، تنتظر
Output:
UPDATE 3
(2) أقفال مستوى الجدول
| وضع القفل | كيفية الاكتساب | التعارض |
|---|---|---|
| ACCESS SHARE | SELECT |
يتعارض مع ACCESS EXCLUSIVE |
| ROW SHARE | SELECT FOR |
يتعارض مع EXCLUSIVE / ACCESS EXCLUSIVE |
| ROW EXCLUSIVE | UPDATE/DELETE |
يتعارض مع SHARE / EXCLUSIVE، إلخ |
| SHARE | LOCK TABLE ... SHARE |
يتعارض مع ROW EXCLUSIVE، إلخ |
| ACCESS EXCLUSIVE | ALTER TABLE |
يتعارض مع جميع الأقفال |
(14) ▶ مثال: قفل جدول صريح
LOCK TABLE orders IN ACCESS EXCLUSIVE MODE;
-- تنفيذ عمليات حرجة
COMMIT;
Output:
-- SQL statement executed successfully
(3) Advisory Locks (خاصة بـ PostgreSQL)
Advisory Locks هي أقفال على مستوى التطبيق غير مرتبطة بأي صف جدول، مناسبة للتنسيق الموزع.
| الدالة | الميزة |
|---|---|
pg_try_advisory_lock(id) |
غير حاظرة؛ تعيد false فوراً عند الفشل |
pg_advisory_lock(id) |
تحظر منتظرة حتى الاكتساب |
pg_advisory_unlock(id) |
تحرير القفل |
pg_advisory_xact_lock(id) |
تحرر تلقائياً عند نهاية المعاملة |
(15) ▶ مثال: استخدام Advisory Lock لمنع المعالجة المكررة
-- محاولة غير حاظرة
SELECT pg_try_advisory_lock(12345);
-- تعيد true: حصلت على القفل، تابع
-- تعيد false: جلسة أخرى تملكه، تجاوز
-- قفل استشاري على مستوى المعاملة (تحرير تلقائي عند COMMIT/ROLLBACK)
BEGIN;
SELECT pg_advisory_xact_lock(12345);
-- قم بالعمل...
COMMIT; -- القفل يحرر تلقائياً
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(16) ▶ مثال: Advisory Lock للمهام أحادية المثيل
CREATE FUNCTION run_daily_report() RETURNS void AS $$
BEGIN
IF pg_try_advisory_lock(99999) THEN
-- جلسة واحدة فقط يمكنها تشغيل هذا في وقت واحد
INSERT INTO report_log (report_date, status)
VALUES (CURRENT_DATE, 'running');
PERFORM pg_sleep(5); -- محاكاة العمل
UPDATE report_log SET status = 'done' WHERE report_date = CURRENT_DATE;
PERFORM pg_advisory_unlock(99999);
ELSE
RAISE NOTICE 'Report already running in another session';
END IF;
END;
$$ LANGUAGE plpgsql;
Output:
INSERT 0 1
7. المفهوم: اكتشاف ومعالجة الجمود (Deadlock)
(1) كيف ينشأ الجمود
معاملتان تنتظران أقفال بعضهما البعض، مشكلتين اعتماداً دائرياً.
flowchart LR
A[Tx1: قفل الصف A] -->|ينتظر الصف B| B[Tx2: قفل الصف B]
B -->|ينتظر الصف A| A
A -->|جمود!| C[PG تكتشف تلقائياً في 1s]
(17) ▶ مثال: سيناريو الجمود
-- الجلسة 1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1; -- قفل الصف 1
-- الجلسة 2
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 2; -- قفل الصف 2
-- الجلسة 1 (تنتظر الآن الصف 2)
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
-- الجلسة 2 (جمود!)
UPDATE accounts SET balance = balance + 100 WHERE account_id = 1;
-- ERROR: deadlock detected
Output:
-- SQL statement executed successfully
(2) اكتشاف ومنع الجمود
| الاستراتيجية | الوصف |
|---|---|
| اكتشاف تلقائي | PostgreSQL تتحقق مرة في الثانية افتراضياً (deadlock_timeout) |
| تراجع تلقائي | عند اكتشاف جمود، تتراجع تلقائياً عن معاملة واحدة |
| ترتيب قفل ثابت | اكتسب الأقفال دائماً بنفس الترتيب، متجنباً الانتظارات الدائرية |
| معاملات قصيرة | قلل الوقت الذي تحتفظ فيه المعاملة بالأقفال |
(18) ▶ مثال: منع الجمود بترتيب قفل ثابت
-- دائماً اقفل الصفوف بترتيب account_id
BEGIN;
SELECT * FROM accounts WHERE account_id IN (1, 2) ORDER BY account_id FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT;
Output:
count
-------
5
(1 row)
8. المفهوم: القفل المتشائم مقابل القفل المتفائل
(1) مقارنة استراتيجيتي التزامن
| البعد | القفل المتشائم | القفل المتفائل |
|---|---|---|
| الفكرة | اقفل أولاً، ثم عدل؛ احظر عند التعارض | عدل أولاً، ثم تحقق؛ أعد المحاولة عند التعارض |
| التنفيذ | SELECT FOR UPDATE |
عمود version + WHERE version = old |
| تكرار التعارض | مناسب لسيناريوهات التعارض العالي | مناسب لسيناريوهات التعارض المنخف�� |
| الأداء | عبء انتظار القفل | عبء إعادة المحاولة |
| خطر الجمود | نعم | لا |
(19) ▶ مثال: قفل متشائم لخصم المخزون
BEGIN;
SELECT stock FROM products WHERE product_id = 1 FOR UPDATE;
-- stock = 1، الصف مقفل
IF stock > 0 THEN
UPDATE products SET stock = stock - 1 WHERE product_id = 1;
END IF;
COMMIT;
Output:
UPDATE 3
(1) ▶ مال: قفل متفائل لخصم المخزون
-- إضافة عمود version
ALTER TABLE products ADD COLUMN version INT DEFAULT 1;
-- تحديث متفائل
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE product_id = 1 AND version = 5;
-- إذا كانت الصفوف المتأثرة = 0، شخص آخر عدلها، تحتاج إعادة المحاولة
Output:
-- SQL statement executed successfully
(20) ▶ مثال: منطق إعادة المحاولة في طبقة التطبيق للقفل المتفائل
CREATE FUNCTION deduct_stock(p_id INT, p_qty INT) RETURNS BOOLEAN AS $$
DECLARE
v_version INT;
v_stock INT;
v_updated INT;
BEGIN
LOOP
SELECT stock, version INTO v_stock, v_version
FROM products WHERE product_id = p_id;
IF v_stock < p_qty THEN RETURN FALSE; END IF;
UPDATE products
SET stock = stock - p_qty, version = version + 1
WHERE product_id = p_id AND version = v_version;
GET DIAGNOSTICS v_updated = ROW_COUNT;
IF v_updated > 0 THEN RETURN TRUE; END IF;
-- تعارض، أعد المحاولة
END LOOP;
END;
$$ LANGUAGE plpgsql;
Output:
count
-------
5
(1 row)
9. المفهوم: الالتزام على مرحلتين (2PC)
(1) 2PC للمعاملات الموزعة
عندما تمتد المعاملة عبر قواعد بيانات متعددة أو موارد خارجية، يحتاج الالتزا�� على مرحلتين لضمان الذرية.
| المرحلة | العملية | الوصف |
|---|---|---|
| PREPARE | PREPARE TRANSACTION 'tx_id' |
تدخل المعاملة حالة محضرة، مكتوبة في WAL |
| COMMIT | COMMIT PREPARED 'tx_id' |
المرحلة 2 تؤكد الالتزام |
| ROLLBACK | ROLLBACK PREPARED 'tx_id' |
المرحلة 2 تؤكد التراجع |
(21) ▶ مثال: تدفق الالتزام على مرحلتين
-- المرحلة 1: التحضير
BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE account_id = 1;
PREPARE TRANSACTION 'transfer_out';
-- (المنسق يؤكد أن جميع المشاركين محضرون)
-- المرحلة 2: الالتزام
COMMIT PREPARED 'transfer_out';
Output:
-- SQL statement executed successfully
(22) ▶ مثال: عرض المعاملات المحضرة
SELECT * FROM pg_prepared_xacts;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| المعامل | الافتراضي | الوصف |
|---|---|---|
max_prepared_transactions |
0 | يجب أن يكون > 0 لاستخدام 2PC |
deadlock_timeout |
1s | فاصل اكتشاف الجمود |
idle_in_transaction_session_timeout |
0 | مهلة خمول المعاملة (مللي ثانية) |
10. تطبيق عملي: معاملة خصم مخزون كاملة للتجارة الإلكترونية
تحتاج Alice إلى معاملة خصم مخزون كاملة تعالج الطلبات المتزامنة، تمنع البيع الزائد، وتسجل الطلب.
-- الخطوة 1: إنشاء الجداول
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name TEXT NOT NULL,
stock INT NOT NULL DEFAULT 0,
price NUMERIC(10,2) NOT NULL,
version INT NOT NULL DEFAULT 1
);
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
total_price NUMERIC(10,2) NOT NULL,
order_status TEXT DEFAULT 'pending',
created_at TIMESTAMP DEFAULT now()
);
INSERT INTO products VALUES
(1, 'Limited Edition Watch', 1, 299.99, 1),
(2, 'Wireless Headphones', 50, 89.99, 1);
-- الخطوة 2: نهج القفل المتشائم للعناصر الساخنة
CREATE FUNCTION place_order(
p_customer_id INT, p_product_id INT, p_qty INT
) RETURNS INT AS $$
DECLARE
v_stock INT;
v_order_id INT;
BEGIN
BEGIN
-- قفل صف المنتج
SELECT stock INTO v_stock
FROM products
WHERE product_id = p_product_id
FOR UPDATE;
IF v_stock < p_qty THEN
RAISE EXCEPTION 'Insufficient stock: % < %', v_stock, p_qty;
END IF;
-- خصم المخزون
UPDATE products
SET stock = stock - p_qty
WHERE product_id = p_product_id;
-- إنشاء طلب
INSERT INTO orders (customer_id, product_id, quantity, total_price)
VALUES (p_customer_id, p_product_id, p_qty,
p_qty * (SELECT price FROM products WHERE product_id = p_product_id))
RETURNING order_id INTO v_order_id;
RETURN v_order_id;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE '%', SQLERRM;
RETURN -1;
END;
END;
$$ LANGUAGE plpgsql;
-- الخطوة 3: اختبار الطلبات المتزامنة
SELECT place_order(101, 1, 1); -- نجاح، تم إنشاء الطلب
SELECT place_order(102, 1, 1); -- فشل، المخزون غير كافٍ
-- الخطوة 4: التحقق من النتائج
SELECT product_id, stock FROM products WHERE product_id = 1;
SELECT order_id, customer_id, order_status FROM orders;
❓ أسئلة شائعة
📖 ملخص
- ACID هي الضمانات الأربعة للمعاملة، تنفذها PostgreSQL عبر WAL، MVCC، القيود، والأقفال
BEGIN/COMMIT/ROLLBACKتتحكم في حدود المعاملة؛SAVEPOINTتمكن التراجع الجزئي- PostgreSQL تنفذ ثلاثة مستويات عزل: READ COMMITTED (افتراضي)، REPEATABLE READ، SERIALIZABLE
- MVCC تجعل القراءات والكتابات لا تحظر بعضها؛ كل معاملة ترى لقطة بيانات؛ التحديثات تنشئ إصدارات جديدة
- الصفوف الميتة تنظف بواسطة VACUUM/autovacuum؛ سيناريوهات التحديث العالي تحتاج اهتماماً باستراتيجية التنظيف
- قفل الصف
SELECT FOR UPDATEيمنع تعارضات التعديل المتزامن؛ Advisory Lock للتنسيق على مستوى التطبيق - الجمود يكتشف تلقائياً بواسطة PostgreSQL (ثانية واحدة) وتتراجع معاملة واحدة
- الأقفال المتشائمة تناسب سيناريوهات التعارض العالي؛ الأقفال المتفائلة تناسب سيناريوهات التعارض المنخفض
- الالتزام على مرحلتين (2PC) يضمن الذرية للمعاملات الموزعة
📝 تمارين
-
⭐ اكتب معاملة تحول 200 دولار من الحساب 1 إلى الحساب 2 في جدول
accounts، وتتراجع إذا كان رصيد الحساب 1 غير كافٍ. -
⭐⭐ استخدم
SAVEPOINTلكتابة معاملة: أولاً أدرج سجل طلب، ثم حاول تحديث المخزون؛ إذا كان المخزون غير كافٍ، تراجع إلى نقطة الحفظ لكن احتفظ بالطلب (علّم حالته كـ 'failed')، ثم أودع المعاملة. -
⭐⭐⭐ نفذ إصدار قفل متفائل لخصم المخزون: استخدم عمود version، أعد المحاولة تلقائياً عند التعارض المتزامن حتى 3 مرات، وأعد FALSE إذا فشلت جميع المحاولات الـ 3. أيضاً سجل كل محاولة في جدول
retry_log.