面试题答案
一键面试1. 锁的粒度设计
- 行级锁粒度:如果业务允许,尽量使用行级锁粒度。例如,在订单系统中,针对每个订单号进行加锁。这样可以减少锁的范围,在高并发场景下,不同订单的操作可以并行执行,提高系统并发性能。在Redis中,可以使用订单号作为锁的键值,例如
SETNX order:123 true
,这里order:123
就是以订单号为粒度的锁。 - 表级锁粒度:当无法精确到行级操作时,可考虑表级锁。但这会导致同一时间只有一个事务能对整个表进行操作,并发度较低。在Redis中,以表名作为锁的键值,如
SETNX users_table true
,来锁住整个users
表。
2. 锁的竞争处理
- 自旋重试:当一个客户端获取锁失败时,可以进行自旋重试。例如,使用循环来不断尝试获取锁,每次尝试之间设置一个短暂的休眠时间,如
100ms
。示例代码(以Python为例):
import time
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
def get_lock(lock_key, lock_value, retry_count=5, retry_delay=0.1):
for _ in range(retry_count):
if r.setnx(lock_key, lock_value):
return True
time.sleep(retry_delay)
return False
- 队列化处理:将获取锁失败的请求放入队列中(如Redis的
list
数据结构)。由一个后台进程按照顺序从队列中取出请求,依次获取锁并执行操作。这样可以避免大量请求同时竞争锁,减轻系统压力。
3. 锁的实现细节
- 使用SETNX指令:在Redis中,使用
SETNX
(SET if Not eXists)指令来获取锁。SETNX key value
,如果key
不存在,则设置key
为value
并返回1
,表示获取锁成功;如果key
已存在,则返回0
,获取锁失败。 - 设置锁的过期时间:为防止死锁,获取锁后需要设置一个合理的过期时间。可以在获取锁成功后,使用
EXPIRE
指令设置过期时间,如EXPIRE lock_key 30
,表示锁在30秒后自动过期。也可以在SETNX
时直接带上过期时间参数,如SET lock_key value NX EX 30
。 - 锁的唯一标识:每个获取锁的客户端应生成一个唯一标识(如UUID)作为锁的值。在释放锁时,只有持有相同唯一标识的客户端才能释放锁,避免误释放其他客户端的锁。例如,在Python中可以使用
uuid.uuid4().hex
生成唯一标识。
import uuid
lock_value = uuid.uuid4().hex
- 释放锁的操作:释放锁时,需要使用Lua脚本保证释放锁操作的原子性。因为在删除锁之前需要先判断锁的值是否与当前客户端持有的值一致,防止误删其他客户端的锁。示例Lua脚本如下:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
在Python中调用Lua脚本释放锁的示例代码:
release_lock_script = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
"""
release_lock = r.register_script(release_lock_script)
release_lock(keys=[lock_key], args=[lock_value])
4. 监控与优化
- 监控锁的持有时间:通过监控锁的持有时间,及时发现持有时间过长的锁,可能存在业务逻辑处理缓慢或死锁等问题。可以在应用程序中记录锁的获取和释放时间,并使用监控工具(如Prometheus + Grafana)展示锁的持有时间分布。
- 优化业务逻辑:尽量缩短锁的持有时间,对业务逻辑进行优化,减少在持有锁期间的复杂计算或I/O操作。例如,将一些非关键的计算移到获取锁之前或释放锁之后执行。