Redis 是后端开发中最常用的中间件之一,但很多开发者对它的两个核心用途 ------ 缓存和分布式锁 ------ 经常混淆。本文从问题本质出发,清晰拆解二者的定义、原理、适用场景和协作方式,帮你建立准确的认知。
一、它们解决的是完全不同的问题
在多用户、多服务器的后端系统中,有两类高频问题:
问题一:读太慢 同一个商品详情页每秒被 1000 个用户访问,每次都去磁盘数据库查询,数据库扛不住,接口越来越慢。
问题二:写冲突 两个用户同时下单抢最后一件商品,两个请求同时查到库存为 1,都扣减成功,库存变成 -1,超卖了。
Redis 缓存解决第一个问题 ------ 加速读。 Redis 分布式锁解决第二个问题 ------ 保护写。
读太慢 ──▶ Redis 缓存 ──▶ 热点数据存内存,下次直接读内存 写冲突 ──▶ Redis 分布式锁 ──▶ 全局互斥,同一时刻只允许一个人写
二、Redis 缓存:加速读取
2.1 核心思想
把高频查询的数据从慢速磁盘数据库复制一份到快速内存中。下次查询时先查内存,命中就直接返回,不再访问数据库。
2.2 工作流程
客户端:查询商品 1001 的详情
│
▼
┌──────────────┐
│ 查 Redis 缓存 │
└──────┬───────┘
│
┌────┴────┐
│ 命中? │
└────┬────┘
是 │ 否
▼ ▼
直接返回 查磁盘数据库
内存数据 │
▼
结果写入 Redis 缓存
(设置过期时间)
│
▼
返回数据
2.3 核心特征
| 特征 | 说明 |
|---|---|
| 操作类型 | 读操作 |
| 数据流向 | 数据库 → Redis → 客户端 |
| 失效方式 | 过期时间自动失效,或手动删除 |
| 一致性 | 允许短暂不一致(缓存和数据库之间有时间窗口) |
| 性能收益 | 微秒级响应,比磁盘数据库快 50~5000 倍 |
2.4 适用场景
- 商品详情、系统配置、字典数据等读多写少的数据
- 复杂报表统计、多表 JOIN 汇总等耗时计算结果
- 会话、Token、验证码等短期临时数据
- 多实例服务器共享同一份查询结果
三、Redis 分布式锁:保护写入
3.1 核心思想
在 Redis 中创建一个 key 作为 "锁"。谁创建成功,谁就获得操作权;其他请求看到 key 已存在,就知道有人在操作,必须等待。操作完成后删除 key,释放锁。
3.2 工作流程
请求 A 请求 B
│ │
▼ ▼
SET lock:order NX EX 30 SET lock:order NX EX 30
│ │
▼ 成功(key 不存在,创建成功) ▼ 失败(key 已存在,被 A 持有)
获得锁 等待 / 重试
│
▼
执行业务逻辑(扣减库存、写入订单)
│
▼
DELETE lock:order(释放锁)
│ │
▼ ▼
SET lock:order NX EX 30
│
▼ 成功
获得锁,执行业务逻辑
3.3 关键命令解析
SET lock:order:1001 unique_id NX EX 30
| 参数 | 含义 |
|---|---|
| lock:order:1001 | 锁的 key,标识被锁定的资源 |
| unique_id | 客户端唯一标识,防止释放别人的锁 |
| NX | 只在 key 不存在时才创建,保证互斥 |
| EX 30 | 设置 30 秒过期,防止进程崩溃后锁永远不释放 |
3.4 核心特征
| 特征 | 说明 |
|---|---|
| 操作类型 | 写操作 |
| 数据流向 | 客户端 → 抢锁 → 写数据库 → 释放锁 |
| 失效方式 | 操作完成后主动释放,或超时自动过期 |
| 一致性 | 强互斥,同一时刻只有一个操作者 |
| 性能收益 | 不加速单次操作,但防止并发写入导致数据错乱 |
3.5 适用场景
- 库存扣减、余额变更等高争抢资源的写操作
- 订单创建,防止重复下单
- 定时任务防重复执行
- 分布式系统中多节点写同一份数据
四、完整对比
| 维度 | Redis 缓存 | Redis 分布式锁 |
|---|---|---|
| 解决的问题 | 读太慢、数据库压力大 | 并发写冲突、脏数据 |
| 作用对象 | 读操作 | 写操作 |
| 核心原理 | 热点数据存内存,加速读取 | 全局互斥,同一时刻只有一个写者 |
| 数据流向 | 数据库 → Redis → 客户端 | 客户端 → 抢锁 → 写库 → 释放 |
| 失效方式 | 过期时间自动失效 | 主动释放 / 超时自动过期 |
| 一致性要求 | 允许短暂不一致 | 强互斥,不允许并发 |
| 性能影响 | 直接提升读取速度 | 不提升速度,防止数据错乱 |
| 存储内容 | 业务数据本身 | 只存一个锁标识(key) |
| 典型命令 | GET / SET / SETEX | SET NX EX / DELETE |
五、它们如何协作
在真实系统中,缓存和分布式锁经常配合使用,各司其职:
┌─────────────────────────────────────────────────────────┐
│ 读请求 │
│ 客户端查询 ──▶ 查 Redis 缓存 ──▶ 命中直接返回 │
│ ──▶ 未命中查数据库 → 回填缓存 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 写请求 │
│ 客户端下单 ──▶ 获取 Redis 分布式锁 │
│ ──▶ 成功:执行写操作 → 更新数据库 → 删缓存 │
│ → 释放锁 │
│ ──▶ 失败:等待重试或返回"系统繁忙" │
└─────────────────────────────────────────────────────────┘
关键细节:写操作完成后,除了更新数据库,还要删除对应的缓存。否则下次读请求会读到缓存中的旧数据。
写操作的完整流程
async def update_product(product_id, new_data):
lock_key = f"lock:product:{product_id}"
# 1. 获取分布式锁
if not await redis.set(lock_key, "1", nx=True, ex=30):
raise Exception("系统繁忙,请稍后重试")
try:
# 2. 更新数据库
await db.execute(
update(Product).where(Product.id == product_id).values(**new_data)
)
# 3. 删除缓存(下次读取时会从数据库重新加载并回填)
await redis.delete(f"cache:product:{product_id}")
finally:
# 4. 释放锁
await redis.delete(lock_key)
六、常见误区
误区一:把缓存当锁用
# ❌ 错误:用缓存 key 判断是否有人在操作
if await redis.get("processing:order:1001"):
return "有人在处理"
await redis.set("processing:order:1001", "1") # 有时间窗口,可能两个人都通过
缓存的 GET + SET 不是原子操作,在并发场景下有时间窗口漏洞。分布式锁用的是 SET NX,是原子操作,天然互斥。
误区二:把锁当缓存用
# ❌ 错误:把业务数据存在锁的 key 里
await redis.set("lock:product:1001", product_data, ex=30)
锁的 key 会在操作完成后被删除,数据会丢失。业务数据应该存在独立的缓存 key 中。
误区三:忘记释放锁
# ❌ 危险:异常时不释放锁,其他请求永远等待
await redis.set(lock_key, "1", nx=True, ex=30)
await do_something() # 如果这里抛异常,锁不会被释放
await redis.delete(lock_key)
# ✅ 正确:用 try-finally 保证释放
await redis.set(lock_key, "1", nx=True, ex=30)
try:
await do_something()
finally:
await redis.delete(lock_key) # 无论成功失败都释放
七、总结
| 概念 | 一句话定义 |
|---|---|
| Redis 缓存 | 把数据存到 Redis 里,下次读的时候直接从内存取,加速读 |
| Redis 分布式锁 | 在 Redis 里占一个坑,别人看到坑被占了就等着,保护写 |
选型决策:
- 你的问题是读太慢? ──▶ 用 Redis 缓存
- 你的问题是写冲突? ──▶ 用 Redis 分布式锁
- 两个问题都有? ──▶ 两个都用,缓存加速读,锁保护写
二者功能独立、互不替代,但在同一个系统中经常搭配使用。理解它们各自解决的问题,才能在架构设计中做出正确的选择。