本文目录导读:

在 PHP 中使用 Redis 实现原子扣减,有多种方法,最常用的是 decr 和 Lua 脚本,以下是完整的实践方案:
最简方式:decr/decrBy(推荐用于简单场景)
<?php
// 连接 Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 初始化库存(仅第一次)
$redis->set('stock:1001', 100);
// 原子扣减 1 个
$remaining = $redis->decr('stock:1001'); // 返回扣减后的剩余值
// 原子扣减 N 个
$remaining = $redis->decrBy('stock:1001', 5);
// 判断是否扣减成功(库存充足)
if ($remaining >= 0) {
echo "扣减成功,剩余库存: {$remaining}";
} else {
// 扣减失败,需要恢复库存
$redis->incrBy('stock:1001', 5); // 回滚
echo "库存不足,扣减失败";
}
⚠️ 注意:
decr不会返回操作前的值,所以无法确认是否扣减了负数,需要额外判断。
Lua 脚本方式(推荐用于需要校验的场景)
1 基础库存扣减
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// Lua 脚本:原子扣减库存
$lua = <<<LUA
-- KEYS[1] = 库存 key
-- ARGV[1] = 扣减数量
local stock = tonumber(redis.call('get', KEYS[1]) or '0')
local deduct = tonumber(ARGV[1])
if stock >= deduct then
redis.call('decrby', KEYS[1], deduct)
return stock - deduct -- 返回剩余库存
else
return -1 -- 库存不足
end
LUA;
$result = $redis->eval($lua, ['stock:1001', 10], 1);
if ($result >= 0) {
echo "扣减成功,剩余库存: {$result}";
} else {
echo "库存不足";
}
2 带过期时间的库存扣减
<?php
// 防止库存永久存在,设置过期时间
$redis->set('stock:1001', 100, ['EX' => 86400]); // 24小时过期
// Lua 脚本:带过期检查
$lua = <<<LUA
local stock_key = KEYS[1]
local deduct = tonumber(ARGV[1])
local ttl = redis.call('ttl', stock_key)
-- 检查 key 是否存在且还有剩余时间
if ttl < 0 then
return -2; -- key 不存在或已过期
end
local stock = tonumber(redis.call('get', stock_key) or '0')
if stock >= deduct then
redis.call('decrby', stock_key, deduct)
-- 重置过期时间(如果需要)
redis.call('expire', stock_key, 86400)
return stock - deduct
else
return -1
end
LUA;
$result = $redis->eval($lua, ['stock:1001', 5], 1);
3 带用户防重的库存扣减
<?php
// 场景:同一用户不能重复扣减
$lua = <<<LUA
local stock_key = KEYS[1]
local user_key = KEYS[2] -- 记录已扣减用户的 key
local user_id = ARGV[1]
local deduct = tonumber(ARGV[2])
-- 检查用户是否已扣减过
if redis.call('sismember', user_key, user_id) == 1 then
return -2; -- 重复操作
end
local stock = tonumber(redis.call('get', stock_key) or '0')
if stock >= deduct then
redis.call('decrby', stock_key, deduct)
redis.call('sadd', user_key, user_id)
-- 设置用户记录过期时间
redis.call('expire', user_key, 3600)
return stock - deduct
else
return -1
end
LUA;
$result = $redis->eval($lua,
['stock:1001', 'order_users:1001'],
['user_123', 2],
2
);
使用事务(MULTI/EXEC)替代方案
<?php
// 虽然事务不是原子的,但可以结合 WATCH 实现乐观锁
$redis->watch('stock:1001');
$stock = $redis->get('stock:1001');
if ($stock < 10) {
$redis->unwatch();
echo "库存不足";
exit;
}
// 开始事务
$redis->multi();
$redis->decrBy('stock:1001', 10);
$result = $redis->exec();
if ($result) {
echo "扣减成功";
} else {
echo "操作失败,请重试";
}
完整实践:秒杀/抢购场景
<?php
class StockService {
private $redis;
public function __construct($redis) {
$this->redis = $redis;
}
/**
* 原子扣减库存
* @param string $productId 商品ID
* @param int $quantity 扣减数量
* @param int $userId 用户ID
* @return array [code, message, data]
*/
public function deductStock($productId, $quantity = 1, $userId = null) {
$stockKey = "stock:{$productId}";
$userKey = "deduct_users:{$productId}";
$lua = <<<LUA
-- KEYS[1]: 库存key
-- KEYS[2]: 用户记录key
-- ARGV[1]: 用户ID
-- ARGV[2]: 扣减数量
-- ARGV[3]: 用户记录过期时间
local user_id = ARGV[1]
local deduct = tonumber(ARGV[2])
local expire_time = tonumber(ARGV[3])
-- 1. 检查用户是否已参与过
if user_id and user_id ~= '' then
if redis.call('sismember', KEYS[2], user_id) == 1 then
return {0, 'duplicate', 0}
end
end
-- 2. 检查是否存在
local stock = redis.call('get', KEYS[1])
if not stock then
return {0, 'not_exists', 0}
end
-- 3. 转换并检查
stock = tonumber(stock)
if stock < deduct then
return {0, 'out_of_stock', 0}
end
-- 4. 执行扣减
redis.call('decrby', KEYS[1], deduct)
-- 5. 记录用户(防止重复)
if user_id and user_id ~= '' then
redis.call('sadd', KEYS[2], user_id)
if expire_time > 0 then
redis.call('expire', KEYS[2], expire_time)
end
end
-- 6. 返回成功
return {1, 'success', stock - deduct}
LUA;
try {
$results = $this->redis->eval($lua,
[$stockKey, $userKey],
[$userId ?? '', $quantity, 86400],
2
);
return [
'success' => $results[0] == 1,
'message' => $results[1],
'remaining' => (int)$results[2]
];
} catch (Exception $e) {
return [
'success' => false,
'message' => '系统错误: ' . $e->getMessage(),
'remaining' => 0
];
}
}
/**
* 恢复库存(订单取消时)
*/
public function rollbackStock($productId, $quantity) {
$key = "stock:{$productId}";
return $this->redis->incrby($key, $quantity);
}
}
使用建议
推荐方案优先级:
- Lua 脚本 - 完全原子性,支持复杂逻辑 ✅ 最推荐
- DECR/DECRBY - 简单场景,性能最好 ✅ 适合独立操作
- WATCH + MULTI - 乐观锁,适合不需要强一致性的场景
注意事项:
- 初始化库存时使用
setnx防止覆盖 - 扣减失败时考虑是否需要回滚
- 为库存 key 设置合理的过期时间
- 在高并发场景下使用连接池
- 监控 Redis 内存使用情况
选择哪种方式取决于你的业务需求,如果场景简单,直接用 decr 就足够了;如果需要复杂校验或防重复,推荐使用 Lua 脚本。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播