Lua 脚本核心原理:
1.单线程模型:Redis 使用单线程处理命令,所有命令按顺序执行。Lua 脚本会被视为一个整体任务,执行期间不会被其他命令中断。
2.原子性保证:将库存检查、扣减、订单记录等多个操作放在一个脚本中,会连续执行,中间不会有其他客户端操作插入。保证了脚本的原子性。
3.无需锁机制:由于 Redis 的单线程特性,Lua 脚本天然避免了并发冲突,无需额外加锁。
实现流程:
1.首先库存预热:活动开始前将对应商品的库存存redis;
2.然后实现lua脚本(可以将库存检查、扣减、订单记录等多个操作放在一个脚本中)。
下述的字段备注
-- 秒杀Lua脚本
-- KEYS[1]: 库存key
-- KEYS[2]: 订单集合key(用于去重)
-- ARGV[1]: 用户ID
-- ARGV[2]: 当前时间戳(可选)
-- 返回: 1-秒杀成功; 0-秒杀失败
php具体实现如下:
class SeckillService
{
private $redis;
public function __construct()
{
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
}
/**
* 执行秒杀
* @param int $productId 商品ID
* @param int $userId 用户ID
* @return array ['success'=>bool, 'msg'=>string]
*/
public function seckill($productId, $userId)
{
// 定义Redis key
$stockKey = "product:{$productId}:stock";
$orderKey = "product:{$productId}:orders";
// Lua脚本
$luaScript = <<<LUA
local stockKey = KEYS[1]
local orderKey = KEYS[2]
local userId = ARGV[1]
local stock = tonumber(redis.call('GET', stockKey) or 0)
if stock <= 0 then
return 0
end
if redis.call('SISMEMBER', orderKey, userId) == 1 then
return 0
end
redis.call('DECR', stockKey)
redis.call('SADD', orderKey, userId)
return 1
LUA;
try {
// 执行Lua脚本
$result = $this->redis->eval(
$luaScript,
[$stockKey, $orderKey, $userId],
2 // KEYS的数量
);
if ($result == 1) {
// 秒杀成功,异步创建实际订单到数据库
$this->createOrderAsync($productId, $userId);
return ['success' => true, 'msg' => '秒杀成功'];
}
return ['success' => false, 'msg' => '秒杀失败,可能已售罄或您已参与过'];
} catch (Exception $e) {
// 记录日志
error_log("秒杀异常: " . $e->getMessage());
return ['success' => false, 'msg' => '系统繁忙,请稍后再试'];
}
}
/**
* 异步创建订单(实际项目中可用消息队列实现)
*/
private function createOrderAsync($productId, $userId)
{
// 这里可以写入消息队列或直接处理
// 示例: 写入文件日志,实际项目中不要这样做
$orderData = json_encode([
'product_id' => $productId,
'user_id' => $userId,
'time' => time()
]);
file_put_contents('orders.log', $orderData . "\n", FILE_APPEND);
// 实际项目建议使用消息队列如RabbitMQ、Kafka等
// $this->mq->publish('order_queue', $orderData);
}
/**
* 初始化商品库存(仅在活动前调用)
*/
public function initStock($productId, $quantity)
{
$stockKey = "product:{$productId}:stock";
return $this->redis->set($stockKey, $quantity);
}
}
注:
在 Redis 集群环境下执行 Lua 脚本时,确实需要确保所有操作的键(Keys)都位于同一个哈希槽(Hash Slot)上(即同一个节点上),否则会触发 CROSSSLOT 错误。这是因为 Redis 集群通过哈希槽分配键,而 Lua 脚本需要在单个节点上原子执行。为了解决这个问题,我们可以使用hash tag来确保相关的键分布在同一个节点上。
在lua脚本中使用哈希标签(Hash Tags)
在键名中用 {} 包裹相同部分,强制这些键分配到同一槽
$stockKey = "product:{$productId}:stock";
$orderKey = "product:{$productId}:orders";
上述的{$productId}是相同的所以会被分配到同一卡槽
补充:
1)关于哈希标签:
Redis 集群通过 CRC16(key) % 16384 计算键的哈希槽,默认情况下,整个键名会参与计算。但如果在键名中用 {} 包裹一部分字符串(如 {1000}),则只有 {} 内的内容会被用于计算哈希槽。
示例
- 键名
user:1000:profile和user:1000:orders:- 哈希槽计算基于完整键名,可能分配到不同槽。
- 键名
user:{1000}:profile和user:{1000}:orders:- 哈希槽计算仅基于
{1000},因此两个键必然分配到同一个槽。
- 哈希槽计算仅基于
2). 为什么需要哈希标签?
在 Redis 集群中,Lua 脚本、事务(MULTI/EXEC)或多键操作(如 MGET、SUNION)要求所有操作的键必须位于同一个节点(即同一个哈希槽)。如果键分散在不同槽,会报错:
(error) CROSSSLOT Keys in request don't hash to the same slot |
通过哈希标签,可以绕过这一限制。
665

被折叠的 条评论
为什么被折叠?



