多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

PostgreSQL事务核心机制:提交、回滚与保存点实战详解

PostgreSQL事务核心机制:提交、回滚与保存点实战详解 上周帮一个朋友排查线上问题场景特别常见订单表里已经写了扣库存记录但订单状态却显示创建失败。折腾了一下午最后发现根因就一句话——程序里开启事务的代码写错了分支扣库存那条SQL根本没有被包进同一个事务里。这种问题在企业项目里太典型了十次有八次绕不开 PostgreSQL 中事务的提交、回滚与保存点这三个操作。今天就把这块掰开揉碎讲清楚。适合刚上手PostgreSQL的开发和运维也适合整天写SQL但没仔细研究过事务细节的老手。文章不会扯虚的每一步都给命令、给现象、给原因看完你至少能回答三个灵魂拷问事务到底是怎么提交的回滚是不是真的把数据撤回去了保存点到底能救回多少操作1. 先搞清楚事务解决什么问题再谈提交和回滚1.1 一个典型的丢事务事故库存扣了订单没建很多刚接触事务的同学最开始都是被原子性这个词教育的但实际项目里踩坑的方式远比教科书精彩。我见过最经典的一个案例是这样的一个下单接口先更新库存表再插入订单表。// 错误示范两条SQL各自处于独立事务 jdbc.execute(UPDATE t_inventory SET stock stock - 1 WHERE sku_id 123); jdbc.execute(INSERT INTO t_order(user_id, sku_id, amount) VALUES (1, 123, 99.00));这段代码跑起来绝大多数时候是好的。但只要第二条SQL因为字段超长、非空约束、主键冲突之类的原因抛异常库存就已经被扣掉了订单却不存在。用户付了钱没订单后台对账怎么都对不上。为这种问题背锅的DBA和程序员我认识不止一个。更隐蔽的坑在事务边界上。Java里面用JDBC的时候很多人知道要setAutoCommit(false)但异常分支里忘了写rollback()直接return了。这时候连接没有提交也没有回滚被丢回连接池。下一次从连接池拿到这个连接的程序会继承上一次那个未提交的事务接着执行新的SQL然后commit。那会发生什么上一次的UPDATE和这一次的UPDATE被莫名其妙地合并到了一个事务里该回滚的没回滚不该提交的可能被提交了。等到线上出现脏数据、锁等待查半天才能翻出这种陈年旧账。事务最大的价值就是把一系列操作绑成一个整体要么全部成功要么全部失败。扣库存和建订单必须同生共死就像你去商场刷卡买手机POS机扣款成功但小票打印失败店员一定不会让你直接走人而是要把这笔交易作废重新走一遍完整流程。1.2 ACID里容易被忽略的两件事一致性与持久性教科书上讲事务四大特性ACID原子性、一致性、隔离性、持久性。前两个句子谁都背得出来但有两件事经常被忽视。第一一致性不是数据库单方面保证的。数据库能保证约束、主键、外键这些但余额不能为负库存不能超卖这类业务规则数据库如果没配CHECK约束就得靠事务里的逻辑来保证。你可以在一个事务里先判断后更新也可以让事务在A和B两个账户间转账时保证A减多少B就加多少。事务只是给你这个保证一致性的机会你得真的用对才行。第二持久性不等于每一次提交都立刻把数据刷进磁盘。PostgreSQL的持久性核心是WALWrite-Ahead Logging机制事务提交时必须先把commit日志记录写入WAL文件并落盘而数据页本身可以留在内存里稍后写。这样即使数据库突然崩溃重启后也能通过WAL把已提交的修改恢复出来。所以commit返回成功意味着这条事务已经安全了而不代表数据页已经写到磁盘物理文件里了。另外还有一个绕不开的参数叫synchronous_commit。默认值是on每次提交都要等WAL刷盘后才返回成功。如果业务对持久性要求没那么苛刻可以把它调成off提交响应会快一些代价是数据库主机掉电时可能丢失最近一小段已提交的事务。注意是可能丢失不是必然丢失。到底怎么选取决于你的业务容忍度没有绝对答案。1.3 PostgreSQL里一个事务的基本生命周期PostgreSQL里最基础的事务流程就是三句话BEGIN开启事务COMMIT提交ROLLBACK回滚。-- 转账场景甲扣100乙加100 BEGIN; UPDATE account SET balance balance - 100 WHERE id 1; UPDATE account SET balance balance 100 WHERE id 2; COMMIT;如果执行过程中发现第二笔更新有问题比如把乙的余额加错了可以在COMMIT之前直接放弃BEGIN; UPDATE account SET balance balance - 100 WHERE id 1; UPDATE account SET balance balance 100 WHERE id 2; -- 发现金额不对撤掉整个事务 ROLLBACK;ROLLBACK之后事务里所有操作都当作没发生过包括已经扣掉的甲账户100块。这一个又一个的独立过程在PostgreSQL内部是通过事务IDxid来标记的。数据库会给每个写事务分配一个递增的事务ID并记录它的状态是IN_PROGRESS、COMMITTED还是ABORTED。后面所有判断某行数据我能不能看见都依赖这些状态。有一点必须提醒在psql或者大部分客户端里默认每条独立的SQL语句是自动提交的也就是说你执行一条UPDATE马上就生效了根本不需要COMMIT。只有当你主动执行了BEGIN数据库才会进入显式事务块模式。进了这个模式之后如果你某条SQL执行出错这个事务会进入一个叫 Aborted 的异常状态后面你再执行任何SQL都会得到这样的报错ERROR: current transaction is aborted, commands ignored until end of transaction block此时唯一能做的就是ROLLBACK或者用COMMIT来结束这个失败的事务COMMIT在这种情况下等价于ROLLBACK。很多新手第一次遇到这个报错会懵其实意思很简单事务里已经出过错了数据库不让你继续在这个坏掉的事务里跑新语句必须彻底结束它再重来。2. 提交与回滚最常用的操作背后却藏着一堆细节2.1 begin、commit、rollback的几种写法别被花活坑了PostgreSQL很贴心地给了好几种等价写法。开启事务可以用BEGIN、BEGIN TRANSACTION、BEGIN WORK也可以用标准SQL的START TRANSACTION提交可以用COMMIT、COMMIT TRANSACTION、COMMIT WORK甚至END回滚可以用ROLLBACK、ROLLBACK TRANSACTION、ROLLBACK WORK还有一个冷门的ABORT。这些写法在功能上完全等价但不同项目里的代码风格可能不一样。如果你看到别的同事用了START TRANSACTION别惊讶它就是标准SQL的写法本质跟BEGIN一样。我个人的习惯是统一用最简短的BEGIN/COMMIT/ROLLBACK少敲几个字母脚本里也清爽。需要注意的是事务和事务之间不要嵌套写BEGIN如果你在事务里又执行了一次BEGINPostgreSQL会给出警告WARNING: there is already a transaction in progress然后忽略这条命令而不是自动帮你开启一个子事务。子事务是靠保存点实现的后面会详细讲。重要提醒代码里如果用了BEGIN一定要有配套的COMMIT或ROLLBACK。尤其是Java、Go这类服务端程序连接是从连接池里拿的如果不能保证每次都在finally里正确关闭事务轻则产生锁等待重则把上一个事务的错误状态带到下一个请求里。我见过太多线上事故追到源头就是某个分支忘了回滚。2.2 提交的时候数据库到底做了什么很多人以为COMMIT就是把数据保存成功但在PostgreSQL里一条COMMIT背后至少做了这么几件事当前事务持有的所有锁并不会马上释放吗不恰恰相反锁在事务结束时释放。事务修改过的数据页不一定落盘但事务的commit记录必须先写进WAL日志并确保刷到磁盘。事务状态会在系统目录pg_xact旧版本叫pg_clog中标记为committed。返回客户端一条COMMIT确认消息。这里的核心是先写日志后写数据。WAL里记录的是事务修改的日志描述而不是直接改数据文件。如果写完数据页还没来得及落盘就断电数据库重启时会重放WAL让数据恢复到一个一致状态如果commit日志已经刷盘那这个事务就算真正提交成功了。这套机制保证了持久性也让COMMIT速度相对稳定不像MySQL InnoDB那样要纠结redo log刷盘策略。顺带一提synchronous_commit调成off后commit不会等待WAL立刻刷盘而是交给后台进程批量刷。这时候你执行COMMIT会非常快但如果在WAL刷盘前发生操作系统崩溃或掉电最近提交的少量事务可能在崩溃恢复时丢失——注意这里说的是丢失已提交事务属于踩了持久性的红线所以对账务、订单类业务我强烈建议保持默认的on。2.3 回滚的代价不是删数据而是贴标签我对很多运维朋友说过一句话在PostgreSQL里你要把回滚理解成记录一个aborted标记而不是把数据变回原来的样子。举个例子BEGIN; UPDATE account SET balance 0 WHERE id 1; ROLLBACK;这条UPDATE执行时数据页上已经产生了新的行版本旧的行版本也还保留着。ROLLBACK只是把当前事务标记为 aborted。之后再开一个事务查询数据库通过事务状态和MVCC可见性规则判断这个aborted事务产生的行版本对别人不可见于是你看到的还是旧值。那些被废弃的行版本不会立刻物理消失它们变成了一种叫 dead tuple 的东西要等后台的VACUUM清理进程来回收。这带来两个非常实际的影响第一频繁回滚的大事务会让表膨胀。我有一个客户跑批任务一个事务里insert了几百万行跑到一半发现数据源有问题直接ROLLBACK。回滚本身几秒钟就完了但表空间膨胀了好几个GB最后手动执行了VACUUM FULL才把空间释放出来。所以设计批量导入任务时尽量别让单个事务积累太多数据再整体回滚。第二序列sequence不回滚。PostgreSQL里用serial自增主键或者nextval()取序列值的时候一旦调用了nextval()这个计数就被消耗了。即使后面事务回滚序列也不会退回去。这就是为什么你会看到自增主键里有空洞比如1、2、5中间缺了3和4。这不是bug是序列的设计初衷——它要保证并发环境下分配的编号不重复所以不能因为事务回滚就还回来。谁要是想让主键绝对连续那不能用数据库序列得另想办法比如业务自定义分配器。3. 保存点让失败只发生在局部不用每次推倒重来3.1 什么时候该用保存点一个批量导入的真实场景先抛一个场景你往一张配置表里批量插入1000条记录每条记录之间没有依赖关系。如果用一个大事务包住插入到第500条时有一条数据违反唯一约束整个事务就要全部回滚前面499条白干还得重头再来。如果不用事务呢每插一条都自动提交成功的插进去了失败的那条单独报错其他999条不受影响——看似没问题但速度太慢而且如果跑到一半程序崩了你无法知道到底插了多少条没有原子性保障。保存点SAVEPOINT就是为这种场景设计的。它允许你在一个大事务里给某个位置打一个锚点之后如果出错了只需要回滚到这个锚点而不是回滚整个事务。锚点之前的操作全部保留锚点之后的操作全部撤销。这句话值得读三遍保存点不是开启一个新事务而是在当前事务内部划出一段可回退的边界。3.2 保存点的基本操作savepoint、rollback to、releasePostgreSQL里保存点的操作有三种关键词SAVEPOINT sp_name在当前事务里创建一个保存点。ROLLBACK TO SAVEPOINT sp_name回滚到指定保存点撤销这个保存点之后的所有操作。RELEASE SAVEPOINT sp_name释放保存点但不撤销任何操作只是把这个锚点删掉。看个完整例子BEGIN; INSERT INTO city (id, name) VALUES (1, 北京); SAVEPOINT sp_before_update; UPDATE city SET name 北平 WHERE id 1; -- 发现改错了回滚到保存点只撤销刚才的update ROLLBACK TO SAVEPOINT sp_before_update; -- 这时city表里仍然有 (1, 北京)因为insert没有被回滚 COMMIT;执行完COMMIT之后你查city表会发现id1的name还是北京。这条INSERT保住了UPDATE被撤销了。这就是保存点的核心价值局部失败局部回滚。再看RELEASE的作用BEGIN; SAVEPOINT sp1; UPDATE account SET balance balance - 10 WHERE id 1; RELEASE SAVEPOINT sp1; -- 此时sp1这个保存点已经不存在了 -- 如果再执行 ROLLBACK TO SAVEPOINT sp1会报错savepoint sp1 does not exist COMMIT;RELEASE之后修改没有提交还在当前事务里事务最终COMMIT时才一起生效。它只是取消了这个锚点让你不能再部分回滚。所以如果你已经确认这一段操作没问题用RELEASE把它释放掉既能让后续无路可退也能避免保存点堆积。3.3 嵌套保存点与同名覆盖这是最容易被忽略的细节保存点是可以嵌套的BEGIN; INSERT INTO t VALUES (1); SAVEPOINT sp_outer; INSERT INTO t VALUES (2); SAVEPOINT sp_inner; INSERT INTO t VALUES (3); ROLLBACK TO SAVEPOINT sp_inner; -- 现在3被撤销2还在1还在 COMMIT;如果直接回滚到外层保存点sp_outer那sp_inner及之后的所有操作都会被撤销包括2和3。嵌套带来的心智负担不轻所以我一般建议非必要不深度嵌套保持一到两层就够了。同名保存点是个很容易踩的坑。执行两次同名SAVEPOINT sp1后面的会覆盖前面的也就是说第一个锚点“失效”了。举个例子BEGIN; SAVEPOINT sp1; INSERT INTO t VALUES (1); SAVEPOINT sp1; -- 同名覆盖了第一个sp1 INSERT INTO t VALUES (2); ROLLBACK TO SAVEPOINT sp1; -- 结果1被保留2被撤销 COMMIT;很多人写循环代码时喜欢在每一轮都用同一个名字创建保存点理论上没问题因为每次新的保存点会覆盖旧的回滚时也只回滚当前轮。但一定要注意别指望还能回滚到更早的同名保存点。循环里如果异常发生回滚到保存点后你还可以继续循环这是正确姿势。3.4 保存点救不了的场景序列、游标、异常块保存点很强大但它不是万能的有几个地方它确实无能为力。序列不回退。前面讲回滚时说过nextval()的消耗不会回滚保存点也一样。你在保存点之后调用了序列回滚到保存点序列值不会归还主键空洞照旧。游标会失效。如果在保存点之后声明了一个游标CURSOR回滚到该保存点时这个游标会被关闭。之后再用这个游标取数据会得到cursor does not exist之类的错误。所以有游标的逻辑别和保存点的回滚边界纠缠在一起。部分FATAL级别的错误会让整个事务直接“死掉”。保存点只能处理常规错误。如果遇到类似磁盘满、死锁重试序列化失败等严重情况事务可能直接被置为 aborted此时保存点无法挽救。比如在REPEATABLE READ隔离级别下发生了可序列化冲突serialization failure整个事务都需要重做ROLLBACK TO SAVEPOINT是救不回来的。还有一个常见误解函数里的EXCEPTION子句其实就隐式使用了保存点机制。当一个PL/pgSQL函数内部有BEGIN ... EXCEPTION WHEN ...块时PostgreSQL会自动为这个块创建一个子事务保存点。块内出错会被异常处理器捕获块外未提交的操作保留。这个特性很好用但会带来额外的子事务开销。如果你在一个循环里给每一行都包一个异常块性能可能会明显下降因为每次成功执行也要支付子事务的创建和释放成本。4. 从事务说到隔离级别与锁热词里全是高频面试点4.1 四种隔离级别速览PostgreSQL默认到底隔离了啥事务隔离级别解决的是并发事务互相能看到什么的问题。标准SQL定义了四个级别PostgreSQL全都支持但它的实现有些特殊隔离级别脏读不可重复读幻读PostgreSQL实际行为READ UNCOMMITTED理论上可能可能可能实际等同READ COMMITTED不会出现脏读READ COMMITTED不可能可能可能默认级别每条语句取新快照REPEATABLE READ不可能不可能理论可能实际快照机制下基本不会事务开始时取快照SERIALIZABLE不可能不可能不可能通过SSI机制检测冲突PostgreSQL里没有真正的脏读原因很简单MVCC的可见性规则本来就不允许你去看未提交事务的数据所以READ UNCOMMITTED这个级别下你依然只能读到已提交数据。默认隔离级别是READ COMMITTED绝大多数在线业务用的都是它。设置事务隔离级别有两种方式-- 方式一在BEGIN时指定 BEGIN ISOLATION LEVEL REPEATABLE READ; -- 方式二在事务中修改 BEGIN; SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;注意SET TRANSACTION必须在事务块内使用而且要放在事务的第一个查询之前否则生效的是下一条语句。4.2 用一个并发测试看懂读已提交与可重复读空说概念不好懂我拿一个实际测试来说明。建一张账户表里面只有一行数据CREATE TABLE account (id int PRIMARY KEY, balance numeric); INSERT INTO account VALUES (1, 100);打开两个psql会话A和B。会话A执行BEGIN; SELECT balance FROM account WHERE id 1; -- 看到 100事务先不提交会话B执行BEGIN; UPDATE account SET balance 50 WHERE id 1; COMMIT;这时候回会话A再执行一次同一个查询。如果你的隔离级别是默认的READ COMMITTED你会看到50。因为READ COMMITTED下每个SQL语句执行时都会重新获取一个快照所以能看到其他事务最新提交的数据这就会出现同一个事务里前后两次查询结果不一样的情况——也就是不可重复读。如果你在会话A开始事务时用了BEGIN ISOLATION LEVEL REPEATABLE READ那么第二次查询会依然看到100。因为在可重复读级别下快照在事务的第一个查询时就固定了之后哪怕别人提交了修改你的事务内部始终看到同一个版本的数据。这个实验在生产环境里非常实用。比如一个报表事务里要跑多个查询如果中间其他事务改了数据默认级别下报表会出现前后统计不一致。改成REPEATABLE READ可以保证整个报表事务看到同一份快照。但代价是如果某个被你读取过的行后来被别的事务修改并提交了你在本事务里再尝试更新这一行会受到could not serialize access due to concurrent update错误整个事务需要重试。4.3 事务没提交时锁的表现为什么别人卡住了你却不卡PostgreSQL的多版本并发控制MVCC决定了普通SELECT不会阻塞写操作写操作也不会阻塞普通SELECT。所以哪怕一个事务长时间开着其他会话查询这张表依然秒回。很多人因此放松警惕觉得事务挂着就挂着呗又不影响别人——这是错误印象。真正会卡的是写入和DDL。看这个经典演示会话ABEGIN; UPDATE account SET balance balance - 10 WHERE id 1; -- 不提交会话B执行同样一行的更新UPDATE account SET balance balance - 20 WHERE id 1; -- 这条语句会一直卡住等待会话A释放行锁为啥卡住因为同一行同时只能有一个事务持有最新版本的修改权。会话A改了这行没提交会话B再改就得等A提交或回滚后才能动手。这种现象叫锁等待。如果你的连接池里全是这种没提交的事务后面的写请求全堵在锁队列里业务接口很快就timeout。比行锁更狠的是DDL锁。ALTER TABLE、DROP TABLE、TRUNCATE这类操作需要拿到ACCESS EXCLUSIVE锁级别它跟几乎所有其他锁都冲突。一个事务只要还保持着哪怕一行SHARE或ROW EXCLUSIVE锁DDL就会在后面干瞪眼。线上最常见的ALTER TABLE卡死了九成是因为有个idle in transaction的连接占着旧锁没释放。锁等待的排查方法下一章具体展开。5. 现场排查长事务、锁等待、死锁的快速定位5.1 最有用的两张视图pg_stat_activity 和 pg_locks排查事务问题我第一件事就是查pg_stat_activity。这张视图会给你当前所有后端进程的状态核心字段有这么几个stateactive表示正在执行SQLidle表示空闲idle in transaction表示事务开了但啥也没干这是最典型的隐形锁源。xact_start当前事务的开始时间。用now() - xact_start能算出事务已经跑了多久。query该连接最近执行的SQL。wait_event_type和wait_event如果这个连接在等待什么这里会显示比如等待锁、等待IO等。查找所有长事务SELECT pid, state, now() - xact_start AS transaction_duration, query FROM pg_stat_activity WHERE state idle ORDER BY transaction_duration DESC;专门抓开了事务不提交的连接SELECT pid, usename, application_name, client_addr, now() - xact_start AS idle_in_tx_duration FROM pg_stat_activity WHERE state idle in transaction AND now() - xact_start interval 5 minutes;一般超过5分钟不提交的事务要么是代码漏了提交要么是业务卡在某个外部调用上都属于高嫌疑对象。确认无误后可以用pg_terminate_backend(pid)强制终止这个后端进程它的事务会自动回滚锁也会释放。注意终止前要和业务方确认别把正在处理的正常事务给掐了。5.2 一个完整的锁等待排查实例假设线上有人报告UPDATE一条SQL执行了好几分钟还没返回。我来演示一遍排查思路。第一步先看谁在等待SELECT pid, state, wait_event_type, wait_event, query FROM pg_stat_activity WHERE wait_event_type Lock;如果查到某条UPDATE的wait_event是transactionid或tuple说明它在等行锁。这时候打开pg_locks看锁的持有关系。一个比较省事的查找阻塞链SQL是这样的SELECT blocked.pid AS blocked_pid, blocking.pid AS blocking_pid, blocked.query AS blocked_query, blocking.query AS blocking_query FROM pg_stat_activity blocked JOIN pg_locks bl ON bl.pid blocked.pid JOIN pg_locks bkl ON bkl.locktype bl.locktype AND bkl.database bl.database AND bkl.relation bl.relation AND bkl.pid bl.pid JOIN pg_stat_activity blocking ON blocking.pid bkl.pid WHERE NOT bl.granted AND bkl.granted;结果里blocked_pid是受害者blocking_pid是持锁者。再看blocking_query十有八九是一句UPDATE后面没有COMMIT或者连接处于idle in transaction。和业务确认后直接SELECT pg_terminate_backend(blocking_pid);就能把堵塞解除。这里我踩过一个坑有些程序里的连接池配置了连接空闲检测但检测的是idle状态对idle in transaction视而不见。连接池把连接还回来后连接以为是干净的实际上事务还挂着。要彻底根治除了在代码层用try/finally保证提交或回滚之外最好把连接池的testOnReturn或者数据库侧的idle_in_transaction_session_timeout参数配起来。PostgreSQL里可以设置SET idle_in_transaction_session_timeout 60s;超过60秒不提交的事务数据库直接帮你断开。这个参数建议在线上设为合理值能挡掉不少僵尸事务。5.3 为什么大事务回滚那么快——准确说是表面快总有人说PostgreSQL回滚大事务很快这话只说对了一半。回滚的快是因为它只是把事务状态标记成aborted并不需要把每一行都物理改回旧值。但从资源清理的角度看它把大量清理工作留给了后面的VACUUM。假如你在大事务里更新了500万行然后ROLLBACK表面上几秒就结束了但数据文件里已经留下了500万个废弃的行版本。后台VACUUM要把它们标记为可复用空间需要扫表、处理可见性映射如果表特别大这个过程可能持续几分钟甚至更久。在这期间如果还伴随大量其他事务表膨胀几乎是必然的。所以我的实操建议是大事务不要一条道走到黑。大批量数据操作能用分批提交就别一个事务塞几十万行。比如导入100万行数据每5000行提交一次失败只回滚当前批次。真遇到不得不整体回滚的场景回滚之后尽快做一次VACUUM必要时VACUUM FULL重建表把膨胀的空间真正释放掉。保存点和子事务也会加重这种负担。每个SAVEPOINT都会创建一个子事务子事务状态记录在pg_subtrans里。如果你在循环里创建了成千上万个保存点事务结束时清理这些子事务状态也需要额外开销性能会明显下降。保存点是给偶发错误用的不是给你每行一个保护伞用的。循环里想让每行都独立失败用PL/pgSQL函数里的EXCEPTION块也没问题但别在热路径上给每个操作都套一层异常捕获那是在拿性能换便利。5.4 经验教训提交、回滚、保存点的最佳实践清单整理一份我个人的实践清单都是踩过坑换来的开启显式事务之前想清楚这个事务里有几个操作哪些必须同生共死哪些允许部分成功。代码里开启事务后用try/finally或者等价写法把COMMIT和ROLLBACK都写在收尾处。我的习惯是先写ROLLBACK兜底再写正常分支的COMMIT宁可有冗余回滚也不要漏掉回滚。一个事务尽量在几百毫秒内完成不要在里面做远程HTTP调用、发消息、等待用户输入。事务活得越久占锁越久膨胀风险越高。批量处理如果不能接受一行失败全部回滚就用保存点或异常块隔离单条记录如果允许部分成功也可以考虑按批次拆分多个独立事务。在线上执行会改变表结构的DDL之前先查一下pg_stat_activity里有没有长事务。DDL需要的ACCESS EXCLUSIVE锁极其霸道一旦有长事务DDL会卡到地老天荒。涉及序列值的时候别指望回滚能抹掉空洞。对账逻辑如果要求主键严格连续说明设计思路有问题得从业务侧解决。定期监控idle in transaction这个状态比active状态可怕得多。它不消耗CPU但占用连接、持有锁、阻止vacuum是个安静的生产事故制造机。最后再分享一个个人体会。做了这些年数据库相关工作我发现提交、回滚、保存点这几个操作语法上谁都会写但真正拉开差距的是边界意识。你写代码时脑子里有没有一张图哪些操作在哪个边界内失败时回滚到哪一层这个边界外的东西是不是安全。想清楚这些很多事务相关的线上事故根本不会发生。这套思路比记住再多命令都管用。
返回列表