Handling Race Conditions in PostgreSQL MVCC

PostgreSQL’s Multi-Version Concurrency Control (MVCC) architecture represents one of the most sophisticated approaches to handling concurrent database operations. By maintaining multiple versions of data rows and providing each transaction with a consistent snapshot of the database state, MVCC allows readers and writers to operate without blocking each other. However, despite these elegant mechanisms, race conditions can still emerge in specific scenarios, particularly when multiple transactions attempt to modify the same data concurrently.

Understanding and mitigating these race conditions is crucial for building robust, high-performance applications that can handle concurrent workloads safely and efficiently.


Understanding Race Conditions in MVCC Context

The Foundation: How MVCC Works

MVCC operates on the principle of snapshot isolation, where each transaction sees a consistent view of the database as it existed at the transaction’s start time. This is achieved through:

  • Transaction IDs (XIDs): Each transaction receives a unique identifier
  • Row Versioning: Multiple versions of the same logical row can coexist
  • Visibility Rules: Transactions can only see data committed before their snapshot time
  • Tuple Headers: Each row contains metadata about which transactions can see it

When Race Conditions Occur

Despite MVCC’s robustness, race conditions manifest in several critical scenarios:

1. Read-Modify-Write Conflicts; When multiple transactions read the same data, perform calculations, and attempt to write back results, the classical “lost update” problem occurs. Each transaction operates on potentially stale data, leading to incorrect final states.

2. Existence Checks and Insertions; The “phantom read” phenomenon becomes problematic when transactions check for the absence of data before inserting. Between the check and insert operations, another transaction might create the very data being tested for absence.

3. Concurrent Row Updates; When multiple transactions target the same row for updates, PostgreSQL’s row-level locking mechanisms engage, but without proper handling, this can lead to blocking, deadlocks, or serialization failures.

4. Aggregate Operations; Operati ons that depend on aggregate values (sums, counts, etc.) across multiple rows are particularly vulnerable, as the underlying data may change between reading and acting upon the aggregate.


Comprehensive Strategies for Race Condition Mitigation

1. Pessimistic Locking: SELECT … FOR UPDATE

Pessimistic locking provides the strongest guarantee against concurrent modifications by explicitly locking rows before modification.

-- Basic row locking
BEGIN;
SELECT account_id, balance, last_updated 
FROM accounts 
WHERE account_id = $1 
FOR UPDATE;

-- Perform business logic validation
-- Update with confidence that no other transaction can interfere
UPDATE accounts 
SET balance = balance - $2, 
    last_updated = NOW()
WHERE account_id = $1;

COMMIT;

Lock Variants and Their Use Cases:

-- FOR UPDATE: Full row lock, blocks all concurrent updates
SELECT * FROM orders WHERE id = 123 FOR UPDATE;

-- FOR NO KEY UPDATE: Allows key updates but blocks non-key updates
SELECT * FROM products WHERE sku = 'ABC123' FOR NO KEY UPDATE;

-- FOR SHARE: Allows concurrent reads but blocks writes
SELECT * FROM inventory WHERE product_id = 456 FOR SHARE;

-- FOR KEY SHARE: Allows all operations except key updates
SELECT * FROM references WHERE target_id = 789 FOR KEY SHARE;

Advanced Locking Patterns:

-- Conditional locking with NOWAIT
BEGIN;
SELECT * FROM critical_resource 
WHERE id = $1 
FOR UPDATE NOWAIT;
-- Fails immediately if lock cannot be acquired

-- Skip locked rows pattern
SELECT * FROM work_queue 
WHERE status = 'pending' 
FOR UPDATE SKIP LOCKED 
LIMIT 10;
-- Useful for job queue processors

2. Atomic Operations: INSERT … ON CONFLICT

The UPSERT pattern eliminates race conditions by making check-and-insert operations atomic.

-- Basic upsert
INSERT INTO user_sessions (user_id, session_token, expires_at)
VALUES ($1, $2, $3)
ON CONFLICT (user_id) 
DO UPDATE SET 
    session_token = EXCLUDED.session_token,
    expires_at = EXCLUDED.expires_at,
    updated_at = NOW();

Complex Conflict Resolution:

-- Conditional updates in conflict resolution
INSERT INTO product_inventory (product_id, quantity, reserved)
VALUES ($1, $2, 0)
ON CONFLICT (product_id) 
DO UPDATE SET 
    quantity = CASE 
        WHEN product_inventory.quantity + EXCLUDED.quantity >= 0 
        THEN product_inventory.quantity + EXCLUDED.quantity
        ELSE product_inventory.quantity
    END,
    last_restocked = CASE 
        WHEN EXCLUDED.quantity > 0 
        THEN NOW()
        ELSE product_inventory.last_restocked
    END
WHERE product_inventory.quantity + EXCLUDED.quantity >= 0;

3. Application-Level Synchronization: Advisory Locks

Advisory locks provide application-controlled synchronization mechanisms beyond row-level locking.

-- Transaction-level advisory locks
BEGIN;
SELECT pg_advisory_xact_lock(hashtext('user_' || $1));
-- Critical section: complex multi-table operations
-- Lock automatically released on transaction end
COMMIT;

-- Session-level advisory locks for longer operations
SELECT pg_advisory_lock(12345);
-- Perform long-running maintenance
SELECT pg_advisory_unlock(12345);

Practical Advisory Lock Patterns:

-- Non-blocking advisory lock attempt
DO $$
BEGIN
    IF pg_try_advisory_xact_lock(hashtext('daily_report_' || CURRENT_DATE)) THEN
        -- Generate report
        INSERT INTO daily_reports (date, data) 
        VALUES (CURRENT_DATE, calculate_daily_stats());
    ELSE
        RAISE NOTICE 'Daily report generation already in progress';
    END IF;
END $$;

4. Isolation Level Management: SERIALIZABLE

The SERIALIZABLE isolation level provides the highest level of consistency by detecting serialization anomalies and forcing transaction retries.

-- Serializable transaction with retry logic
CREATE OR REPLACE FUNCTION transfer_funds(
    from_account INTEGER, 
    to_account INTEGER, 
    amount DECIMAL
) RETURNS BOOLEAN AS $$
DECLARE
    max_retries INTEGER := 3;
    retry_count INTEGER := 0;
BEGIN
    LOOP
        BEGIN
            -- Start serializable transaction
            SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
            
            -- Validate source account
            UPDATE accounts 
            SET balance = balance - amount 
            WHERE account_id = from_account 
            AND balance >= amount;
            
            IF NOT FOUND THEN
                RAISE EXCEPTION 'Insufficient funds';
            END IF;
            
            -- Credit destination account
            UPDATE accounts 
            SET balance = balance + amount 
            WHERE account_id = to_account;
            
            -- Success - exit retry loop
            RETURN TRUE;
            
        EXCEPTION
            WHEN serialization_failure THEN
                retry_count := retry_count + 1;
                IF retry_count >= max_retries THEN
                    RAISE EXCEPTION 'Transaction failed after % retries', max_retries;
                END IF;
                -- Brief delay before retry
                PERFORM pg_sleep(0.1 * retry_count);
        END;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

5. Optimistic Concurrency Control

Optimistic locking assumes conflicts are rare and detects them at update time rather than preventing them.

-- Version-based optimistic locking
CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    content TEXT,
    version INTEGER NOT NULL DEFAULT 1,
    updated_at TIMESTAMP DEFAULT NOW()
);

-- Update function with version check
CREATE OR REPLACE FUNCTION update_document(
    doc_id INTEGER,
    new_content TEXT,
    expected_version INTEGER
) RETURNS BOOLEAN AS $$
DECLARE
    rows_affected INTEGER;
BEGIN
    UPDATE documents 
    SET content = new_content,
        version = version + 1,
        updated_at = NOW()
    WHERE id = doc_id 
    AND version = expected_version;
    
    GET DIAGNOSTICS rows_affected = ROW_COUNT;
    
    IF rows_affected = 0 THEN
        -- Version mismatch - document was modified
        RETURN FALSE;
    END IF;
    
    RETURN TRUE;
END;
$$ LANGUAGE plpgsql;

Using System Columns for Optimistic Locking:

-- Using xmin system column as version indicator
UPDATE user_preferences 
SET theme = $2, 
    updated_at = NOW()
WHERE user_id = $1 
AND xmin::text = $3;  -- $3 contains the xmin value from initial SELECT

Advanced Patterns and Best Practices

Minimizing Lock Contention

-- Bad: Long-running transaction holding locks
BEGIN;
SELECT * FROM hot_table WHERE id = 123 FOR UPDATE;
-- ... extensive processing ...
UPDATE hot_table SET value = new_value WHERE id = 123;
COMMIT;

-- Better: Minimize critical section
-- Process data outside transaction
local_data := process_data(input);

BEGIN;
SELECT * FROM hot_table WHERE id = 123 FOR UPDATE;
UPDATE hot_table SET value = local_data WHERE id = 123;
COMMIT;

Batch Processing with Proper Locking

-- Process multiple items with proper locking
WITH locked_items AS (
    SELECT id, data
    FROM work_queue 
    WHERE status = 'pending'
    ORDER BY priority DESC
    FOR UPDATE SKIP LOCKED
    LIMIT 100
)
UPDATE work_queue 
SET status = 'processing',
    started_at = NOW()
FROM locked_items 
WHERE work_queue.id = locked_items.id
RETURNING work_queue.id, work_queue.data;

Handling Deadlocks Gracefully

-- Deadlock-aware transaction wrapper
CREATE OR REPLACE FUNCTION execute_with_deadlock_retry(
    operation TEXT,
    max_retries INTEGER DEFAULT 3
) RETURNS VOID AS $$
DECLARE
    retry_count INTEGER := 0;
BEGIN
    LOOP
        BEGIN
            EXECUTE operation;
            EXIT; -- Success
        EXCEPTION
            WHEN deadlock_detected THEN
                retry_count := retry_count + 1;
                IF retry_count >= max_retries THEN
                    RAISE EXCEPTION 'Deadlock persists after % retries', max_retries;
                END IF;
                -- Randomized exponential backoff
                PERFORM pg_sleep(0.1 * power(2, retry_count) * random());
        END;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

Performance Considerations and Monitoring

Lock Monitoring Queries

-- Monitor active locks
SELECT 
    l.locktype,
    l.database,
    l.relation::regclass,
    l.page,
    l.tuple,
    l.transactionid,
    l.mode,
    l.granted,
    a.query,
    a.query_start,
    a.state
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE NOT l.granted
ORDER BY l.granted, a.query_start;

-- Identify blocking relationships
SELECT 
    blocked_locks.pid AS blocked_pid,
    blocked_activity.usename AS blocked_user,
    blocking_locks.pid AS blocking_pid,
    blocking_activity.usename AS blocking_user,
    blocked_activity.query AS blocked_statement,
    blocking_activity.query AS blocking_statement
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_locks.pid = blocked_activity.pid
JOIN pg_catalog.pg_locks blocking_locks ON (
    blocked_locks.locktype = blocking_locks.locktype
    AND blocked_locks.DATABASE IS NOT DISTINCT FROM blocking_locks.DATABASE
    AND blocked_locks.relation IS NOT DISTINCT FROM blocking_locks.relation
    AND blocked_locks.page IS NOT DISTINCT FROM blocking_locks.page
    AND blocked_locks.tuple IS NOT DISTINCT FROM blocking_locks.tuple
    AND blocked_locks.transactionid IS NOT DISTINCT FROM blocking_locks.transactionid
)
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_locks.pid = blocking_activity.pid
WHERE NOT blocked_locks.granted;

Summary and Decision Matrix

StrategyUse CaseProsConsPerformance Impact
SELECT … FOR UPDATEPreventing concurrent updates to critical dataStrong consistency, simple to implementCan cause blocking, potential deadlocksMedium – holds locks longer
INSERT … ON CONFLICTAtomic insert-or-update operationsEliminates race conditions, atomicLimited to single-table operationsLow – single atomic operation
Advisory LocksApplication-level coordinationFlexible, custom logicRequires careful managementLow – lightweight
SERIALIZABLE IsolationMaximum consistency requirementsStrongest guaranteesRequires retry logic, can be expensiveHigh – may cause frequent retries
Optimistic LockingLow contention scenariosNon-blocking, good for read-heavy workloadsRequires application retry logicVery Low – no locks held

The choice of strategy depends on your specific use case, performance requirements, and consistency needs. Often, a combination of approaches works best, using different strategies for different parts of your application based on their concurrency patterns and requirements.


Example Handling of Race Condition in Bank Account System

The bank account system example demonstrates a real-world scenario where multiple race conditions can occur simultaneously. Let’s dissect each component and understand how it addresses specific concurrency challenges.

System Architecture Overview

The system consists of two main tables and several stored procedures designed to handle concurrent financial operations safely:

Core Tables Structure

-- Account table with proper constraints
CREATE TABLE accounts (
    account_id SERIAL PRIMARY KEY,
    customer_id INTEGER NOT NULL,
    balance DECIMAL(15,2) NOT NULL CHECK (balance >= 0),
    version INTEGER NOT NULL DEFAULT 1,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

-- Transaction log for audit trail
CREATE TABLE account_transactions (
    transaction_id SERIAL PRIMARY KEY,
    from_account INTEGER,
    to_account INTEGER,
    amount DECIMAL(15,2) NOT NULL,
    transaction_type VARCHAR(20) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT NOW(),
    completed_at TIMESTAMP
);

Key Design Decisions Explained

1. Balance Constraint: CHECK (balance >= 0)

  • Purpose: Prevents negative balances at the database level
  • Race Condition Prevention: Even if application logic fails, the database ensures data integrity
  • Impact: Failed transactions will roll back rather than create invalid states

2. Version Column: version INTEGER NOT NULL DEFAULT 1

  • Purpose: Enables optimistic locking for scenarios where pessimistic locking isn’t suitable
  • Usage: Incremented on every update, allows detection of concurrent modifications
  • Benefit: Provides a lightweight alternative to row locking for read-heavy scenarios

3. Audit Trail: account_transactions Table

  • Purpose: Maintains complete history of all financial operations
  • Concurrency Benefit: Allows transaction status tracking without locking account records
  • Compliance: Essential for financial systems requiring audit trails

Race Condition Scenarios Addressed

Scenario 1: Concurrent Withdrawals

The Problem:

Time | Transaction A        | Transaction B
-----|---------------------|--------------------
T1   | READ balance: $100  | READ balance: $100
T2   | Withdraw $80        | Withdraw $60
T3   | UPDATE balance: $20 | UPDATE balance: $40

Result: Final balance is $40, but $140 was withdrawn from $100!

How Our System Prevents This:

-- In withdraw_funds function
SELECT balance INTO current_balance
FROM accounts 
WHERE account_id = p_account_id 
FOR UPDATE;  -- ← This prevents the race condition

The FOR UPDATE clause ensures that only one transaction can read and modify the balance at a time. Transaction B will wait until Transaction A completes.

Scenario 2: Double-Spending in Transfers

The Problem: Two concurrent transfers from the same account could both read the same balance and both succeed, even if the combined amount exceeds the available balance.

How Our System Prevents This:

-- Lock accounts in consistent order to prevent deadlocks
IF p_from_account < p_to_account THEN
    SELECT balance INTO from_balance
    FROM accounts 
    WHERE account_id = p_from_account 
    FOR UPDATE;
    
    PERFORM 1 FROM accounts 
    WHERE account_id = p_to_account 
    FOR UPDATE;
ELSE
    PERFORM 1 FROM accounts 
    WHERE account_id = p_to_account 
    FOR UPDATE;
    
    SELECT balance INTO from_balance
    FROM accounts 
    WHERE account_id = p_from_account 
    FOR UPDATE;
END IF;

Key Insights:

  • Consistent Lock Ordering: Always lock accounts in the same order (by ID) to prevent deadlocks
  • Both Accounts Locked: Prevents interference from other transactions affecting either account
  • Atomic Operation: The entire transfer happens as one atomic unit

Detailed Function Analysis

1. withdraw_funds Function

CREATE OR REPLACE FUNCTION withdraw_funds(
    p_account_id INTEGER,
    p_amount DECIMAL,
    p_transaction_id INTEGER
) RETURNS BOOLEAN AS $$
DECLARE
    current_balance DECIMAL;
    transaction_completed BOOLEAN := FALSE;
BEGIN
    -- Start with appropriate isolation level
    SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
    
    BEGIN
        -- Lock the account row
        SELECT balance INTO current_balance
        FROM accounts 
        WHERE account_id = p_account_id 
        FOR UPDATE;
        
        -- Validate sufficient funds
        IF current_balance < p_amount THEN
            UPDATE account_transactions 
            SET status = 'failed', 
                completed_at = NOW()
            WHERE transaction_id = p_transaction_id;
            RETURN FALSE;
        END IF;
        
        -- Perform withdrawal
        UPDATE accounts 
        SET balance = balance - p_amount,
            version = version + 1,
            updated_at = NOW()
        WHERE account_id = p_account_id;
        
        -- Update transaction log
        UPDATE account_transactions 
        SET status = 'completed',
            completed_at = NOW()
        WHERE transaction_id = p_transaction_id;
        
        transaction_completed := TRUE;
        
    EXCEPTION
        WHEN OTHERS THEN
            -- Mark transaction as failed
            UPDATE account_transactions 
            SET status = 'failed',
                completed_at = NOW()
            WHERE transaction_id = p_transaction_id;
            RAISE;
    END;
    
    RETURN transaction_completed;
END;
$$ LANGUAGE plpgsql;

Step-by-Step Breakdown:

Step 1: Isolation Level

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
  • Why READ COMMITTED: Balances consistency with performance
  • Alternative: Could use REPEATABLE READ or SERIALIZABLE for stricter consistency
  • Trade-off: Higher isolation levels reduce concurrency but increase consistency

Step 2: Pessimistic Locking

SELECT balance INTO current_balance
FROM accounts 
WHERE account_id = p_account_id 
FOR UPDATE;
  • Critical Section: This creates a critical section around the account
  • Blocking Behavior: Other transactions attempting to modify this account will wait
  • Lock Duration: Lock held until transaction commits or rolls back

Step 3: Business Logic Validation

IF current_balance < p_amount THEN
    UPDATE account_transactions 
    SET status = 'failed', 
        completed_at = NOW()
    WHERE transaction_id = p_transaction_id;
    RETURN FALSE;
END IF;
  • Validation Within Lock: Ensures no other transaction can change balance during validation
  • Audit Trail: Failed attempts are recorded for compliance and debugging
  • Early Exit: Prevents unnecessary processing if operation cannot succeed

Step 4: Atomic Update

UPDATE accounts 
SET balance = balance - p_amount,
    version = version + 1,
    updated_at = NOW()
WHERE account_id = p_account_id;
  • Atomic Operation: Balance update and version increment happen together
  • Version Tracking: Enables detection of concurrent modifications
  • Timestamp Update: Maintains audit trail of when changes occurred

2. transfer_funds Function Deep Dive

The transfer function is more complex because it involves multiple accounts and potential deadlock scenarios:

-- Lock accounts in consistent order to prevent deadlocks
IF p_from_account < p_to_account THEN
    SELECT balance INTO from_balance
    FROM accounts 
    WHERE account_id = p_from_account 
    FOR UPDATE;
    
    PERFORM 1 FROM accounts 
    WHERE account_id = p_to_account 
    FOR UPDATE;
ELSE
    PERFORM 1 FROM accounts 
    WHERE account_id = p_to_account 
    FOR UPDATE;
    
    SELECT balance INTO from_balance
    FROM accounts 
    WHERE account_id = p_from_account 
    FOR UPDATE;
END IF;

Deadlock Prevention Strategy

The Deadlock Scenario:

Time | Transaction A (Transfer 1→2) | Transaction B (Transfer 2→1)
-----|------------------------------|------------------------------
T1   | Lock Account 1               | Lock Account 2
T2   | Try to lock Account 2 (WAIT) | Try to lock Account 1 (WAIT)
T3   | ← DEADLOCK DETECTED →        | ← DEADLOCK DETECTED →

Our Solution:

  • Consistent Ordering: Always lock accounts in ascending ID order
  • Global Rule: All transfers follow the same locking order
  • Result: Impossible to create circular wait conditions

Retry Logic for Resilience

-- Retry loop for handling serialization failures
LOOP
    BEGIN
        -- ... main transaction logic ...
        EXIT; -- Success
        
    EXCEPTION
        WHEN serialization_failure OR deadlock_detected THEN
            retry_count := retry_count + 1;
            IF retry_count >= max_retries THEN
                UPDATE account_transactions 
                SET status = 'failed', completed_at = NOW()
                WHERE transaction_id = transaction_id;
                RAISE EXCEPTION 'Transfer failed after % retries', max_retries;
            END IF;
            -- Exponential backoff with jitter
            PERFORM pg_sleep(0.1 * power(2, retry_count) * (0.5 + random() * 0.5));
    END;
END LOOP;

Retry Strategy Components:

1. Exception Handling

  • Specific Exceptions: Only retries on serialization failures and deadlocks
  • Other Errors: Business logic errors (insufficient funds) are not retried
  • Fail-Safe: After max retries, transaction is marked as failed

2. Exponential Backoff

  • Base Delay: 0.1 seconds
  • Exponential Growth: Doubles with each retry
  • Jitter: Random factor (0.5-1.0) prevents thundering herd

3. Audit Trail Maintenance

  • Failed Transactions: Recorded even after retries fail
  • Status Tracking: Complete lifecycle of each transaction attempt

Advanced Concurrency Patterns Demonstrated

1. Two-Phase Locking Protocol

The system implements a variant of two-phase locking:

-- Growing Phase: Acquire all locks
SELECT balance FROM accounts WHERE account_id = p_from_account FOR UPDATE;
PERFORM 1 FROM accounts WHERE account_id = p_to_account FOR UPDATE;

-- Working Phase: Perform operations
UPDATE accounts SET balance = balance - p_amount WHERE account_id = p_from_account;
UPDATE accounts SET balance = balance + p_amount WHERE account_id = p_to_account;

-- Shrinking Phase: Release locks (automatic on COMMIT)
COMMIT;

2. Optimistic Concurrency Control Alternative

For read-heavy scenarios, you could implement optimistic locking:

-- Alternative withdrawal using optimistic locking
CREATE OR REPLACE FUNCTION withdraw_funds_optimistic(
    p_account_id INTEGER,
    p_amount DECIMAL,
    p_expected_version INTEGER
) RETURNS BOOLEAN AS $$
DECLARE
    rows_affected INTEGER;
BEGIN
    -- Attempt optimistic update
    UPDATE accounts 
    SET balance = balance - p_amount,
        version = version + 1,
        updated_at = NOW()
    WHERE account_id = p_account_id 
    AND version = p_expected_version
    AND balance >= p_amount;
    
    GET DIAGNOSTICS rows_affected = ROW_COUNT;
    
    IF rows_affected = 0 THEN
        -- Either version mismatch or insufficient funds
        RETURN FALSE;
    END IF;
    
    RETURN TRUE;
END;
$$ LANGUAGE plpgsql;

Usage Pattern:

-- Client-side retry logic
DO $$
DECLARE
    current_version INTEGER;
    current_balance DECIMAL;
    success BOOLEAN := FALSE;
    max_retries INTEGER := 5;
    retry_count INTEGER := 0;
BEGIN
    WHILE NOT success AND retry_count < max_retries LOOP
        -- Read current state
        SELECT balance, version INTO current_balance, current_version
        FROM accounts WHERE account_id = 123;
        
        -- Validate business rules
        IF current_balance < 50.00 THEN
            RAISE EXCEPTION 'Insufficient funds';
        END IF;
        
        -- Attempt optimistic update
        success := withdraw_funds_optimistic(123, 50.00, current_version);
        
        IF NOT success THEN
            retry_count := retry_count + 1;
            PERFORM pg_sleep(0.01); -- Brief pause
        END IF;
    END LOOP;
    
    IF NOT success THEN
        RAISE EXCEPTION 'Withdrawal failed after % retries', max_retries;
    END IF;
END $$;

Performance Implications and Monitoring

1. Lock Contention Analysis

High-Contention Scenario:

-- Many concurrent operations on popular accounts
SELECT account_id, COUNT(*) as operation_count
FROM account_transactions 
WHERE created_at > NOW() - INTERVAL '1 hour'
GROUP BY account_id
ORDER BY operation_count DESC;

Mitigation Strategies:

  • Account Sharding: Distribute load across multiple account records
  • Batch Processing: Group multiple operations together
  • Read Replicas: Use read-only replicas for balance inquiries

2. Deadlock Monitoring

-- Monitor deadlock frequency
SELECT 
    datname,
    deadlocks,
    deadlocks / (blks_read + blks_hit) * 100 as deadlock_percentage
FROM pg_stat_database 
WHERE datname = current_database();

3. Transaction Duration Analysis

-- Identify long-running transactions
SELECT 
    pid,
    now() - pg_stat_activity.query_start AS duration,
    query,
    state
FROM pg_stat_activity
WHERE (now() - pg_stat_activity.query_start) > interval '5 minutes'
AND state != 'idle';

Real-World Adaptations

1. Microservices Architecture

In a microservices environment, you might need distributed locking:

-- Using advisory locks for distributed coordination
CREATE OR REPLACE FUNCTION distributed_transfer(
    p_from_account INTEGER,
    p_to_account INTEGER,
    p_amount DECIMAL,
    p_service_instance_id TEXT
) RETURNS INTEGER AS $$
DECLARE
    lock_key BIGINT;
    transaction_id INTEGER;
BEGIN
    -- Create distributed lock key
    lock_key := hashtext(p_service_instance_id || '_' || 
                        LEAST(p_from_account, p_to_account) || '_' || 
                        GREATEST(p_from_account, p_to_account));
    
    -- Acquire distributed lock
    IF NOT pg_try_advisory_xact_lock(lock_key) THEN
        RAISE EXCEPTION 'Could not acquire distributed lock';
    END IF;
    
    -- Proceed with transfer logic
    SELECT transfer_funds(p_from_account, p_to_account, p_amount) 
    INTO transaction_id;
    
    RETURN transaction_id;
END;
$$ LANGUAGE plpgsql;

2. High-Frequency Trading Adaptations

For extremely high-frequency scenarios:

-- Optimized for minimal lock time
CREATE OR REPLACE FUNCTION hft_transfer(
    p_from_account INTEGER,
    p_to_account INTEGER,
    p_amount DECIMAL
) RETURNS BOOLEAN AS $$
BEGIN
    -- Pre-validate outside of critical section
    IF NOT EXISTS (
        SELECT 1 FROM accounts 
        WHERE account_id = p_from_account 
        AND balance >= p_amount
    ) THEN
        RETURN FALSE;
    END IF;
    
    -- Minimal critical section
    BEGIN
        WITH account_updates AS (
            SELECT account_id, 
                   CASE WHEN account_id = p_from_account 
                        THEN balance - p_amount 
                        ELSE balance + p_amount 
                   END as new_balance
            FROM accounts 
            WHERE account_id IN (p_from_account, p_to_account)
            FOR UPDATE
        )
        UPDATE accounts 
        SET balance = au.new_balance,
            version = version + 1
        FROM account_updates au
        WHERE accounts.account_id = au.account_id
        AND au.new_balance >= 0;
        
        RETURN FOUND;
    EXCEPTION
        WHEN OTHERS THEN
            RETURN FALSE;
    END;
END;
$$ LANGUAGE plpgsql;

Testing Race Conditions

1. Concurrent Load Testing

-- Test script to simulate concurrent operations
DO $$
DECLARE
    i INTEGER;
BEGIN
    -- Create test accounts
    FOR i IN 1..100 LOOP
        INSERT INTO accounts (customer_id, balance)
        VALUES (i, 1000.00);
    END LOOP;
    
    -- Simulate concurrent transfers
    FOR i IN 1..1000 LOOP
        PERFORM pg_background_launch(
            'SELECT transfer_funds($1, $2, $3)',
            (random() * 99 + 1)::INTEGER,
            (random() * 99 + 1)::INTEGER,
            (random() * 100 + 1)::DECIMAL
        );
    END LOOP;
END $$;

2. Consistency Verification

-- Verify system consistency
WITH transaction_summary AS (
    SELECT 
        SUM(CASE WHEN status = 'completed' THEN amount ELSE 0 END) as total_transferred,
        COUNT(CASE WHEN status = 'completed' THEN 1 END) as successful_transfers,
        COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed_transfers
    FROM account_transactions 
    WHERE transaction_type = 'transfer'
),
account_summary AS (
    SELECT SUM(balance) as total_balance
    FROM accounts
)
SELECT 
    ts.total_transferred,
    ts.successful_transfers,
    ts.failed_transfers,
    ac.total_balance,
    -- Total balance should remain constant in a closed system
    1000.00 * (SELECT COUNT(*) FROM accounts) as expected_total
FROM transaction_summary ts
CROSS JOIN account_summary ac;

This comprehensive bank account system demonstrates how to handle multiple types of race conditions in a real-world scenario, providing both safety and performance through careful application of PostgreSQL’s concurrency control mechanisms.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>