Skip to content

PostgreSQL 锁定和索引

概述

在高并发的数据库系统中,锁定机制是确保数据一致性和并发访问的关键。PostgreSQL 虽然提供对表数据的非阻塞读/写访问,但不同的索引访问方法在并发控制方面有着不同的实现策略。理解各种索引类型的锁定机制对于优化数据库性能和避免并发问题至关重要。

索引类型与锁定机制对比

锁定机制概览

详细对比表格

索引类型锁定级别锁定时长并发性死锁风险适用场景
B-tree页面级极短期最高标量数据,高并发
GiST/SP-GiST页面级极短期非标量数据,几何数据
哈希索引桶级中等中等等值查询,低并发
GIN页面级极短期全文搜索,数组数据

B-tree、GiST 和 SP-GiST 索引

工作原理

B-tree、GiST 和 SP-GiST 索引使用短期的共享/独占页面级锁进行读/写访问。这种机制的特点是:

  • 锁定粒度:页面级别
  • 锁定时长:在获取或插入索引行后立即释放
  • 并发性:提供最高的并发性
  • 死锁风险:无死锁情况

实际应用示例

场景:电商订单系统

假设我们有一个电商订单表,需要处理大量并发的查询和插入操作:

sql
-- 创建订单表
CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    customer_id INTEGER NOT NULL,
    order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    total_amount DECIMAL(10,2),
    status VARCHAR(20)
);

-- 创建B-tree索引
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
CREATE INDEX idx_orders_date ON orders (order_date);
CREATE INDEX idx_orders_status ON orders (status);

并发操作演示

sql
-- 查询客户订单(读取操作)
BEGIN;
SELECT * FROM orders
WHERE customer_id = 12345
ORDER BY order_date DESC;
-- B-tree索引页面锁立即释放,不阻塞其他操作
COMMIT;
sql
-- 同时插入新订单(写入操作)
BEGIN;
INSERT INTO orders (customer_id, total_amount, status)
VALUES (12345, 299.99, 'pending');
-- 索引更新完成后,页面锁立即释放
COMMIT;
sql
-- 同时更新订单状态
BEGIN;
UPDATE orders
SET status = 'completed'
WHERE order_id = 98765;
-- 索引页面锁在更新完成后立即释放
COMMIT;

TIP

在上述并发场景中,三个会话可以同时执行而不会相互阻塞,因为 B-tree 索引的页面级锁会在每次操作完成后立即释放。

性能分析

锁定时间测试

sql
-- 创建测试表和索引
CREATE TABLE test_btree (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_test_btree_name ON test_btree (name);

-- 插入测试数据
INSERT INTO test_btree (name)
SELECT 'user_' || generate_series(1, 100000);

-- 分析索引使用情况
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM test_btree
WHERE name = 'user_50000';

输出结果分析:

Index Scan using idx_test_btree_name on test_btree
  (cost=0.29..8.31 rows=1 width=25)
  (actual time=0.045..0.046 rows=1 loops=1)
  Index Cond: (name = 'user_50000'::text)
  Buffers: shared hit=4
Planning Time: 0.123 ms
Execution Time: 0.078 ms

分析过程:

  • Buffers: shared hit=4:只访问了 4 个缓存页面
  • Execution Time: 0.078 ms:执行时间极短
  • 页面级锁在访问每个页面后立即释放,整个查询过程锁定时间不超过几微秒

哈希索引

工作原理

哈希索引使用共享/独占哈希桶级锁进行读/写访问,其特点是:

  • 锁定粒度:桶级别(比页面级更粗粒度)
  • 锁定时长:处理完整个桶后才释放
  • 并发性:比索引级锁好,但不如页面级锁
  • 死锁风险:由于锁持有时间较长,可能出现死锁

实际应用示例

场景:用户会话管理

sql
-- 创建会话表
CREATE TABLE user_sessions (
    session_id VARCHAR(64) PRIMARY KEY,
    user_id INTEGER NOT NULL,
    login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    ip_address INET
);

-- 创建哈希索引(适合等值查询)
CREATE INDEX idx_sessions_hash ON user_sessions USING HASH (session_id);

并发场景与潜在问题

sql
-- 场景:高并发会话验证
-- 会话1:验证用户A的会话
SELECT user_id FROM user_sessions
WHERE session_id = 'abc123def456ghi789';

-- 会话2:同时验证用户B的会话(可能在同一哈希桶)
SELECT user_id FROM user_sessions
WHERE session_id = 'xyz789abc123def456';

-- 会话3:删除过期会话(可能导致死锁)
DELETE FROM user_sessions
WHERE last_activity < CURRENT_TIMESTAMP - INTERVAL '24 hours';

死锁风险演示

WARNING

哈希索引的桶级锁可能导致死锁,特别是在以下情况:

  • 大批量数据操作
  • 事务中访问多个哈希桶
  • 高并发环境下的复杂查询

性能对比测试

sql
-- 创建对比测试
CREATE TABLE hash_test (
    id SERIAL,
    lookup_key VARCHAR(64) UNIQUE,
    data TEXT
);

-- 插入100万测试数据
INSERT INTO hash_test (lookup_key, data)
SELECT
    md5(random()::text),
    'data_' || generate_series(1, 1000000);

-- B-tree索引(默认)
CREATE INDEX idx_hash_test_btree ON hash_test (lookup_key);

-- 哈希索引
CREATE INDEX idx_hash_test_hash ON hash_test USING HASH (lookup_key);

-- 性能测试:等值查询
EXPLAIN (ANALYZE, BUFFERS, TIMING)
SELECT * FROM hash_test
WHERE lookup_key = '5d41402abc4b2a76b9719d911017c592';

GIN 索引

工作原理

GIN(Generalized Inverted Index)索引使用短期的共享/独占页面级锁,但有其特殊性:

  • 锁定粒度:页面级别
  • 锁定时长:获取或插入索引行后立即释放
  • 特殊性:插入值通常为每行生成多个索引键
  • 工作量:单个值的插入可能需要执行大量工作

实际应用示例

场景:全文搜索系统

sql
-- 创建文档表
CREATE TABLE documents (
    doc_id SERIAL PRIMARY KEY,
    title TEXT,
    content TEXT,
    tags TEXT[],
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 创建GIN索引用于全文搜索
CREATE INDEX idx_documents_fts ON documents
USING GIN(to_tsvector('english', title || ' ' || content));

-- 创建GIN索引用于数组搜索
CREATE INDEX idx_documents_tags ON documents
USING GIN(tags);

多键插入演示

sql
-- 插入一篇文档
INSERT INTO documents (title, content, tags) VALUES (
    'PostgreSQL Performance Tuning',
    'This article discusses various techniques for optimizing PostgreSQL database performance including indexing strategies and query optimization.',
    ARRAY['postgresql', 'performance', 'database', 'optimization', 'indexing']
);

分析过程:

  1. 全文搜索索引更新

    • 文本被分词为:postgresql, performance, tuning, article, discusses, various, techniques, optimizing, database, including, indexing, strategies, query, optimization
    • 每个词都需要在 GIN 索引中创建或更新条目
    • 总共可能涉及 10-15 个索引键的操作
  2. 数组索引更新

    • 数组包含 5 个标签
    • 每个标签都需要在 GIN 索引中创建条目
    • 涉及 5 个索引键的操作

查询性能测试

sql
-- 全文搜索查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT doc_id, title
FROM documents
WHERE to_tsvector('english', title || ' ' || content)
      @@ to_tsquery('english', 'postgresql & performance');

-- 数组查询
EXPLAIN (ANALYZE, BUFFERS)
SELECT doc_id, title
FROM documents
WHERE tags @> ARRAY['postgresql', 'performance'];

输出结果示例:

Bitmap Heap Scan on documents
  (cost=20.25..24.27 rows=1 width=36)
  (actual time=0.156..0.157 rows=1 loops=1)
  Recheck Cond: (tags @> '{postgresql,performance}'::text[])
  Heap Blocks: exact=1
  ->  Bitmap Index Scan on idx_documents_tags
      (cost=0.00..20.25 rows=1 width=0)
      (actual time=0.152..0.152 rows=1 loops=1)
      Index Cond: (tags @> '{postgresql,performance}'::text[])
  Buffers: shared hit=4

INFO

GIN 索引虽然在插入时需要处理多个键值,但在查询复杂条件时表现出色,特别适合:

  • 全文搜索
  • 数组元素查询
  • JSON 数据查询
  • 复合条件检索

并发性能建议与最佳实践

索引选择决策树

性能优化策略

1. 高并发系统推荐配置

sql
-- 电商系统示例配置
-- 用户表
CREATE TABLE users (
    user_id SERIAL PRIMARY KEY,        -- 自动B-tree索引
    email VARCHAR(255) UNIQUE,         -- 自动B-tree索引
    username VARCHAR(50) UNIQUE,       -- 自动B-tree索引
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 订单表
CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,       -- 自动B-tree索引
    user_id INTEGER REFERENCES users(user_id),
    order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    status VARCHAR(20),
    total_amount DECIMAL(10,2)
);

-- 高并发场景的B-tree索引
CREATE INDEX idx_orders_user_id ON orders (user_id);           -- 关联查询
CREATE INDEX idx_orders_date ON orders (order_date);           -- 时间范围查询
CREATE INDEX idx_orders_status ON orders (status);             -- 状态筛选
CREATE INDEX idx_orders_composite ON orders (user_id, status); -- 复合查询

2. 避免哈希索引的场景

sql
-- ❌ 避免:在高并发系统中使用哈希索引
-- CREATE INDEX idx_orders_hash ON orders USING HASH (order_id);

-- ✅ 推荐:使用B-tree索引替代
CREATE INDEX idx_orders_btree ON orders (order_id);

3. GIN 索引优化策略

sql
-- 文档搜索系统优化
CREATE TABLE articles (
    article_id SERIAL PRIMARY KEY,
    title TEXT,
    content TEXT,
    tags TEXT[],
    category_id INTEGER,
    published_at TIMESTAMP
);

-- GIN索引配置
CREATE INDEX idx_articles_fts ON articles
USING GIN(to_tsvector('english', title || ' ' || coalesce(content, '')))
WITH (fastupdate = off);  -- 禁用快速更新以提高查询性能

-- 组合索引策略
CREATE INDEX idx_articles_category_published ON articles (category_id, published_at);
CREATE INDEX idx_articles_tags ON articles USING GIN(tags);

监控和诊断

锁等待监控

sql
-- 查看当前锁等待情况
SELECT
    pg_stat_activity.pid,
    pg_stat_activity.usename,
    pg_stat_activity.query,
    pg_blocking_pids(pg_stat_activity.pid) AS blocked_by,
    pg_stat_activity.wait_event_type,
    pg_stat_activity.wait_event
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pg_stat_activity.pid)) > 0;

索引使用统计

sql
-- 查看索引使用情况
SELECT
    schemaname,
    tablename,
    indexname,
    idx_scan,
    idx_tup_read,
    idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;

实际案例:电商系统优化

问题场景

某电商平台在促销期间遇到性能问题:

  • 订单查询响应缓慢
  • 库存更新出现死锁
  • 用户投诉页面卡顿

解决方案

sql
-- 1. 检查慢查询
SELECT
    query,
    calls,
    total_time,
    mean_time,
    rows
FROM pg_stat_statements
WHERE mean_time > 100  -- 超过100ms的查询
ORDER BY mean_time DESC;

-- 2. 检查锁冲突
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 current_statement_in_blocking_process
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity
  ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
  ON blocking_locks.locktype = blocked_locks.locktype
JOIN pg_catalog.pg_stat_activity blocking_activity
  ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;
sql
-- 3. 创建合适的B-tree索引
CREATE INDEX CONCURRENTLY idx_orders_user_status_date
ON orders (user_id, status, order_date)
WHERE status IN ('pending', 'processing');

-- 4. 优化库存表索引
CREATE INDEX CONCURRENTLY idx_inventory_product_warehouse
ON inventory (product_id, warehouse_id);

-- 5. 移除不必要的哈希索引
DROP INDEX IF EXISTS idx_sessions_hash;
CREATE INDEX CONCURRENTLY idx_sessions_btree
ON user_sessions (session_id);
sql
-- 6. 优化后的查询
-- 原查询(慢):
-- SELECT * FROM orders o
-- JOIN users u ON o.user_id = u.user_id
-- WHERE o.status = 'pending'
-- ORDER BY o.order_date DESC;

-- 优化后查询:
SELECT o.order_id, o.total_amount, u.username
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE o.status = 'pending'
  AND o.order_date >= CURRENT_DATE - INTERVAL '7 days'
ORDER BY o.order_date DESC
LIMIT 100;

优化结果

指标优化前优化后改善程度
平均查询时间850ms45ms94.7%
死锁次数/小时15 次0 次100%
并发用户数5002000300%
CPU 使用率85%35%58.8%

总结

关键要点

  1. B-tree 索引是首选:对于并发应用程序,B-tree 索引提供最佳性能和最高并发性
  2. 避免哈希索引死锁:在高并发环境中谨慎使用哈希索引,注意潜在的死锁风险
  3. GIN 索引权衡:虽然插入成本较高,但在复杂查询场景下表现出色
  4. 合理选择索引类型:根据数据类型和查询模式选择最适合的索引类型