面试题答案
一键面试设计思路
- 数据结构选择
- 库存:使用Redis的字符串(String)数据结构存储商品库存数量。例如,以商品ID为键,库存数量为值,如
SET product:1 100
表示商品1的初始库存为100。 - 抢购资格:使用Redis的集合(Set)数据结构存储有抢购资格的用户ID。例如,
SADD eligible_users:1 user1 user2
表示商品1有user1和user2两个用户有抢购资格。
- 库存:使用Redis的字符串(String)数据结构存储商品库存数量。例如,以商品ID为键,库存数量为值,如
- 脚本编写逻辑
- 库存扣减与资格验证:编写Lua脚本实现原子操作。脚本接收商品ID和用户ID作为参数。
- 首先,通过
GET product:{product_id}
获取商品库存。如果库存小于等于0,返回提示“库存不足”。 - 然后,通过
SISMEMBER eligible_users:{product_id} {user_id}
检查用户是否有抢购资格。如果没有,返回提示“无抢购资格”。 - 若库存充足且用户有资格,通过
DECRBY product:{product_id} 1
原子性地扣减库存,并通过SREM eligible_users:{product_id} {user_id}
移除用户的抢购资格,表示该用户已参与抢购。最后返回“抢购成功”。
- 处理超卖问题
- 使用Lua脚本原子操作:因为Lua脚本在Redis中是原子执行的,在脚本执行过程中不会被其他命令打断,保证了库存扣减和资格验证的一致性。在检查库存和扣减库存的操作在一个脚本中完成,避免了并发情况下,多个请求同时检查库存都通过,但实际扣减库存时出现超卖。
- 乐观锁机制:可以在商品库存值上附加版本号。每次扣减库存前,先获取库存值和版本号,扣减库存时,通过
WATCH
命令监视库存键,若库存值在扣减操作前被其他客户端修改(版本号变化),则本次操作失败,客户端可以重新尝试。不过在Lua脚本原子操作的场景下,这种方式相对复杂,Lua脚本原子操作本身已经能很好避免超卖,乐观锁作为一种补充手段。
示例Lua脚本如下:
-- 获取商品库存
local stock = redis.call('GET', 'product:'.. ARGV[1])
if stock == false or tonumber(stock) <= 0 then
return "库存不足"
end
-- 检查用户抢购资格
local is_eligible = redis.call('SISMEMBER', 'eligible_users:'.. ARGV[1], ARGV[2])
if is_eligible == 0 then
return "无抢购资格"
end
-- 扣减库存
redis.call('DECRBY', 'product:'.. ARGV[1], 1)
-- 移除用户抢购资格
redis.call('SREM', 'eligible_users:'.. ARGV[1], ARGV[2])
return "抢购成功"
在代码中调用该脚本时,将商品ID和用户ID作为参数传递给Redis执行脚本的命令,如Python中使用redis - py
库:
import redis
r = redis.Redis(host='localhost', port=6379, db = 0)
script = """
-- 获取商品库存
local stock = redis.call('GET', 'product:'.. ARGV[1])
if stock == false or tonumber(stock) <= 0 then
return "库存不足"
end
-- 检查用户抢购资格
local is_eligible = redis.call('SISMEMBER', 'eligible_users:'.. ARGV[1], ARGV[2])
if is_eligible == 0 then
return "无抢购资格"
end
-- 扣减库存
redis.call('DECRBY', 'product:'.. ARGV[1], 1)
-- 移除用户抢购资格
redis.call('SREM', 'eligible_users:'.. ARGV[1], ARGV[2])
return "抢购成功"
"""
sha = r.script_load(script)
result = r.evalsha(sha, 0, 'product_id', 'user_id')
print(result)