自己实现Redis分布式锁
业务需求:秒杀商品
初步,2台服务器负载均衡处理请求
问题1: 服务器A处理请求,事务时间比较长,中间服务器B更新了数据库,A最后更新数据库覆盖B的更新结果,最后超卖。
解决1:使用乐观锁,秒杀商品,并发量会很大,大量冲突,会导致乐观锁不断回滚,一直重试(没法用)
解决2:由于Redis是单线程的,所以可以使用Redis锁,通过setnx指令保证同一时间只有一台服务器操作库存,操作结束del指令释放锁,解决结果覆盖问题
// 加锁
setnx lock 1
// 释放锁
del lock
问题2: 如果存锁的服务端挂掉怎么办?
解决1:存锁的客户端挂掉,会导致锁无法释放,这里可以加过期时间
// 加锁
setnx lock 1
// 设置10秒过期
expire lock 100
问题3: 如果加锁成功了,但是加过期时间失败怎么办?
lua脚本使用介绍
-- redis自从2.6.0版本起就采用内置的Lua解释器通过EVAL命令去执行脚本
-- lua脚本中调用redis命令可以使用两个命令
redis.call()
redis.pcall()
解决1:使用lua脚本
-- 如果加锁成功
if redis.call("SETNX", "lock", "1") == 1 then
-- 设置过期时间10秒
local expireResult = redis.call("expire", "lock", "100")
-- 如果设置过期时间成功
if expireResult == 1 then
return "success"
else
return "expire failed"
end
else
return "setnx not null"
end
解决2:自2.6.12版本开始,Redis的SET 命令的行为可以通过一系列参数来修改, 可以代替SETNX 、 SETEX 和 PSETEX三个命令,直接使用set(key,value,NX,EX,timeout),同时加锁并设置超时时间
-- 设置锁及过期时间
eval "return redis.call('SET','lock','1','NX','PX','100')"
问题4: 如果锁过期了,但是没执行完怎么办?
解决:如果过期没执行完会导致同一时间多个客户端操作库存,导致超卖,这里通过锁续期来解决
-- ARGV[1] 是可传入的参数变量,表示持有锁的系统的唯一值,也就是只有持有锁的客户端才能刷新 key 的超时时间。
if redis.call("get", "lock") == ARGV[1]
then
return redis.call("expire", "lock", "10")
else
return 0
end
问题5: 如果锁释放错了怎么办?
每个服务设置key时带上自己服务的唯一标识,如UUID,这样每个服务在删除锁前,先比较value值,值相同在进行删除操作
问题6: 如果Redis单例挂掉,分布式锁没法用怎么办?
解决:组建Redis主从集群,实现高可用
问题7: Redis主从集群数据同步延迟,存在问题主机上的Redis已经建好了锁,但是锁还未同步到从机上,主机宕机了,这个时候从机升级为主机,锁丢了。
解决: 使用RedLock,取消slave节点,不需要从库和哨兵,由此解决数据同步丢失锁的问题
实现原理:设有5个节点
1、客户端获取当前时间戳startTime
2、客户端分别向5个节点申请加锁
3、单个节点加锁超时则立即向下一个节点申请加锁
4、加锁成功:如果最后有3个及以上节点加锁成功,则获取时间戳endTime,如果endTime-startTime<锁过期时间则成功加锁
5、加锁失败:如果少于3个节点加锁成功则通知所有节点释放锁
问题8:
1、要实现RedLock,官方推荐最少5个实例,成本太高
2、同时整个流程下来锁太重
3、分布式系统中的NPC问题
-
N:Network Delay,网络延迟
-
P:Process Pause,进程暂停(GC)
-
C:Clock Drift,时钟漂移
解决:相比下来主从切换概率小,兜底操作性价比高,问题不大,而且发生NPC也需要兜底操作
参考博客: