一、回顾:我们是怎么走到分布式的?
在前两篇文章中,我们讨论了一个系统典型的演进路径:
- 单机时代:所有流量打在一台机器上,慢查询靠索引优化。
- 读写分离 / 缓存时代:读多写少,加从库、加 Redis。
- 分库分表时代:单表数据过大,索引失效,引入中间件拆分数据。
但即便做了分表,如果访问量(QPS) 继续增长,单台应用服务器也会扛不住。
于是,我们把应用部署成多实例,前面挂上负载均衡。
这就进入了分布式时代。
二、分布式到底"分布"了什么?
这是一个非常容易被混淆的问题。
✅ 最常见的分布式形态(90% 的中小型系统)
markdown
负载均衡(Nginx)
/ | \
App1 App2 App3 ← 分布的是访问量(计算)
\ | /
数据库 ← 数据通常还是一份
在这个架构下:
- 分布的是访问量和计算压力(CPU、内存、连接数)。
- 代码在多台机器上是一样的。
- 数据通常集中在同一个(或一组主从)数据库中。
这也是为什么很多初学者会问:
"既然数据都一样,为什么要锁?"
✅ 更深一层的分布式(数据层)
当数据量进一步膨胀,数据库也会"分布":
- 读写分离:主库写,从库读(数据通过复制同步)。
- 分库分表 :数据按规则拆分到不同库/表(如
orders_01,orders_02)。 - 分布式存储:ES、HBase、TiDB 等,数据天然分片。
但在应用层引入多实例的那一刻起,分布式锁的需求就已经产生了。
三、为什么需要分布式锁?
很多人以为分布式锁是为了"保证数据一致性",这个说法太笼统了。
更准确的解释是:
因为本地锁(如 synchronized、ReentrantLock)只在单个 JVM 内有效,无法跨进程工作。
来看一个经典场景:库存扣减
markdown
负载均衡
/ | \
App1 App2 App3
\ | /
↓
库存 = 1
如果没有锁:
- App1 查询库存 = 1
- App2 查询库存 = 1
- App1 扣减库存 → 0
- 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)解决的,就不要加分布式锁。
六、总结:从慢查询到分布式的完整演进
让我们把这三篇文章串起来,形成一条清晰的架构演进路线:
- 慢查询优化:当系统变慢,先查 SQL,加索引,这是成本最低的优化。
- 分库分表:当单表数据量成为瓶颈,通过中间件拆分数据,但引入跨分片查询的复杂性。
- 分布式架构:当访问量成为瓶颈,应用多实例部署,引入负载均衡。
- 分布式锁:为了解决多实例并发修改共享资源的问题,引入 Redis/Zookeeper 作为协调者。
技术的本质是解决问题,而不是炫技。
从索引到分表,再到分布式锁,每一步都是为了应对特定的规模挑战。理解它们背后的Why ,远比记住 How 更重要。
附:面试速记体系(三篇联动)
- 慢查询:日志 → EXPLAIN → 索引 → SQL 改写 → 分表(最后手段)
- 分库分表:中间件 → 逻辑表 → 分片键 → 避免广播查询
- 分布式:分布访问量 → 本地锁失效 → 分布式锁(Redis NX PX)→ 锁续期