fix(lock script): fix bug of lock srcipt

1.fix bug of reset time wrong with ReentrantRLock in RLock script
2.fix bug of write lock failing to lock for the first time
3.fix bug of unlock failed with ReentrantWLock in UnWLock script

test(lock script): add new test of RLock and WLock

1.add refresh test of RLock
2.add new test of ReentrantWLock#
This commit is contained in:
douxu 2025-03-13 16:51:50 +08:00
parent d962462c42
commit 7b282c49f7
4 changed files with 202 additions and 36 deletions

View File

@ -11,6 +11,7 @@ const (
UnLockSuccess = RedisCode(1)
RefreshLockSuccess = RedisCode(1)
UnRLockSuccess = RedisCode(0)
UnWLockSuccess = RedisCode(0)
RLockFailureWithWLockOccupancy = RedisCode(-1)
UnRLockFailureWithWLockOccupancy = RedisCode(-2)
WLockFailureWithRLockOccupancy = RedisCode(-3)
@ -28,6 +29,8 @@ type RedisLockType int
const (
LockType = RedisLockType(iota)
UnRLockType
UnWLockType
UnLockType
RefreshLockType
)
@ -55,13 +58,17 @@ func NewRedisResult(res RedisCode, lockType RedisLockType, redisMsg string) erro
case 1:
if lockType == LockType {
return &RedisResult{Code: res, Message: "redis lock success"}
} else if lockType == UnLockType {
} else if (lockType == UnRLockType) || (lockType == UnWLockType) || (lockType == UnLockType) {
return &RedisResult{Code: res, Message: "redis unlock success"}
} else {
return &RedisResult{Code: res, Message: "redis refresh lock success"}
}
case 0:
return &RedisResult{Code: res, Message: "redis unlock read lock success, the lock is still occupied by other processes read lock"}
if lockType == UnRLockType {
return &RedisResult{Code: res, Message: "redis unlock read lock success, the lock is still occupied by other processes read lock"}
} else {
return &RedisResult{Code: res, Message: "redis unlock write lock success, the lock is still occupied by other processes write lock"}
}
case -1:
return &RedisResult{Code: res, Message: "redis lock read lock failure,the lock is already occupied by another processes write lock"}
case -2:
@ -97,13 +104,17 @@ func TranslateResultToStr(res RedisCode, lockType RedisLockType) string {
case 1:
if lockType == LockType {
return "redis lock success"
} else if lockType == UnLockType {
} else if (lockType == UnRLockType) || (lockType == UnWLockType) || (lockType == UnLockType) {
return "redis unlock success"
} else {
return "redis refresh lock success"
}
case 0:
return "redis unlock read lock success, the lock is still occupied by other processes read lock"
if lockType == UnRLockType {
return "redis unlock read lock success, the lock is still occupied by other processes read lock"
} else {
return "redis unlock write lock success, the lock is still occupied by other processes write lock"
}
case -1:
return "redis lock read lock failure,the lock is already occupied by another processes write lock"
case -2:

View File

@ -70,7 +70,7 @@ local mode = redis.call('hget', KEYS[1], 'mode');
if (mode == false) then
local writeWait = KEYS[1] .. ':write';
-- 优先写锁加锁,无写锁的情况通知读锁加锁
local counter = redis.call('llen',writeWait)
local counter = redis.call('llen',writeWait);
if (counter >= 1) then
redis.call('publish', KEYS[4], ARGV[1]);
end;
@ -86,6 +86,8 @@ if ((mode == 'read') and (lockExists == 0)) then
end;
local counter = redis.call('hincrby', KEYS[1], lockKey, -1);
local delTTLs = redis.call('httl', KEYS[1], 'fields', '1', lockKey);
local delTTL = tonumber(delTTLs[1]);
if (counter == 0) then
redis.call('hdel', KEYS[1], lockKey);
end;
@ -107,20 +109,24 @@ if (redis.call('hlen', KEYS[1]) > 1) then
until cursor == 0;
if (maxRemainTime > 0) then
local remainTime = redis.call('ttl', KEYS[1]);
redis.call('expire', KEYS[1], math.max(tonumber(remainTime),maxRemainTime));
if (delTTL > maxRemainTime) then
redis.call('expire', KEYS[1], maxRemainTime);
else
local remainTime = redis.call('ttl', KEYS[1]);
redis.call('expire', KEYS[1], math.max(tonumber(remainTime),maxRemainTime));
end;
end;
else
redis.call('del', KEYS[1]);
local writeWait = KEYS[1] .. ':write';
-- 优先写锁加锁,无写锁的情况通知读锁加锁
local counter = redis.call('llen',writeWait)
local counter = redis.call('llen',writeWait);
if (counter >= 1) then
redis.call('publish', KEYS[4], ARGV[1]);
else
redis.call('publish', KEYS[3], ARGV[1]);
end;
return 1;
return 1;
end;
`
@ -136,15 +142,18 @@ local mode = redis.call('hget', KEYS[1], 'mode');
local lockKey = KEYS[2] .. ':' .. ARGV[2];
local waitKey = KEYS[1] .. ':write';
if (mode == false) then
local firstToken = redis.call('lindex', waitKey,'0')
if (firstToken ~= ARGV[2]) then
return -7;
local waitListLen = redis.call('llen', waitKey);
if (waitListLen > 0) then
local firstToken = redis.call('lindex', waitKey,'0');
if (firstToken ~= ARGV[2]) then
return -7;
end;
end;
redis.call('hset', KEYS[1], 'mode', 'write');
redis.call('hset', KEYS[1], lockKey, 1);
redis.call('hexpire', KEYS[1], ARGV[1], 'fields', '1', lockKey);
redis.call('expire', KEYS[1], ARGV[1]);
redis.call('lpop', waitKey, '1')
redis.call('lpop', waitKey, '1');
return 1;
elseif (mode == 'read') then
-- 放到 list 中等待读锁释放后再次尝试加锁并且订阅读锁释放的消息
@ -152,8 +161,8 @@ elseif (mode == 'read') then
return -3;
else
-- 可重入写锁逻辑
local lockKey = KEYS[2] .. ':' .. ARGV[2]
local lockExists = redis.call('hexists', KEYS[1], lockKey)
local lockKey = KEYS[2] .. ':' .. ARGV[2];
local lockExists = redis.call('hexists', KEYS[1], lockKey);
if (lockExists == 1) then
redis.call('hincrby', KEYS[1], lockKey, 1);
redis.call('hexpire', KEYS[1], ARGV[1], 'fields', '1', lockKey);
@ -180,7 +189,7 @@ local mode = redis.call('hget', KEYS[1], 'mode');
local writeWait = KEYS[1] .. ':write';
if (mode == false) then
-- 优先写锁加锁,无写锁的情况通知读锁加锁
local counter = redis.call('llen',writeWait)
local counter = redis.call('llen',writeWait);
if (counter >= 1) then
redis.call('publish', KEYS[3], ARGV[1]);
end;
@ -188,23 +197,25 @@ if (mode == false) then
elseif (mode == 'read') then
return -5;
else
-- 可重入写锁逻辑
local lockKey = KEYS[2] .. ':' .. ARGV[2]
local lockExists = redis.call('hexists', KEYS[1], lockKey)
if (lockExists == 1) then
local lockKey = KEYS[2] .. ':' .. ARGV[2];
local lockExists = redis.call('hexists', KEYS[1], lockKey);
if (lockExists >= 1) then
-- 可重入写锁逻辑
local incrRes = redis.call('hincrby', KEYS[1], lockKey, -1);
if (incrRes == 0) then
redis.call('del', KEYS[1]);
local counter = redis.call('llen',writeWait)
local counter = redis.call('llen',writeWait);
if (counter >= 1) then
redis.call('publish', KEYS[4], ARGV[1]);
else
redis.call('publish', KEYS[3], ARGV[1]);
end;
return 1
return 1;
end;
return 0;
else
return -6;
end;
return -6;
end;
`
@ -216,9 +227,9 @@ ARGV[1]:锁的过期时间lockLeaseTime,单位为秒。
ARGV[2]:当前客户端的唯一标识token,用于区分不同的客户端
*/
var RefreshRWLockScript = `
local lockKey = KEYS[2] .. ':' .. ARGV[2]
local lockKey = KEYS[2] .. ':' .. ARGV[2];
local lockExists = redis.call('hexists', KEYS[1], lockKey);
local mode = redis.call('hget', KEYS[1], 'mode')
local mode = redis.call('hget', KEYS[1], 'mode');
local maxRemainTime = tonumber(ARGV[1]);
if (lockExists == 1) then
redis.call('hexpire', KEYS[1], ARGV[1], 'fields', '1', lockKey);

View File

@ -130,11 +130,11 @@ func (rl *RedissionRWLocker) UnRLock() error {
val, err := res.Int()
if err != redis.Nil && err != nil {
rl.logger.Info("unlock read lock failed", zap.String("token", rl.token), zap.String("key", rl.key), zap.Error(err))
return fmt.Errorf("unlock read lock failed:%w", constant.NewRedisResult(constant.UnknownInternalError, constant.UnLockType, err.Error()))
return fmt.Errorf("unlock read lock failed:%w", constant.NewRedisResult(constant.UnknownInternalError, constant.UnRLockType, err.Error()))
}
if (constant.RedisCode(val) == constant.UnLockSuccess) || (constant.RedisCode(val) == constant.UnRLockSuccess) {
if rl.needRefresh {
if rl.needRefresh && (constant.RedisCode(val) == constant.UnLockSuccess) {
rl.cancelRefreshLockTime()
}
@ -144,7 +144,7 @@ func (rl *RedissionRWLocker) UnRLock() error {
if constant.RedisCode(val) == constant.UnRLockFailureWithWLockOccupancy {
rl.logger.Info("unlock read lock failed", zap.String("token", rl.token), zap.String("key", rl.key))
return fmt.Errorf("unlock read lock failed:%w", constant.NewRedisResult(constant.UnRLockFailureWithWLockOccupancy, constant.UnLockType, ""))
return fmt.Errorf("unlock read lock failed:%w", constant.NewRedisResult(constant.UnRLockFailureWithWLockOccupancy, constant.UnRLockType, ""))
}
return nil
}
@ -221,11 +221,11 @@ func (rl *RedissionRWLocker) UnWLock() error {
val, err := res.Int()
if err != redis.Nil && err != nil {
rl.logger.Info("unlock write lock failed", zap.String("token", rl.token), zap.String("key", rl.key), zap.Error(err))
return fmt.Errorf("unlock write lock failed:%w", constant.NewRedisResult(constant.UnknownInternalError, constant.UnLockType, err.Error()))
return fmt.Errorf("unlock write lock failed:%w", constant.NewRedisResult(constant.UnknownInternalError, constant.UnWLockType, err.Error()))
}
if constant.RedisCode(val) == constant.UnLockSuccess {
if rl.needRefresh {
if (constant.RedisCode(val) == constant.UnLockSuccess) || constant.RedisCode(val) == constant.UnWLockSuccess {
if rl.needRefresh && (constant.RedisCode(val) == constant.UnLockSuccess) {
rl.cancelRefreshLockTime()
}
rl.logger.Info("unlock write lock success", zap.String("token", rl.token), zap.String("key", rl.key))
@ -234,7 +234,7 @@ func (rl *RedissionRWLocker) UnWLock() error {
if (constant.RedisCode(val) == constant.UnWLockFailureWithRLockOccupancy) || (constant.RedisCode(val) == constant.UnWLockFailureWithWLockOccupancy) {
rl.logger.Info("unlock write lock failed", zap.String("token", rl.token), zap.String("key", rl.key))
return fmt.Errorf("unlock write lock failed:%w", constant.NewRedisResult(constant.RedisCode(val), constant.UnLockType, ""))
return fmt.Errorf("unlock write lock failed:%w", constant.NewRedisResult(constant.RedisCode(val), constant.UnWLockType, ""))
}
return nil
}

View File

@ -49,12 +49,11 @@ func TestRWLockRLockAndUnRLock(t *testing.T) {
num, err = rdb.HGet(rwLocker.key, tokenKey).Int()
assert.Equal(t, redis.Nil, err)
assert.Equal(t, 0, num)
t.Log("test success")
t.Log("rwLock rlock and unrlock test success")
return
}
// TODO 实现可重入读锁测试
func TestRWLockReentrantLock(t *testing.T) {
func TestRWLockReentrantRLock(t *testing.T) {
rdb := redis.NewClient(&redis.Options{
Network: "tcp",
Addr: "192.168.2.103:6379",
@ -89,19 +88,164 @@ func TestRWLockReentrantLock(t *testing.T) {
assert.Equal(t, nil, err)
assert.Equal(t, 2, num)
// 第一次解读锁
err = rwLocker.UnRLock()
assert.Equal(t, nil, err)
num, err = rdb.HGet(rwLocker.key, tokenKey).Int()
assert.Equal(t, nil, err)
assert.Equal(t, 1, num)
// 第二次解读锁
err = rwLocker.UnRLock()
assert.Equal(t, nil, err)
num, err = rdb.HGet(rwLocker.key, tokenKey).Int()
assert.Equal(t, redis.Nil, err)
assert.Equal(t, 0, num)
t.Log("rwLock reentrant lock test success")
return
}
func TestRWLockRefreshRLock(t *testing.T) {
rdb := redis.NewClient(&redis.Options{
Network: "tcp",
Addr: "192.168.2.103:6379",
Password: "cnstar",
PoolSize: 50,
DialTimeout: 10 * time.Second,
})
rwLocker := GetRWLocker(rdb, &RedissionLockConfig{
LockLeaseTime: 10,
NeedRefresh: true,
Key: "component",
Token: "fd348a84-e07c-4a61-8c19-f753e6bc556a",
})
rwLocker.logger = log
duration := 10 * time.Second
// 第一次加读锁
err := rwLocker.RLock(duration)
assert.Equal(t, nil, err)
tokenKey := strings.Join([]string{rwLocker.rwTokenTimeoutPrefix, rwLocker.token}, ":")
num, err := rdb.HGet(rwLocker.key, tokenKey).Int()
assert.Equal(t, nil, err)
assert.Equal(t, 1, num)
time.Sleep(10 * time.Second)
script := `return redis.call('httl', KEYS[1], 'fields', '1', ARGV[1]);`
result, err := rdb.Eval(script, []string{rwLocker.key}, tokenKey).Result()
assert.Equal(t, nil, err)
ttls, ok := result.([]interface{})
assert.Equal(t, true, ok)
ttl, ok := ttls[0].(int64)
assert.Equal(t, true, ok)
compareValue := int64(8)
assert.Greater(t, ttl, compareValue)
err = rwLocker.UnRLock()
assert.Equal(t, nil, err)
num, err = rdb.HGet(rwLocker.key, tokenKey).Int()
assert.Equal(t, redis.Nil, err)
assert.Equal(t, 0, num)
t.Log("test success")
t.Log("rwLock refresh lock test success")
return
}
// TODO 设计两个客户端分别加读锁,测试是否可以加锁成功
// TODO 设计两个客户端分别加时间不同的读锁,测试ttl时间在有一个key删除后是否可以变换成功
func TestRWLockWLockAndUnWLock(t *testing.T) {
rdb := redis.NewClient(&redis.Options{
Network: "tcp",
Addr: "192.168.2.103:6379",
Password: "cnstar",
PoolSize: 50,
DialTimeout: 10 * time.Second,
})
rwLocker := GetRWLocker(rdb, &RedissionLockConfig{
LockLeaseTime: 120,
NeedRefresh: true,
Key: "component",
Token: "fd348a84-e07c-4a61-8c19-f753e6bc556a",
})
rwLocker.logger = log
duration := 10 * time.Second
// 第一次加读锁
err := rwLocker.WLock(duration)
assert.Equal(t, nil, err)
tokenKey := strings.Join([]string{rwLocker.rwTokenTimeoutPrefix, rwLocker.token}, ":")
num, err := rdb.HGet(rwLocker.key, tokenKey).Int()
assert.Equal(t, nil, err)
assert.Equal(t, 1, num)
err = rwLocker.UnWLock()
assert.Equal(t, nil, err)
num, err = rdb.HGet(rwLocker.key, tokenKey).Int()
assert.Equal(t, redis.Nil, err)
assert.Equal(t, 0, num)
t.Log("rwLock rlock and unrlock test success")
return
}
// TODO 完成写锁可重入测试
func TestRWLockReentrantWLock(t *testing.T) {
rdb := redis.NewClient(&redis.Options{
Network: "tcp",
Addr: "192.168.2.103:6379",
Password: "cnstar",
PoolSize: 50,
DialTimeout: 10 * time.Second,
})
rwLocker := GetRWLocker(rdb, &RedissionLockConfig{
LockLeaseTime: 120,
NeedRefresh: true,
Key: "component",
Token: "fd348a84-e07c-4a61-8c19-f753e6bc556a",
})
rwLocker.logger = log
duration := 10 * time.Second
// 第一次加写锁
err := rwLocker.WLock(duration)
assert.Equal(t, nil, err)
tokenKey := strings.Join([]string{rwLocker.rwTokenTimeoutPrefix, rwLocker.token}, ":")
num, err := rdb.HGet(rwLocker.key, tokenKey).Int()
assert.Equal(t, nil, err)
assert.Equal(t, 1, num)
// 第二次加写锁
err = rwLocker.WLock(duration)
assert.Equal(t, nil, err)
num, err = rdb.HGet(rwLocker.key, tokenKey).Int()
assert.Equal(t, nil, err)
assert.Equal(t, 2, num)
// 第一次解写锁
err = rwLocker.UnWLock()
assert.Equal(t, nil, err)
num, err = rdb.HGet(rwLocker.key, tokenKey).Int()
assert.Equal(t, nil, err)
assert.Equal(t, 1, num)
// 第二次解写锁
err = rwLocker.UnWLock()
assert.Equal(t, nil, err)
num, err = rdb.HGet(rwLocker.key, tokenKey).Int()
assert.Equal(t, redis.Nil, err)
assert.Equal(t, 0, num)
t.Log("rwLock reentrant lock test success")
return
}