一款基于Goland语言实现的Redis分布式锁产品,支持百万级实例/协程并发,适用于各种常见的分布式场景。
目前业务中分布式锁场景依据任务对象所需的Occupied Time可分为两种:短任务类型、长任务类型。
长任务类型
任务A需要在很长的一段时间占有锁,这个时间未知,直至任务A结束,甚至特殊情况下A的周期为业务的全生命周期,才能释放锁,再供任务A/B/C/D/争抢;
短任务类型
任务A在极短的时间内可完成,可在已知的时间阈值内,释放锁,再供任务A/B/C/D/争抢;
实质上
两种类型都是对分布式场景下公共资源的一致性保证;
功能上
长任务类型更倾向于实现某单实例的动态切换,如解决实例单点问题等;
短任务类型更倾向于对时刻高并发的限制,如短时间内的流量控制等
Redisgo_task两种任务类型都支持。
基于Redis SetNx()方法进行封装,创建子协程监听任务执行状态,任务执行中频次拉取Luck的Expire,当Expire在配置的阈值范围内,持续增加Expire,从而确保Luck在任务进行中不会过期。
lock.Conn.Do("SET", lock.Key, lock.Token, "EX", int(lock.TimeOut), "NX")
当持续增加任务中所属Lock中的Expire时,设计阈值是为了保证持有的Lock Key Expire始终在可控范围内的同时,更好的便于Expire粒度控制
由doneCh <-chan struct{} Channel阻塞控制,在Unlock()中会出发Close()信号
通过Ticker,持续进行Expire Add操作,可有效避免阻塞及单次Expire Add失败的场景,且有效验证当前锁状态,及时Stop
重试机制
1、瞬时重试;2、等间隔重试;3、随机重试;4、队列重试
经过考量,网络抖动下,瞬时、等间隔,可能导致重试成功率低,且DDOS现象;而队列重试在当前产品场景不适用;参考TCP重试机制,故设计采用二进制指数退避算法作为重试机制;
采用TCP超时重试等同的二进制指数退避算法,解决在持续重试获取锁场景下,对服务造成的瞬时负载高及网络抖动导致重试成功率底下的问题。
dir:config/redisgo_task.toml
[redis]
#expire-锁默认expire「单位s」
#cron 持续增加expire频次
#retries_count 持续获取锁重试次数,重试间隔遵循退避算法,于次数耦合
#monitor_try_all 标识是否永久重试抢锁,true/false
host = "IP:HOST"
key = "Redisgo_Task_Lock_key"
monitor_try_all = true
#retries_count = 4.0
# value = "8292884c-a7a7-0050-9778-e47362a8f578"
# expire = "5s"
# cron = "1s"
type RedisLock struct {
Host string
Expire time.Duration
Key string
Value string
Conn redis.Conn
Cron time.Duration
DoneExpireChan chan struct{}
Retry *Retry
}
考量到产品架构在实用中的健壮性,针对产品的整体架构设计,对实现过程做出了一下方向的调整:
对功能实现过程依赖的参数,及功能函数进行封装成不同程度的Struct、Interface,方便后期功能扩展
对产品功能涉及到的主要环节阈值拆分,抽象为依赖,支持外部配置化,且作为唯一入口,依据实际场景调整,保证配置的灵活性及功能最细粒度的控制
在产品功能实现过程中,添加完整的日志输出,确保逻辑的清晰可读,降低产品的上手难度
在数据量、实例数量两个维度验证:在高并发场景下每个实例获得锁的成功率一致;
实验分为三组,分别为样本一、样本二、样本三如下图:
样本一中数据规模每组在10+,较少,两组成功率相差4.2%,无法体现在双实例下,每个实例成功率一致的目标;
样本二中数据规模每组在3w+,尚可,三组成功率均差在0.2%,已经十分接近目标;
样本三中数据规模每组在1w+,尚可,四组成功率均差在0.0025%,可以验证并发场景下,实例强锁成功率均等;
1、任务执行时长未知,如何保证任务期间,持续占有锁/?
任务占有锁,会启子协程频次监听锁TTL,在可控粒度下,持续保障锁的Expire延长更新。
2、主任务结束,如何终止AddExpireTime子协程终止/?
关联Goland中协程通信,项目实现中采用Channel/Close()方案实现。
Of crouse,ctx context/Done方案也可行。
3、当并发场景下,会不会出现任务A占有锁的同时,Expire时间到期,锁被任务B占用/?
较低的概率会出现这种问题。
当默认Expire时间内ReNewExpire策略无数次失败,才会导致锁到期自动释放,被其他任务占用。
4、如何解决1中,任务A假锁,任务B真锁,A执行结束又将Key删除,破坏任务B的问题/?
在Unlock()实现中,做del操作之前会进行Value的校验,匹配时进行del操作,且通过Lua脚本保证原子性。
5、AddExpireTime逻辑在短类型场景中,是否没必要存在/?
AddExpireTime逻辑是为兼容长类型场景设计,在短类型场景中不影响业务逻辑正常进行。
./redisgo_task task --config config/redisgo_task.toml
Consul分布式中Luck()/Unluck()实现原理
Redis大型网站高并发场景下分布式锁实现原理
重试设计之二进制指数退避机制
感兴趣的各位可以打星交流!!!