MST
星途 面试题库

面试题:缓存设计之专家难度:复杂业务下Redis事务与Lua脚本原子性保证优化策略

假设在一个电商后端缓存设计场景中,存在多个复杂的业务操作,如库存扣减、订单生成、积分增加等,这些操作需要在缓存层面保证原子性。请设计一套基于Redis事务和Lua脚本的原子性解决方案,并说明如何对该方案进行性能优化和异常处理。
39.3万 热度难度
后端开发缓存设计

知识考点

AI 面试

面试题答案

一键面试

基于Redis事务和Lua脚本的原子性解决方案设计

  1. Lua脚本编写
    • 由于Redis事务是一种弱原子性(所有命令会排队执行,但执行过程中如果某个命令失败,其他命令依然会执行),而Lua脚本在Redis中执行是原子性的,所以使用Lua脚本来封装复杂业务操作。
    • 以库存扣减、订单生成、积分增加为例,假设Redis中库存键为stock:{productId},订单信息存储在order:{orderId},积分键为points:{userId}
    -- 获取参数
    local productId = ARGV[1]
    local orderId = ARGV[2]
    local userId = ARGV[3]
    local stockDelta = tonumber(ARGV[4])
    local pointsDelta = tonumber(ARGV[5])
    
    -- 库存扣减
    local stockKey ='stock:'.. productId
    local currentStock = redis.call('GET', stockKey)
    if currentStock == false or tonumber(currentStock) < stockDelta then
        return 0 -- 库存不足,操作失败
    end
    redis.call('DECRBY', stockKey, stockDelta)
    
    -- 订单生成
    local orderKey = 'order:'.. orderId
    redis.call('SET', orderKey, 'order details here')
    
    -- 积分增加
    local pointsKey = 'points:'.. userId
    redis.call('INCRBY', pointsKey, pointsDelta)
    
    return 1 -- 操作成功
    
  2. 调用Lua脚本: 在代码中(以Python为例),使用Redis客户端调用该Lua脚本。
import redis

r = redis.Redis(host='localhost', port=6379, db = 0)

productId = '123'
orderId = '456'
userId = '789'
stockDelta = 1
pointsDelta = 10

script = """
-- 获取参数
local productId = ARGV[1]
local orderId = ARGV[2]
local userId = ARGV[3]
local stockDelta = tonumber(ARGV[4])
local pointsDelta = tonumber(ARGV[5])

-- 库存扣减
local stockKey ='stock:'.. productId
local currentStock = redis.call('GET', stockKey)
if currentStock == false or tonumber(currentStock) < stockDelta then
    return 0 -- 库存不足,操作失败
end
redis.call('DECRBY', stockKey, stockDelta)

-- 订单生成
local orderKey = 'order:'.. orderId
redis.call('SET', orderKey, 'order details here')

-- 积分增加
local pointsKey = 'points:'.. userId
redis.call('INCRBY', pointsKey, pointsDelta)

return 1 -- 操作成功
"""

result = r.eval(script, 0, productId, orderId, userId, stockDelta, pointsDelta)
if result == 1:
    print('操作成功')
else:
    print('操作失败')

性能优化

  1. 减少网络开销
    • 批量操作:尽量将多个相关操作合并成一个Lua脚本执行,减少客户端与Redis之间的往返次数。例如,如果还有其他相关的缓存操作,也可以一并加入到上述Lua脚本中。
    • 使用管道(Pipeline):在需要执行多个独立的Redis命令时,使用管道将多个命令一次性发送到Redis服务器,减少网络延迟。不过要注意,管道中的命令不是原子性执行的,与Lua脚本原子性执行不同。
  2. 优化Lua脚本
    • 减少脚本复杂度:避免在Lua脚本中进行复杂的计算和循环,因为Redis单线程执行脚本,复杂计算会阻塞其他命令执行。如果有复杂计算,尽量在客户端完成后再传入Redis。
    • 合理使用Redis数据结构:例如,使用哈希(Hash)结构存储订单信息等相关数据,而不是简单的字符串,这样可以减少键的数量,提高Redis存储效率。

异常处理

  1. Lua脚本内异常处理
    • 在Lua脚本中,通过返回不同的值来表示操作结果。如上述脚本中,库存不足时返回0表示操作失败,成功时返回1。这样调用端可以根据返回值判断操作是否成功。
    • 对于Lua脚本语法错误等异常,在开发和测试阶段要确保脚本的正确性。可以在本地使用Redis命令行工具redis-cli --eval进行脚本测试。
  2. 客户端异常处理
    • 网络异常:在客户端代码中,使用异常捕获机制(如Python中的try - except)捕获网络相关异常,如连接超时、断开连接等。发生异常时,可以选择重试操作一定次数,或者记录日志并进行相应的业务处理。
    • 返回值异常:除了根据Lua脚本返回的成功或失败标识进行处理外,还要考虑Redis返回的其他异常情况,如命令不存在等。同样通过异常捕获机制进行处理,确保系统的稳定性。