问题分析
- 锁持有时间控制不精准:
- 原因:在设置锁的过期时间时,由于网络延迟、系统时钟偏差等因素,很难精确控制锁的释放时间。如果过期时间设置过短,可能导致业务未完成锁就提前释放,出现并发问题;若设置过长,又会影响系统并发性能,其他等待获取锁的进程会长时间等待。
- 锁竞争时等待策略不够优化:
- 原因:常见的等待策略可能是简单的循环重试,这会浪费大量的CPU资源。在高并发场景下,大量客户端同时竞争锁,频繁的重试会导致系统负载过高,而且无法保证公平性,可能会出现某些客户端长时间无法获取锁的情况。
改进方案
- 锁持有时间精准控制:
- 使用时间戳对比:在获取锁时,记录当前时间戳
startTime
,并在Lua脚本中计算业务执行时间executionTime
。当业务执行完成释放锁时,再次获取当前时间戳endTime
,计算endTime - startTime
得到实际执行时间。将实际执行时间与预设的锁最长持有时间进行对比,如果未超过,则正常释放锁;若超过,可通过日志记录等方式进行告警。示例Lua脚本如下:
local lockKey = KEYS[1]
local clientId = ARGV[1]
local unlockTime = ARGV[2]
local startTime = ARGV[3]
local currentTime = redis.call('TIME')
local endTime = tonumber(currentTime[1]) * 1000 + math.floor(tonumber(currentTime[2]) / 1000)
local executionTime = endTime - startTime
if executionTime <= unlockTime then
if redis.call('GET', lockKey) == clientId then
return redis.call('DEL', lockKey)
else
return 0
end
else
-- 记录日志等操作
return 0
end
- 优化锁竞争等待策略:
- 引入队列机制:使用Redis的列表(List)数据结构作为等待队列。当客户端获取锁失败时,将客户端标识添加到等待队列中。在释放锁时,从等待队列中取出第一个客户端标识,通知该客户端获取锁。这样可以保证公平性,减少不必要的重试。示例Lua脚本如下:
-- 获取锁
local lockKey = KEYS[1]
local clientId = ARGV[1]
local result = redis.call('SETNX', lockKey, clientId)
if result == 1 then
return 1
else
-- 添加到等待队列
local queueKey = lockKey.. ':queue'
redis.call('RPUSH', queueKey, clientId)
return 0
end
-- 释放锁
local lockKey = KEYS[1]
local clientId = ARGV[1]
if redis.call('GET', lockKey) == clientId then
local queueKey = lockKey.. ':queue'
local nextClient = redis.call('LPOP', queueKey)
if nextClient then
redis.call('SET', lockKey, nextClient)
else
redis.call('DEL', lockKey)
end
return 1
else
return 0
end
利用Redis Lua环境协作组件提升性能和可靠性
- 原子性操作:Redis Lua脚本保证了在执行过程中不会被其他命令打断,通过将获取锁、释放锁等相关操作封装在Lua脚本中,可以确保这些操作的原子性。例如在锁持有时间精准控制的脚本中,通过Lua脚本可以在同一原子操作中完成时间对比和锁释放,避免了因时间对比和锁释放之间出现其他操作而导致的并发问题,提升了可靠性。
- 减少网络开销:将复杂的逻辑封装在Lua脚本中,客户端只需向Redis发送一次脚本执行请求,而不是多次命令请求。在优化锁竞争等待策略中,获取锁、添加到等待队列、释放锁等操作如果分开发送命令,会增加网络开销和延迟。使用Lua脚本可以将这些操作整合,减少网络往返次数,提升性能。
- 利用Redis数据结构:Redis提供了丰富的数据结构,如列表(List)。在改进方案中利用列表实现等待队列,Lua脚本可以方便地操作这些数据结构,并且借助Redis的持久化机制,即使系统故障重启,等待队列中的数据也不会丢失,进一步提升了分布式锁的可靠性。