MST
星途 面试题库

面试题:如何在复杂业务场景下,设计和优化基于Redis细粒度化分布式锁的系统架构

假设存在一个电商秒杀系统,涉及库存扣减、订单生成等多个复杂业务逻辑,且需要使用Redis细粒度化分布式锁来保证数据一致性和业务准确性。请阐述你将如何设计和优化该系统架构,包括锁的粒度划分、锁的获取与释放机制、异常处理以及如何避免死锁等问题,并说明设计背后的原理和考虑因素。
40.4万 热度难度
数据库Redis

知识考点

AI 面试

面试题答案

一键面试

系统架构设计与优化

  1. 锁的粒度划分
    • 商品维度:以商品ID作为锁的标识,对每个商品的库存扣减和订单生成操作加锁。这样能确保同一商品在秒杀过程中的数据一致性,不同商品的秒杀操作可并行处理,提高系统并发性能。例如,在一个包含多种商品的秒杀活动中,商品A和商品B的秒杀操作可以同时进行,互不干扰。
    • 订单维度:对于订单生成过程中涉及的唯一性校验等操作,可以订单号为标识加锁。比如,防止同一订单号重复生成订单,保证订单数据的准确性。
  2. 锁的获取与释放机制
    • 获取锁:使用Redis的SETNX(SET if Not eXists)命令获取锁。示例代码(以Python为例):
import redis

r = redis.Redis(host='localhost', port=6379, db = 0)
lock_key = 'product:1:lock'
lock_value = 'unique_value'
is_lock_acquired = r.set(lock_key, lock_value, nx=True, ex = 10)
if is_lock_acquired:
    # 执行秒杀业务逻辑
    pass
else:
    # 锁获取失败,处理逻辑
    pass

其中,nx=True表示只有当键不存在时才设置值,ex = 10表示锁的过期时间为10秒,防止因程序异常未释放锁而导致死锁。 - 释放锁:使用Lua脚本保证释放锁操作的原子性。示例Lua脚本如下:

if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

在Python中调用该Lua脚本释放锁的代码如下:

unlock_script = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end
"""
unlock_result = r.eval(unlock_script, 1, lock_key, lock_value)
if unlock_result:
    print('锁已成功释放')
else:
    print('锁释放失败')
  1. 异常处理
    • 获取锁失败:当获取锁失败时,可采用重试机制。例如,设置重试次数和重试间隔时间,在重试一定次数后若仍未获取到锁,则返回秒杀失败信息给用户。示例代码如下:
max_retries = 3
retry_delay = 0.1
for i in range(max_retries):
    is_lock_acquired = r.set(lock_key, lock_value, nx=True, ex = 10)
    if is_lock_acquired:
        # 执行秒杀业务逻辑
        break
    else:
        time.sleep(retry_delay)
else:
    # 重试次数用尽仍未获取到锁,返回秒杀失败
    pass
- **业务逻辑异常**:在获取锁并执行秒杀业务逻辑过程中,如果出现异常,如库存不足、数据库插入订单失败等,需要确保锁能被正确释放,避免因异常导致锁无法释放而产生死锁。可使用`try - finally`语句块实现,示例如下:
try:
    if is_lock_acquired:
        # 执行库存扣减、订单生成等业务逻辑
        pass
except Exception as e:
    # 异常处理逻辑
    pass
finally:
    if is_lock_acquired:
        unlock_script = """
        if redis.call("GET", KEYS[1]) == ARGV[1] then
            return redis.call("DEL", KEYS[1])
        else
            return 0
        end
        """
        unlock_result = r.eval(unlock_script, 1, lock_key, lock_value)
  1. 避免死锁
    • 设置合理的锁过期时间:如上述获取锁时设置的ex = 10,确保即使锁未正常释放,在一定时间后也能自动过期,避免死锁。但过期时间不宜过短,否则可能导致业务未执行完锁就过期,出现并发问题;也不宜过长,防止长时间占用锁资源影响系统性能。
    • 监控与清理:可以定期检查Redis中存在时间过长的锁,对于可能出现死锁的锁进行强制清理。例如,通过一个定时任务,查询所有锁的创建时间,对于创建时间超过某个阈值(如锁过期时间的两倍)的锁进行删除操作。

设计背后的原理和考虑因素

  1. 锁的粒度划分原理
    • 商品维度:商品库存是独立的资源,每个商品的秒杀操作应相互隔离,以保证库存数据的一致性。采用商品ID作为锁标识,能在保证数据准确性的同时,充分利用系统并发能力,提高秒杀系统的整体性能。
    • 订单维度:订单生成过程中的某些操作,如订单号唯一性校验,需要针对每个订单进行原子化处理,以确保订单数据的完整性和准确性。
  2. 锁的获取与释放机制原理
    • 获取锁:SETNX命令利用Redis单线程特性保证了锁获取操作的原子性,只有一个客户端能成功设置锁的值,从而获取锁。设置过期时间是为了在程序异常情况下避免死锁。
    • 释放锁:使用Lua脚本是因为Redis执行Lua脚本是原子性的,能确保在释放锁时,先判断锁是否为当前客户端持有,再进行删除操作,避免误删其他客户端的锁。
  3. 异常处理原理
    • 获取锁失败重试:重试机制是为了在高并发场景下,由于短暂的锁竞争导致获取锁失败时,给予客户端再次尝试的机会,提高秒杀成功的概率。
    • 业务逻辑异常释放锁try - finally语句块确保无论业务逻辑执行过程中出现何种异常,都能正确释放锁,维护系统的正常运行。
  4. 避免死锁原理
    • 设置合理的锁过期时间:从根本上防止因程序异常未释放锁而导致的死锁问题,保证锁资源的可复用性。
    • 监控与清理:作为一种额外的保障机制,定期检查和清理可能出现死锁的锁,进一步提高系统的稳定性和可靠性。