你的代码没改,部署没动,CPU正常,连接池健康——但P99延迟从50毫秒飙到30秒。问题出在哪?

答案藏在数据库锁里。当一千个Lambda函数同时醒来,争抢同一行座位库存,无服务器架构会把你对锁的模糊理解,变成凌晨三点的告警电话。

打开网易新闻 查看精彩图片

悲观锁:先占坑,再办事

最直觉的做法是"先锁后改"。Postgres的FOR UPDATE让第一个事务独占这行数据,其他人排队等着。

代码看起来很简单:

先开启事务,用SELECT ... FOR UPDATE把目标行锁住,检查余量,扣减库存,插入订单,提交。逻辑清晰,但隐患藏在排队里。

无服务器环境下,每个Lambda都是短生命周期的函数。它们启动快、扩容快、并发高——这意味着锁竞争被放大了十倍。传统服务器有几十条连接,Lambda可能瞬间涌出上千个并发执行环境。

悲观锁的排队机制在这里变成瓶颈。事务持有锁的时间越长,队列越长。而Lambda的计费模型是按执行时间付费,排队等待直接变成真金白银的浪费。

更隐蔽的问题是连接池。Postgres的默认连接数有限,大量Lambda同时抢锁会快速耗尽连接。你以为只是慢,其实是连接池在崩溃边缘。

乐观锁:先假设没人抢,撞墙再重来

另一种思路是"先改再看"。给每行数据加个版本号,更新时检查版本是否变化。

实现依赖那张表里的version字段。读取时记下版本,更新时带上条件——只有版本匹配才执行扣减,同时版本加一。

如果两个事务同时读到版本5,第一个提交的成功变成版本6,第二个提交时条件不匹配,返回0行受影响。应用层捕获这个失败,决定重试还是报错。

乐观锁的优势在于不阻塞。没有排队,没有锁等待,吞吐量理论上更高。但代价是冲突时的重试成本,以及应用层必须实现重试逻辑。

在无服务器场景下,乐观锁有个反直觉的陷阱:重试风暴。如果库存只剩1个座位,一千个Lambda同时尝试,只有一个成功,九百九十九个需要重试。重试怎么设计?立即重试会让冲突更剧烈,退避重试又拉长响应时间。

更麻烦的是,Lambda的冷启动特性让重试行为难以预测。一个执行环境可能刚启动就遭遇冲突,重试时已经超时;另一个可能刚好复用暖实例,快速完成。

隔离级别:你以为的锁,其实不是你以为的

很多人把隔离级别和锁混为一谈。Postgres的默认隔离级别是"读已提交",但具体锁什么、锁多久,和直觉常有偏差。

一个常见误解:UPDATE语句只在修改瞬间加锁。实际上,Postgres的行锁从事务开始时的行定位持续到事务结束。如果WHERE条件需要扫描多行,被检查的行都可能被短暂锁定。

另一个坑是幻读。在"读已提交"级别,同一个事务内两次查询可能看到不同的行集合。对于座位库存,这意味着你可能先查到有票,更新时票已经被别人抢走——但你的更新条件没覆盖这个变化。

升级到"可重复读"能解决这个问题,但代价是更多的锁冲突和可能的序列化失败。Postgres的可重复读实现会跟踪读取的行集合,并发修改时抛出错误,让应用层重试。

最高的"序列化"级别理论上最安全,但性能代价在无服务器环境下往往不可接受。它通过谓词锁防止幻读,但谓词锁的粒度粗、冲突多,和Lambda的高并发特性直接冲突。

短生命周期函数的放大效应

传统应用服务器是长连接的。连接池稳定,事务模式可预测,锁的持有时间相对可控。

Lambda完全不同。每个执行环境可能只存活几分钟,甚至几十秒。连接频繁创建销毁,事务必须在函数生命周期内完成,超时阈值通常是几秒到几十秒。

这带来三个连锁反应:

第一,锁持有时间被压缩。函数超时前必须提交或回滚,否则连接被强制断开,事务回滚,锁释放——但此时可能已经造成大量阻塞。

第二,重试逻辑必须内嵌在函数内部。传统应用可以把重试放在请求处理层,Lambda的超时限制让跨函数重试变得复杂。

第三,并发模型从"预测able"变成"爆发式"。流量激增时,Lambda的自动扩容瞬间创造大量执行环境,锁竞争从线性增长变成指数级恶化。

那个30秒延迟的场景,往往是锁等待队列的雪崩。第一个Lambda拿到锁,后面的排队。第二个还没等到,第三个又来了。Postgres的锁等待队列没有优先级,先到先服务在超高压下变成先到先饿死。

实战中的折中策略

没有银弹。悲观锁适合冲突概率高的场景,比如最后几个座位的争抢;乐观锁适合读多写少,或者冲突可以接受的场景。

一个混合方案:用乐观锁处理大部分请求,冲突时降级到悲观锁重试。或者,在应用层做库存分片,把一千个并发拆成十个库存行,每个行一百个并发,降低单点锁竞争。

另一个方向是减少锁粒度。不要把所有座位放在一行,按座位号拆行。牺牲一些存储效率,换取并发能力的线性提升。

连接池也必须重新设计。Lambda和RDS代理(RDS Proxy)的组合几乎是必选项,它管理连接复用,防止连接风暴直接压垮数据库。

最终,理解锁在无服务器环境下的行为模式,比选择悲观还是乐观更重要。监控锁等待时间、死锁频率、事务回滚率,把这些指标和Lambda的并发度、冷启动率关联起来,才能定位真正的瓶颈。

那个从50毫秒到30秒的跳跃,不是数据库变慢了,是架构模型变了,而锁的理解没跟上。