面试题答案
一键面试系统架构设计与优化
- 锁的粒度划分
- 商品维度:以商品ID作为锁的标识,对每个商品的库存扣减和订单生成操作加锁。这样能确保同一商品在秒杀过程中的数据一致性,不同商品的秒杀操作可并行处理,提高系统并发性能。例如,在一个包含多种商品的秒杀活动中,商品A和商品B的秒杀操作可以同时进行,互不干扰。
- 订单维度:对于订单生成过程中涉及的唯一性校验等操作,可以订单号为标识加锁。比如,防止同一订单号重复生成订单,保证订单数据的准确性。
- 锁的获取与释放机制
- 获取锁:使用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('锁释放失败')
- 异常处理
- 获取锁失败:当获取锁失败时,可采用重试机制。例如,设置重试次数和重试间隔时间,在重试一定次数后若仍未获取到锁,则返回秒杀失败信息给用户。示例代码如下:
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)
- 避免死锁
- 设置合理的锁过期时间:如上述获取锁时设置的
ex = 10
,确保即使锁未正常释放,在一定时间后也能自动过期,避免死锁。但过期时间不宜过短,否则可能导致业务未执行完锁就过期,出现并发问题;也不宜过长,防止长时间占用锁资源影响系统性能。 - 监控与清理:可以定期检查Redis中存在时间过长的锁,对于可能出现死锁的锁进行强制清理。例如,通过一个定时任务,查询所有锁的创建时间,对于创建时间超过某个阈值(如锁过期时间的两倍)的锁进行删除操作。
- 设置合理的锁过期时间:如上述获取锁时设置的
设计背后的原理和考虑因素
- 锁的粒度划分原理
- 商品维度:商品库存是独立的资源,每个商品的秒杀操作应相互隔离,以保证库存数据的一致性。采用商品ID作为锁标识,能在保证数据准确性的同时,充分利用系统并发能力,提高秒杀系统的整体性能。
- 订单维度:订单生成过程中的某些操作,如订单号唯一性校验,需要针对每个订单进行原子化处理,以确保订单数据的完整性和准确性。
- 锁的获取与释放机制原理
- 获取锁:SETNX命令利用Redis单线程特性保证了锁获取操作的原子性,只有一个客户端能成功设置锁的值,从而获取锁。设置过期时间是为了在程序异常情况下避免死锁。
- 释放锁:使用Lua脚本是因为Redis执行Lua脚本是原子性的,能确保在释放锁时,先判断锁是否为当前客户端持有,再进行删除操作,避免误删其他客户端的锁。
- 异常处理原理
- 获取锁失败重试:重试机制是为了在高并发场景下,由于短暂的锁竞争导致获取锁失败时,给予客户端再次尝试的机会,提高秒杀成功的概率。
- 业务逻辑异常释放锁:
try - finally
语句块确保无论业务逻辑执行过程中出现何种异常,都能正确释放锁,维护系统的正常运行。
- 避免死锁原理
- 设置合理的锁过期时间:从根本上防止因程序异常未释放锁而导致的死锁问题,保证锁资源的可复用性。
- 监控与清理:作为一种额外的保障机制,定期检查和清理可能出现死锁的锁,进一步提高系统的稳定性和可靠性。