事务操作中的限制和潜在问题:LOCK,DEADLOCK
PostgreSQL里的事务保证了隔离性——这是ACID的一个关键特性。为此,系统会用到锁。锁就是一种机制,保证多个事务不会“抢”同一条记录或表。你可以想象超市排队结账:收银台一次只服务一个人。锁的作用差不多,就是用来管理表里数据的访问。
锁的主要类型
PostgreSQL有几种锁类型,有些你可能已经在EXPLAIN ANALYZE里见过:
ROW EXCLUSIVE(行级排他锁)——当你修改某行数据时会用到。这是最常见的锁类型,比如执行INSERT、UPDATE或DELETE时。SHARE(共享锁)——用于保证在查询执行期间数据不会被改动,比如SELECT ... FOR SHARE。EXCLUSIVE(排他锁)——阻止除了读以外的所有其他操作。
可以说,锁就像看门狗一样,守护着我们的数据。
锁的问题:可能会出啥岔子?
现在我们知道锁是啥了,来看看它们可能带来的问题。最大的问题就是死锁。死锁(deadlock)就是两个(或更多)事务互相等待对方释放资源,结果大家都卡住了,进了死循环。一般是这样:
- 事务1锁住了行A,想要访问行B。
- 事务2锁住了行B,想要访问行A。
- 因为两行都被锁住了,两个事务都没法完成。
死锁的例子
想象一下程序员遇到死锁的痛苦,场景如下:
-- 事务1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 同时
-- 事务2
BEGIN;
UPDATE accounts SET balance = balance + 200 WHERE id = 2;
-- 事务1试图锁id = 2
UPDATE accounts SET balance = balance - 100 WHERE id = 2;
-- 事务2试图锁id = 1
UPDATE accounts SET balance = balance + 200 WHERE id = 1;
瞧!每个事务都被另一个锁住了。PostgreSQL迟早会发现这个问题,然后抛出死锁错误。
怎么避免死锁?
- 数据访问顺序要一致。 尽量总是按同样的顺序访问数据。如果事务1先锁A再锁B,那事务2也应该先锁A再锁B。
-- 正确的数据访问顺序
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 200 WHERE id = 2;
COMMIT;
尽量缩短事务持续时间。 事务时间越长,死锁的概率越高。尽快完成事务:
BEGIN,做完所有需要的更改,然后立刻COMMIT。用合适的事务隔离级别。 如果你的需求不需要很严格的数据隔离,可以考虑用
READ COMMITTED隔离级别。这样能避免一些锁。
锁的超时和诊断
PostgreSQL有工具可以诊断和预防锁问题。比如,你可以设置锁的最大等待时间:
SET lock_timeout = '5s'; -- 把锁超时设置为5秒
如果锁被持有超过这个时间,事务就会被中断,这样就不会彻底卡死了。
PostgreSQL里怎么监控锁
一个很有用的监控锁的命令是pg_locks。它能显示系统里所有的活动锁:
SELECT * FROM pg_locks;
你可以看到哪些事务持有锁,哪些在等锁。调试死锁的时候特别有用。
LOCK的特点和手动锁定对象
如果你需要手动管理锁,可以用LOCK命令:
LOCK TABLE orders IN ACCESS EXCLUSIVE MODE;
注意:ACCESS EXCLUSIVE是最强的锁,会禁止对表的所有其他操作,连SELECT都不行。只有在特殊情况下(比如改表结构)才用它。
如果你只想锁定某几行,可以用SELECT ... FOR UPDATE:
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
这样可以保证在你的事务结束前,其他事务不能改这些行。
关于锁你要记住啥?
锁不是坏东西,而是工具。它们帮你保证数据完整性,但用的时候要小心。给你几个建议:
- 没想好为啥要用事务就别开事务。
- 事务要尽快结束。
- 避免用不同的顺序访问数据。
- 用适合你需求的隔离级别。
现在你已经准备好和锁、死锁打交道啦!祝你用pg_locks顺利,ACID智慧常伴你身边。
GO TO FULL VERSION