RedisModule剖析 - RedLock

RedLock 是一款基于 RedisModule 实现的分布式锁模块,该模块提供了写/读锁的操作接口,相比于使用 Redis 的现有命令进行支持,这个模块的命令语义更明确,且有利于进行流量区分与筛选,在使用中能够很好的区分开其他流量与锁操作相关的流量。整体的模块实现比较简单,阅读相对比较容易。

一、简介

二、架构设计

2.1、相关命令

  • slock.lock : 新增加一个写锁,可以指定该锁的过期时间;
  • slock.unlock : 解锁之前的一个写锁,只有具有相同的客户端id的链接才可以解锁;
  • slock.rlock : 新增加一个读锁,可以指定该锁的过期时间,多个客户端同时获取写锁时会影响该锁的读的引用计数;
  • slock.runlock : 解锁之前的一个读锁,实际是减少对应锁的引用计数,当引用计数减少为0时,删除该key;
  • slock.info : 获取锁的信息;

2.2、数据结构

// RedLock结构体
typedef struct slock {
mstime_t lock_time; // 上锁的时间,单位毫秒
int is_write; // 当前锁的特征,读锁 或 写锁
unsigned long long reader_count; // 读锁的引用计数,只有当锁是读锁时才会用到
unsigned long long write_client_id; // 上锁的客户端id
} SLock;

2.3、持久化

2.3.1、RDB的持久化

RDB 的存储过程比较简单,直接把对应结构体的所有信息持久化到 RDB 文件中。

void SLockRdbSave(RedisModuleIO *rdb, void *value) {
SLock *sl = value;
RedisModule_SaveUnsigned(rdb, sl->write_client_id);
RedisModule_SaveSigned(rdb, sl->lock_time);
RedisModule_SaveSigned(rdb, sl->is_write);
RedisModule_SaveUnsigned(rdb, sl->reader_count);
}

2.3.2、AOF的持久化

AOF 的存储过程相当于把现有的锁转换成 slock.lockslock.rlock 命令进行存储。(注意:转储的时候没有记录对应锁的过期时间,重新加载的所有锁都不会过期,算是一个异常);

void SLockAofRewrite(RedisModuleIO *aof, RedisModuleString *key, void *value) {
SLock *sl = value;
if (sl->is_write) {
RedisModule_EmitAOF(aof, "SLOCK.LOCK", "s", key);
return;
}

unsigned long long count = sl->reader_count;
while (count--){
RedisModule_EmitAOF(aof, "SLOCK.rLOCK", "s", key);
}
}

三、问题与思考

3.1、问题

  • 写锁的释放方式依赖于加锁的客户端释放或者到达过期时间,但是如果加锁的客户端异常断开之后,重新连接之后新客户端的id与加锁的客户端id也不同了,这时就只能等待对应锁过期释放,这里关于加锁与释放锁的关系不可靠,极容易出现问题;
  • AOF 的持久化过程中没有覆盖过期属性,结合上一个问题,服务重启之后锁永远都不会被释放;

3.2、思考

  • 该锁模块没有封装更为高级的接口或命令,相比于Redis现有的命令,没有特别明显的优势,我们完全可以使用以下的命令来替换对应该模块中的命令,在替换的过程中由于读锁和写锁的 value 格式不同,可以通过设置不同的 key 的格式来进行区分。
    • slock.lock :
      • 思路 : 使用 string 类型数据替换即可;
      • 替换为 : set key_w value nx px expire
    • slock.unlock :
      • 思路 : 解锁之前需要判断加锁的归属,因此需要使用 lua 脚本;
      • 替换为 : eval "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 key_w value
    • slock.rlock :
      • 思路 : 保持多个客户端访问同一个读锁,又要确保能够表示引用计数,可以使用 string 数据类型并使用数字 value ;
      • 替换为 : incr key_r
    • slock.runlock : 替换为 decr key_r
    • slock.info : 替换为 get key_r
作者: bugwz
链接: https://bugwz.com/2021/09/01/redismodule-redlock/
声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 咕咕