一、秒杀(Seckill)
1. 定义
- 秒杀:短时间内(如1秒内)大量用户同时抢购 限量低价商品 的营销活动。
- 典型场景:双11热门商品抢购、小米手机首发、演唱会门票开售。
2. 技术挑战
挑战点 | 说明 | 后果示例 |
---|---|---|
高并发请求 | 瞬时QPS可达10万+ | 服务器宕机、请求超时 |
库存精准控制 | 库存扣减需绝对准确 | 超卖(实际卖了1001件,库存1000) |
公平性 | 防止机器人抢购 | 正常用户抢不到 |
3. 示例流程
sequenceDiagram
用户->>秒杀系统: 提交抢购请求(iPhone 100台)
秒杀系统->>Redis: 检查库存 >0?
Redis-->>秒杀系统: 库存剩余10
秒杀系统->>Redis: 原子扣减库存(DECR)
秒杀系统->>消息队列: 生成订单(异步)
用户-->>秒杀系统: 抢购成功
二、超卖(Oversell)
1. 定义
- 超卖:实际卖出的商品数量 超过库存数量(如库存100件,卖出105件)。
- 本质问题:并发场景下 库存判断和扣减的非原子性。
2. 超卖原因
原因 | 技术解释 | 类比场景 |
---|---|---|
并发读脏数据 | 多个线程同时读到库存>0 | 100人同时看到最后1件商品 |
非原子操作 | 先查库存再扣减(非原子) | 收银员A和B同时卖最后1件商品 |
网络延迟 | 请求到达顺序不可控 | 多人同时提交订单 |
3. 超卖过程模拟
bash
# 初始库存:stock=1
用户A:读取stock=1 → 准备扣减(未提交)
用户B:读取stock=1 → 扣减 → stock=0(已提交)
用户A:继续扣减 → stock=-1(超卖!)
三 .代码示例
我用的php代码 语法规则不重要 重要的是思路
首先
你需要在redis中提前缓存 库存的数据 例如: 我这里提前缓存了stock:10=10 然后通过redis去获取库存余额 ,库存不为零则对库存进行递减,如果秒杀用户小于10,就往用户里面添加一个用户id,直到添加了10个秒杀成功的用户。这里的用户id是我前端模拟的js多线程请求传进来的参数。
失败示例
public function pageIndex($inPath)
{
会出现超卖
$path_data = base_lib_BaseUtils::sstripslashes($this->getUrlParams($inPath));
$user_id = base_lib_BaseUtils::getStr($path_data['user_id']);
$SRedis=new SRedis();
$SRedis->init();
$redis= $SRedis->getRedisInstance();
if ($redis->get("stock:10")!=0)
{
$redis->set("stock:10",$redis->get("stock:10")-1);
if ($redis->lLen("miaosha_user")<10){
$redis->rPush("miaosha_user",$user_id);
return base_lib_Return::REDataJson(0,"秒杀成功");
}else{
return base_lib_Return::REDataJson(0,"很抱歉秒杀失败");
}
}
}
我的思考:
虽然这个代码看起来没毛病,并且在请求量
小的情况下,他确实也不会超卖,但是增加了大量并发请求的情况下,就会出现超卖。按照网上redis教程的知识,redis是单线程,这也就是说,正常情况下来说,redis的任何一个单命令都应该是原子性的,可以应对并发,所有客户端请求的命令按顺序执行,天然线程安全。 那我上面的代码按这个说法来讲,应该就是线程安全的,但是为什么会出现并发超卖?
原因:
单个命令确实是线程安全的,但是组合命令就会出现竞态,出现并发问题 参考我的代码这里
我先是get然后set接着又对list长度进行判断,并且对list的内部推入了一个userid,是组合命令
if ($redis->get("stock:10")!=0)
{
$redis->set("stock:10",$redis->get("stock:10")-1);
if ($redis->lLen("miaosha_user")<10){
$redis->rPush("miaosha_user",$user_id);
return base_lib_Return::REDataJson(0,"秒杀成功");
}else{
return base_lib_Return::REDataJson(0,"很抱歉秒杀失败");
}
}
所以这种写法并不能实现应对并发
解决思路:
使用lua脚本以及redis的原子命令,当判断库存不足,或者有用户扣减库存导致了库存为负数,就进行回滚,回复到初始状态0。这样就可以应对高并发的场景。
参考代码
public function pageIndex($inPath) {
$path_data = base_lib_BaseUtils::sstripslashes($this->getUrlParams($inPath));
$user_id = base_lib_BaseUtils::getStr($path_data['user_id']);
$SRedis = new SRedis();
$SRedis->init();
$redis = $SRedis->getRedisInstance();
//lua脚本 原子命令
$lua = <<<LUA
local stock_key = KEYS[1]
local list_key = KEYS[2]
local user_id = ARGV[1]
-- 检查库存
local stock = tonumber(redis.call('GET', stock_key) or 0)
if stock <= 0 then return 0 end
-- 扣减库存
local remaining = redis.call('DECR', stock_key)
if remaining < 0 then
redis.call('INCR', stock_key)
return 0
end
-- 加入用户列表
redis.call('RPUSH', list_key, user_id)
return 1
LUA;
// 执行脚本(KEYS[1], KEYS[2], ARGV[1])
$result = $redis->eval($lua, ["stock:10", "miaosha_user", $user_id], 2);
if ($result === 1) {
return base_lib_Return::REDataJson(0, "秒杀成功");
} else {
return base_lib_Return::REDataJson(0, "很抱歉,秒杀失败");
}
}
}
前端模拟多线程并发请求:
<html>
<meta charset="utf-8">
<body>
<textarea style="width: 500px;height: 500px;font-size: 20px" id="box"></textarea>
<script>
// 生成20个随机用户ID(范围:10000-99999)
const generateRandomUserIds = () => {
const ids = new Set();
while (ids.size < 20) {
const timestamp = Date.now().toString().slice(-5); // 取时间戳后5位
const randomPart = Math.floor(Math.random() * 9000) + 1000; // 4位随机数
ids.add(`${timestamp}${randomPart}`);
}
return Array.from(ids);
};
// 修改后的请求函数
function readysell(userId) {
fetch(`/product/index?user_id=${userId}`, {
method: 'POST',
headers: { 'Accept': 'application/json' }
})
.then(response => {
if (!response.ok) throw new Error('请求失败');
return response.json();
})
.then(data => {
const logMsg = data.data ? `用户 ${userId} 获得锁` : `用户 ${userId} 请求繁忙`;
document.getElementById('box').append(logMsg + '\n')
console.log(logMsg);
})
.catch(error => console.error(error));
}
// 并发模拟逻辑(携带20个随机ID)
function simulateConcurrency() {
const userIds = generateRandomUserIds();
userIds.forEach((userId, index) => {
setTimeout(() => {
console.log(`线程 ${index + 1} 使用ID: ${userId}`);
readysell(userId);
}, Math.random() * 500); // 随机延迟
});
}
// 启动并发测试
simulateConcurrency();
</script>
</body>
</html>
最终结果
可以看到库存扣减为0没有出现负数,并且用户列表添加了十个秒杀成功的用户
分布式锁的实现
redis分布式锁实现秒杀防止超卖https://blog.csdn.net/m0_68711597/article/details/146335587