前言
诚然Redis作为分布式锁具备争议,但是在项目实践上,出于易用性、可维护性上考虑,还是有许多场景使用redis作为分布式锁。
本文主要介绍Redis作为分布式锁的一些坑和进阶实践,依次拆分现存问题,并给予解决方式及实现代码;
全文实践基于golang进行实践演示(语言不是桎梏,思维是互通的),后文附项目链接
基础锁
大部分人使用redis作为分布式锁,都是直接setnx
操作,但是这里面就存在以下问题:
set
锁后,程序出现问题,导致长期未释放
- 执行时间长,redis锁超时释放后未自动续期
- 错误释放他人的锁,如果单纯依赖场景key,可能存在项目迭代过程中逻辑错误,导致释放同场景其他请求锁的情况,所以要增加请求的唯一标识(注意不是用户身份唯一标识)
对于上面存在的问题,首先要做保证程序或者机器发生故障后能自动释放锁,那么最好是给锁设置超时时间,而非单纯依赖程序去释放锁,其次在程序执行过程中需要保证锁存续。
具体实现代码如下(省略内部获取锁的过程,主要展示自动续期):
// 开启事务
func LockTransaction(key, reqID string, f func() error) (bool, error) {
lock := NewRLock(key, reqID, 10*time.Second)
result := lock.TryLock()
if !result {
log.Errorf("lock exist, key=%v, reqID=%v", key, reqID)
return false, nil
}
stop := make(chan bool, 1)
defer func() {
stop <- true
close(stop)
err := lock.UnLock()
if err != nil {
log.Errorf("unlock failed, key=%v, reqID=%v", key, reqID)
return
}
log.Infof("unlock succeed, key=%v, reqID=%v", key, reqID)
}()
// automatic renewal of additional maintenance lock
go renewlRLock(lock, stop, key, reqID)
return true, f()
}
// 自动续期锁
func renewlRLock(lock *RLock, stop chan bool, key, reqID string) {
ticker := time.NewTicker(5 * time.Second)
renewTime := 0
defer func() {
ticker.Stop()
if err := recover(); err != nil {
log.Errorf("renew RLock happened, key=%v, reqID=%v, err=%v", key, reqID, err)
}
}()
for {
select {
case <-ticker.C:
err := lock.RenewLock()
if err != nil {
log.Errorf("renew RLock failed, key=%v, reqID=%v, err=%v", key, reqID, err)
return
}
renewTime++
log.Infof("renew RLock succeed, key=%v, reqID=%v, renewTime=%v", key, reqID, renewTime)
case <-stop:
log.Infof("renew RLock over, key=%v, reqID=%v", key, reqID)
return
}
}
}
测试效果:
共计开启5个任务say hello
,每隔5秒启动一个,每个任务间隔5秒打印hello
具体执行时序:
测试代码:
func TestLockTransaction(t *testing.T) {
err := InitRedisLock("localhost:6379", "", 0, 10)
if err != nil {
t.Fatalf("init redis failed, err=%v", err)
}
wait := &sync.WaitGroup{}
for i := 0; i < 5; i++ {
wait.Add(1)
go SayHello(t, fmt.Sprint(i), wait)
time.Sleep(5 * time.Second)
}
}
func SayHello(t *testing.T, reqID string, wait *sync.WaitGroup) {
defer func() {
wait.Done()
}()
result, err := LockTransaction("say_hello", reqID, func() error {
for i := 0; i < 3; i++ {
time.Sleep(5 * time.Second)
t.Logf("hello reqID=%v", reqID)
}
return nil
})
if !result {
t.Logf("say hello fail, lock exist, reqID=%v", reqID)
return
}
if err != nil {
t.Logf("say hello fail, process err=%v, reqID=%v", err, reqID)
return
}
}
分段锁
如果一个资源都用一个锁来替换,那么很明显所有用户都要挤在一个通道,彼此竞争一个锁,所以我们可以将资源分段处理,让用户散列到多个区间,各自获取所属分段的资源
需要注意锁分段后,资源也要进行分段
可重入锁
对于一些特殊的业务场景,比如需要递归加锁的情况,那么需要考虑加入加锁层数,通过层数来判断来维持锁的存续,当层数为0的时候可以释放锁
需要注意的是层数的维护必须保证是原子性的,防止并发出现问题
一般分布式锁很少需要支持可重入的情况,建议在系统设计上避免可重入的情况
具体实现思路如下:
项目地址:
- github:github.com/Gnight-jump…
- gitee:gitee.com/g_night/go-…
欢迎讨论,提出改进意见