Skip to content

060. 悲观锁与乐观锁处理并发更新

学习目标

这一节用账户充值案例讲并发更新的两种解决方式。

学完后,你应该能理解:

  • for update 如何形成悲观锁;
  • 悲观锁为什么必须放在事务里;
  • 行锁为什么依赖索引;
  • 锁表为什么危险;
  • 乐观锁为什么需要版本号;
  • 乐观锁失败后为什么要重试;
  • 两种方案分别适合什么场景。

悲观锁

悲观锁假设并发冲突一定会发生。

所以读取数据时就先加锁。

在 MySQL 里,可以在查询语句后面加:

sql
for update

这会让数据库给查询到的记录加排他锁。

其他请求再查同一条记录并尝试加锁时,会被阻塞等待。

排他锁如何解决覆盖问题

还是账户充值案例。

账户余额是 200。

多个请求同时给同一个账户充值。

第一个请求查到账户并加锁。

第二个请求查同一条记录时,会等第一个请求提交事务。

第一个请求提交后,第二个请求再读到的是更新后的余额。

这样后续请求不会基于旧余额计算,结果就不会互相覆盖。

必须开启事务

for update 必须在事务里才有意义。

如果没有事务,查询结束后锁会很快释放。

后续更新还没完成,锁已经没了,自然无法保护并发更新。

所以悲观锁流程通常是:

  1. 开启事务;
  2. 查询记录并 for update
  3. 处理业务逻辑;
  4. 更新记录;
  5. 提交事务;
  6. 锁自动释放。

行锁与表锁

理想情况下,悲观锁只锁住目标行。

例如只锁账户 001,不会影响账户 002

但如果查询条件没有命中索引,数据库可能退化成锁表。

锁表会影响整张表里的其他记录。

这会导致大量请求被阻塞,甚至引发大面积超时。

索引非常关键

行锁是加在索引记录上的。

所以 where 条件要尽量命中索引。

按主键查询通常比较稳。

如果按没有索引的字段加 for update,就可能锁表。

使用悲观锁前,要用 explain 检查 SQL 是否命中索引。

悲观锁的适用场景

悲观锁适合:

  • 并发量不高;
  • 数据一致性要求极高;
  • 查询条件明确命中索引;
  • 可以接受请求排队等待。

它的优点是逻辑直接,正确性强。

缺点是并发能力差,且锁表风险高。

乐观锁

乐观锁假设大多数时候不会冲突。

它不在读取时加数据库锁。

而是在更新时判断:我读到的那条记录,在我处理业务逻辑期间有没有被别人改过。

如果没被改过,就更新成功。

如果被改过,就重新读取、重新计算、重新更新。

版本号字段

乐观锁通常需要一个版本号字段。

例如:

txt
version

读取记录时,同时读到当前 version。

更新时带上旧 version 作为条件。

例如:

sql
update account
set amount = new_amount,
    version = version + 1
where id = '001'
  and version = old_version;

如果更新行数是 0,说明 version 已经被别人改过。

这次更新失败,需要重试。

为什么要重试

乐观锁失败不是业务一定失败。

它只是说明你手里的数据过期了。

正确流程是:

  1. 重新查最新数据;
  2. 重新执行业务计算;
  3. 再次尝试更新;
  4. 超过最大次数后返回失败。

重试次数不能无限。

否则高并发时会让后端一直空转。

重试要离开事务

一次更新失败后,要结束当前事务,再等待一小段时间,然后重新开始下一轮。

不要在同一个事务里一直等待。

失败后抛出异常,事务会回滚。

下一轮重试重新开启事务,读到更新后的数据。

乐观锁的压力转移

悲观锁把等待压力放在数据库。

请求会在数据库锁上排队。

乐观锁把更多压力放到后端。

后端自己重试、等待、重新计算。

这对扩展更友好,因为后端服务更容易横向扩容。

数据库通常更难无限扩容。

重试次数的取舍

重试次数越多,业务成功概率越高。

但后端 CPU 和请求占用也越高。

重试次数越少,后端压力越小,但用户遇到失败的概率更高。

课程案例里使用有限次数重试。

真实项目要根据接口重要性、并发规模和用户体验决定。

悲观锁与乐观锁对比

悲观锁:

  • 数据一致性强;
  • 实现简单;
  • 数据库压力大;
  • 容易因为索引问题锁表;
  • 适合低并发高一致性场景。

乐观锁:

  • 并发能力更好;
  • 需要版本号;
  • 代码更复杂;
  • 失败后要重试;
  • 适合读多写少或冲突概率可控的场景。

这一节的重点

这一节解决字段累加时的并发覆盖问题。

你需要记住:

  • for update 是悲观锁;
  • 悲观锁必须在事务里使用;
  • 行锁依赖索引,索引失效可能锁表;
  • 乐观锁依赖版本号;
  • 乐观锁失败要重试,而不是覆盖;
  • 选方案时要看一致性要求和并发压力。

AI Agent 课程学习文档。