分布式与分布式锁:从访问量到数据一致性

一、回顾:我们是怎么走到分布式的?

在前两篇文章中,我们讨论了一个系统典型的演进路径:

  1. 单机时代:所有流量打在一台机器上,慢查询靠索引优化。
  2. 读写分离 / 缓存时代:读多写少,加从库、加 Redis。
  3. 分库分表时代:单表数据过大,索引失效,引入中间件拆分数据。

但即便做了分表,如果访问量(QPS) 继续增长,单台应用服务器也会扛不住。

于是,我们把应用部署成多实例,前面挂上负载均衡。

这就进入了分布式时代


二、分布式到底"分布"了什么?

这是一个非常容易被混淆的问题。

✅ 最常见的分布式形态(90% 的中小型系统)

markdown 复制代码
负载均衡(Nginx)
     /    |    \
  App1   App2   App3   ← 分布的是访问量(计算)
     \    |    /
       数据库           ← 数据通常还是一份

在这个架构下:

  • 分布的是访问量和计算压力(CPU、内存、连接数)。
  • 代码在多台机器上是一样的。
  • 数据通常集中在同一个(或一组主从)数据库中。

这也是为什么很多初学者会问:

"既然数据都一样,为什么要锁?"

✅ 更深一层的分布式(数据层)

当数据量进一步膨胀,数据库也会"分布":

  • 读写分离:主库写,从库读(数据通过复制同步)。
  • 分库分表 :数据按规则拆分到不同库/表(如 orders_01, orders_02)。
  • 分布式存储:ES、HBase、TiDB 等,数据天然分片。

但在应用层引入多实例的那一刻起,分布式锁的需求就已经产生了


三、为什么需要分布式锁?

很多人以为分布式锁是为了"保证数据一致性",这个说法太笼统了。

更准确的解释是:

因为本地锁(如 synchronized、ReentrantLock)只在单个 JVM 内有效,无法跨进程工作。

来看一个经典场景:库存扣减

markdown 复制代码
负载均衡
     /    |    \
  App1   App2   App3
     \    |    /
          ↓
       库存 = 1

如果没有锁:

  1. App1 查询库存 = 1
  2. App2 查询库存 = 1
  3. App1 扣减库存 → 0
  4. App2 扣减库存 → -1 ❌

如果在单体应用中,用 synchronized就能解决。

但在多实例部署时,synchronized只能锁住当前这台机器,App1 和 App2 互相感知不到对方的锁。

👉 这时候,我们需要一个所有节点都能看到的"裁判"------分布式锁。


四、分布式锁的常见实现

✅ 1. Redis 实现(最主流)

利用 Redis 的 SET key value NX PX命令:

sql 复制代码
SET lock:order:123 uuid NX PX 30000
  • NX:保证只有一个客户端能设置成功(互斥性)。
  • PX:自动过期,防止死锁。
  • uuid:唯一标识,确保"谁加锁谁解锁"。

解锁时必须使用 Lua 脚本保证原子性:

vbnet 复制代码
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
end
return 0

注意事项:

  • 锁续期:如果业务执行时间超过锁过期时间,需要"看门狗"线程自动续期。
  • 集群容错:Redis 主从切换可能导致锁丢失(Redlock 算法是解决方案之一,但在非极端严苛场景下,单节点 + 合理 TTL 通常足够)。

✅ 2. Zookeeper 实现(高一致性)

  • 利用临时顺序节点
  • 客户端宕机,会话失效,节点自动删除,锁自动释放。
  • 优点:可靠性高,无过期时间困扰。
  • 缺点:性能较 Redis 低,实现复杂。

✅ 3. 数据库实现(了解即可)

利用主键唯一约束或乐观锁(version字段)。

  • 优点:简单,无需引入新组件。
  • 缺点:性能差,不适合高并发场景。

五、分布式锁 ≠ 万能药

这一点非常重要,也是面试高分的关键。

分布式锁解决的是并发互斥问题,而不是所有的数据一致性问题:

问题场景 解决方案
并发扣减库存 分布式锁 / 乐观锁
数据库主从延迟 读写分离策略 / 强制走主库
分布式事务 MQ 最终一致性 / TCC / Saga
缓存与数据库不一致 延迟双删 / Canal 订阅 binlog

特别要注意:

  • 锁粒度:锁的范围要尽可能小(比如锁订单ID,而不是锁整个表)。
  • 避免死锁:必须有超时机制。
  • 非必要不加锁 :能使用数据库乐观锁(UPDATE ... SET stock = stock - 1 WHERE stock > 0)解决的,就不要加分布式锁。

六、总结:从慢查询到分布式的完整演进

让我们把这三篇文章串起来,形成一条清晰的架构演进路线:

  1. 慢查询优化:当系统变慢,先查 SQL,加索引,这是成本最低的优化。
  2. 分库分表:当单表数据量成为瓶颈,通过中间件拆分数据,但引入跨分片查询的复杂性。
  3. 分布式架构:当访问量成为瓶颈,应用多实例部署,引入负载均衡。
  4. 分布式锁:为了解决多实例并发修改共享资源的问题,引入 Redis/Zookeeper 作为协调者。

技术的本质是解决问题,而不是炫技。

从索引到分表,再到分布式锁,每一步都是为了应对特定的规模挑战。理解它们背后的Why ,远比记住 How​ 更重要。


附:面试速记体系(三篇联动)

  • 慢查询:日志 → EXPLAIN → 索引 → SQL 改写 → 分表(最后手段)
  • 分库分表:中间件 → 逻辑表 → 分片键 → 避免广播查询
  • 分布式:分布访问量 → 本地锁失效 → 分布式锁(Redis NX PX)→ 锁续期
相关推荐
再吃一根胡萝卜1 小时前
分库分表:为什么我不建议一上来就分表
后端
再吃一根胡萝卜1 小时前
MySQL 慢查询优化:从定位到根治的完整思路
后端
Nturmoils2 小时前
订单查询丢数据,查了半天原来是 WHERE 惹的祸
数据库·后端
叫我Paul就好2 小时前
自从用上 Agents View 之后, 我的心力交瘁感消失了
后端
SamDeepThinking2 小时前
组合还是继承?我在项目里主要看这两个判断
java·后端·面试
niuju2 小时前
一种基于 Agent 的需求规范化闭环实践:让合规与规范内建于流程
后端
团团崽_七分甜2 小时前
四大数据库底层原理详解
后端
glartchen2 小时前
使用 Pixi 配置 Whisper 环境,实现采用本地大模型读取视频字幕
后端
站大爷IP2 小时前
明明传了参数,进程池却说没找到?记一次变量丢失的排查实录
后端