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 分布式锁
- 两个问题都有? ──▶ 两个都用,缓存加速读,锁保护写
二者功能独立、互不替代,但在同一个系统中经常搭配使用。理解它们各自解决的问题,才能在架构设计中做出正确的选择。