理解学习Redis分布式锁,面对秒杀等场景

简介:

Redis(Remote Dictionary Server)是一个开源的内存数据结构存储系统,可以用作数据库、缓存和消息中间件。

🚀 核心特点

1. 内存存储,超快性能

  • 数据存储在内存中,读写速度极快(10万+ QPS)
  • 支持持久化到磁盘,保证数据不丢失

2. 丰富的数据结构

  • String:字符串(最常用)
  • Hash:哈希表(适合存储对象)
  • List:列表(支持队列操作)
  • Set:集合(去重、交并差运算)
  • Sorted Set:有序集合(带权重排序)
  • Stream:消息流(5.0版本新增)

3. 多功能用途

  • 缓存:减轻数据库压力,提升响应速度
  • 会话存储:存储用户登录状态
  • 排行榜:利用Sorted Set实现
  • 消息队列:List或Stream实现异步处理
  • 分布式锁:实现系统间互斥操作

💡 实际应用场景

缓存示例(PHP):

1
2
3
4
5
// 缓存用户信息
$redis->setex('user:123', 3600, json_encode($userInfo));

// 读取缓存
$userInfo = $redis->get('user:123');

分布式锁:

1
2
3
4
5
6
7
8
// 获取锁
$lockKey = 'order:lock:1001';
if ($redis->set($lockKey, 'locked', ['nx', 'ex' => 30])) {
// 执行业务逻辑
// ...
// 释放锁
$redis->del($lockKey);
}

🔧 为什么开发者喜欢Redis?

  • 简单易用:命令直观,学习成本低
  • 高性能:单机就能支撑高并发
  • 可靠性:支持主从复制、集群模式
  • 灵活性:多种数据结构适应不同场景

对于您这样的PHP开发者来说,Redis几乎是标配工具,特别是在处理高并发、缓存优化、会话管理等场景时非常有用。

为什么高并发场景会出现脏数据?

根本原因是:多个线程/进程同时操作共享数据时缺乏有效的同步机制

🎯 主要产生原因

1. 竞态条件(Race Condition)

这是最常见的问题,多个请求同时读取、修改、写入同一数据。

典型例子 - 库存扣减:

1
2
3
4
5
6
// 错误的实现方式
$stock = $db->query("SELECT stock FROM products WHERE id = 1");
if ($stock > 0) {
// 这里可能发生上下文切换!
$db->query("UPDATE products SET stock = stock - 1 WHERE id = 1");
}

问题分析:

  • 请求A读取库存:10
  • 请求B也读取库存:10
  • 请求A扣减库存:9
  • 请求B扣减库存:9(应该是8!)

2. 缓存与数据库不一致

1
2
3
4
5
// 先更新数据库
$db->update('user', ['name' => '张三'], ['id' => 1]);
// 再删除缓存
$redis->del('user:1');
// 如果删除缓存失败,缓存还是旧数据!

3. 事务隔离级别不当

数据库默认的隔离级别可能不够严格,导致:

  • 脏读:读到未提交的数据
  • 不可重复读:同一事务内多次读取结果不同
  • 幻读:范围查询时出现新记录

🔧 解决方案

1. 数据库层面

悲观锁(适合写多读少):

1
2
3
4
-- MySQL行锁
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- 执行业务逻辑
UPDATE products SET stock = stock - 1 WHERE id = 1;

乐观锁(适合读多写少):

1
2
3
4
-- 增加版本号字段
UPDATE products SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5;
-- 检查影响行数,为0说明并发冲突

2. Redis+Lua脚本分布式锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用Redis原子操作获取锁
$lockKey = "product:stock:1";
$lockValue = uniqid(); // 确保只能自己释放

if ($redis->set($lockKey, $lockValue, ['nx', 'ex' => 30])) {
try {
// 执行库存扣减逻辑
$stock = $db->query("SELECT stock FROM products WHERE id = 1");
if ($stock > 0) {
$db->query("UPDATE products SET stock = stock - 1 WHERE id = 1");
}
} finally {
// Lua脚本保证原子性释放锁
$script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
$redis->eval($script, [$lockKey, $lockValue], 1);
}
}

3. 消息队列串行化

将并发请求放入队列,逐个处理:

1
2
3
4
5
6
7
8
// 将扣库存请求发送到队列
$queue->push(['type' => 'deduct_stock', 'product_id' => 1, 'quantity' => 1]);

// 消费者逐个处理
while ($job = $queue->pop()) {
// 串行执行,避免并发冲突
processStockDeduction($job);
}

4. 缓存一致性策略

Cache-Aside模式改进:

1
2
3
4
5
// 删除缓存 → 更新数据库 → 再删除缓存(双删)
$redis->del('user:1');
$db->update('user', ['name' => '张三'], ['id' => 1]);
sleep(0.1); // 等待主从同步
$redis->del('user:1');

🛡️ 最佳实践建议

针对您的PHP项目:

  1. 库存类操作:优先使用数据库悲观锁或Redis分布式锁
  2. 用户信息更新:使用乐观锁(version字段)
  3. 缓存更新:采用”先更新DB,再删缓存”策略
  4. 复杂业务:考虑用消息队列串行化处理

监控和测试:

  • 压力测试:模拟高并发场景验证数据一致性
  • 监控告警:监控库存负数、数据不一致等异常
  • 日志追踪:记录关键操作的日志,便于问题排查

进阶理解:为什么要加分布式锁?

分布式锁是为了解决在分布式系统中多个服务实例同时操作共享资源时的数据一致性问题

🎯 核心问题:单机锁失效

单机应用 vs 分布式应用

单机PHP应用:

1
2
3
4
5
6
// 使用文件锁或内存锁就足够了
$fp = fopen('/tmp/lock.txt', 'w');
if (flock($fp, LOCK_EX)) {
// 执行业务逻辑
flock($fp, LOCK_UN);
}

有效:所有请求都在同一台服务器上,锁能控制所有线程

分布式PHP应用:

  • 服务器A、B、C都部署了相同的PHP代码
  • 负载均衡将请求分发到不同服务器
  • 每台服务器的文件锁/内存锁彼此独立

问题:服务器A的锁对服务器B完全无效!

🔥 典型场景分析

场景1:电商秒杀库存

1
2
3
4
5
6
7
8
9
10
11
// 假设3台PHP服务器处理秒杀请求
// 用户请求可能被分发到任意服务器

// 服务器A收到请求
if ($stock > 0) { /* 扣库存 */ }

// 服务器B同时收到请求
if ($stock > 0) { /* 扣库存 */ }

// 服务器C同时收到请求
if ($stock > 0) { /* 扣库存 */ }

结果:3个请求都认为库存充足,导致超卖!

场景2:订单重复创建

1
2
3
4
5
6
// 用户网络不好,重复点击支付按钮
// 请求被分发到不同服务器

// 服务器A:检查订单不存在 → 创建订单
// 服务器B:同时检查订单不存在 → 创建订单
// 结果:同一个用户创建了多个相同订单!

🛡️ 分布式锁的作用

分布式锁提供了一个所有服务实例都能看到的统一协调机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 所有服务器都向同一个Redis申请锁
$lockKey = "order:create:user_123";

if (acquireDistributedLock($lockKey)) {
try {
// 只有一个请求能进入这里
if (!orderExists($userId)) {
createOrder($userId);
}
} finally {
releaseDistributedLock($lockKey);
}
}
// 其他请求要么等待,要么直接失败

💡 为什么选择Redis做分布式锁?

Redis的优势:

  1. 高性能:内存操作,微秒级响应
  2. 原子性:SET命令支持NX(不存在才设置)+ EX(自动过期)
  3. 高可用:支持主从复制、集群模式
  4. 简单可靠:相比ZooKeeper等更轻量

基本实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 获取锁 - 原子操作
$lockAcquired = $redis->set(
"my_lock", // 锁key
"unique_value", // 锁value(用于安全释放)
['nx', 'ex' => 30] // 不存在才设置 + 30秒过期
);

if ($lockAcquired) {
// 执行临界区代码
// ...

// 释放锁 - 使用Lua脚本保证原子性
$script = "
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
";
$redis->eval($script, ["my_lock", "unique_value"], 1);
}

⚠️ 不加分布式锁的风险

场景 风险 业务影响
库存扣减 超卖 经济损失、用户体验差
订单创建 重复订单 财务混乱、客服压力
积分发放 重复发放 成本失控
配置更新 配置冲突 系统异常

🎯 什么时候需要分布式锁?

需要的情况:

  • ✅ 操作涉及共享资源(数据库记录、文件、外部API配额等)
  • ✅ 业务要求强一致性(不能容忍重复、超卖等)
  • ✅ 系统是分布式的(多实例部署)

不需要的情况:

  • ❌ 纯读操作(可以用缓存解决)
  • ❌ 幂等操作(重复执行结果相同)
  • ❌ 单机应用

总结:分布式锁是分布式系统的”交通信号灯”,确保多个服务实例不会”撞车”,保证关键业务的数据正确性。