MST

星途 面试题库

面试题:Redis脚本管理命令在复杂业务场景的应用

假设你正在开发一个电商秒杀系统,要求使用Redis脚本管理命令实现库存扣减、用户抢购资格验证等自动化操作,以确保高并发下数据的一致性和准确性。请详细描述你的设计思路,包括脚本的编写逻辑、数据结构的选择以及如何处理可能出现的超卖等问题。
28.1万 热度难度
数据库Redis

知识考点

AI 面试

面试题答案

一键面试

设计思路

  1. 数据结构选择
    • 库存:使用Redis的字符串(String)数据结构存储商品库存数量。例如,以商品ID为键,库存数量为值,如SET product:1 100表示商品1的初始库存为100。
    • 抢购资格:使用Redis的集合(Set)数据结构存储有抢购资格的用户ID。例如,SADD eligible_users:1 user1 user2表示商品1有user1和user2两个用户有抢购资格。
  2. 脚本编写逻辑
    • 库存扣减与资格验证:编写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}移除用户的抢购资格,表示该用户已参与抢购。最后返回“抢购成功”。
  3. 处理超卖问题
    • 使用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)