并发控制实践
核心问题:如何处理热点行?如何实现乐观锁?如何防止重复下单?
1. 热点行处理
sql
-- 使用 SELECT FOR UPDATE
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;2. 乐观锁
sql
-- 使用版本号
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT,
stock INT DEFAULT 100,
version INT DEFAULT 0
);
-- 扣减库存(乐观锁)
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND stock > 0 AND version = 5;
-- 检查影响行数,0 表示冲突需要重试3. 防止重复下单
sql
-- 方案1:唯一约束(最简单)
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
idempotency_key VARCHAR(64) UNIQUE, -- 幂等键
amount DECIMAL(10,2),
created_at TIMESTAMP DEFAULT NOW()
);
INSERT INTO orders (user_id, product_id, idempotency_key, amount)
VALUES (1, 100, 'order-20240615-001', 99.9)
ON CONFLICT (idempotency_key) DO NOTHING;
-- 方案2:咨询锁
BEGIN;
SELECT pg_advisory_xact_lock(user_id); -- 用户级别互斥
INSERT INTO orders (user_id, product_id, amount) VALUES (1, 100, 99.9);
COMMIT;4. 批量更新
sql
-- 使用 CTE 批量更新
WITH batch AS (
SELECT id FROM orders WHERE status = 'pending' LIMIT 1000
)
UPDATE orders SET status = 'processing'
WHERE id IN (SELECT id FROM batch);5. 分布式 ID 生成
sql
-- 方案1:序列(Sequence)
CREATE SEQUENCE order_id_seq;
SELECT nextval('order_id_seq');
-- 方案2:UUID
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
INSERT INTO orders (id, user_id) VALUES (uuid_generate_v4(), 1);6. 并发安全的计数器
sql
-- ✅ UPDATE 本身是原子操作,不会丢失更新
UPDATE counters SET value = value + 1 WHERE name = 'page_views'
RETURNING value;
-- ✅ 高并发场景:使用 CTE 批量计数
WITH new_counts AS (
SELECT name, count(*) AS cnt
FROM page_view_logs
WHERE created_at > (SELECT MAX(last_processed) FROM counter_sync)
GROUP BY name
)
UPDATE counters c
SET value = c.value + nc.cnt
FROM new_counts nc
WHERE c.name = nc.name;7. 读写分离
sql
-- 查看复制延迟
SELECT
client_addr,
state,
sent_lsn,
replay_lsn,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes
FROM pg_stat_replication;应用层配置(Spring Boot):
yaml
spring:
datasource:
write:
url: jdbc:postgresql://master:5432/mydb
read:
url: jdbc:postgresql://slave:5432/mydb