redisson分布式锁实现思路

分布式锁要满足四个基本的特点:
1.互斥性
2.加锁和解锁的人必须一致
3.不能发生死锁
4.容错性。

redisson中,通过检查key是否存在来保证唯一性。
同时加锁的时候,加锁的根据redisson客户端uuid+线程id组生成客户端唯一uuid,写入到哈希表中。
每个锁的名称就是key,给每个key设置ttl。一旦过期key就会被删除,保证不会发生死锁。

redisson巧妙的利用redis执行脚本的时候原子执行的特点,将加锁和解锁的过程封装在lua脚本中,实现加锁的原子性。
加锁的脚本:

--[[
/*
*key number:1
*KEYS[1]: 锁名
*ARGV[1]: 锁的存活期
*ARGV[2]: 锁id
*返回值: 0 - ok, >0 - fail
*/
]]--
-- 若锁不存在:则新增锁,并设置锁重入计数为1、设置锁过期时间\
if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hset', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
 
-- 若锁存在,且唯一标识也匹配:则表明当前加锁请求为锁重入请求,故锁重入计数+1,并再次设置锁过期时间
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
 
-- 若锁存在,但唯一标识不匹配:表明锁是被其他线程占用,当前线程无权解他人的锁,直接返回锁剩余过期时间
return redis.call('pttl', KEYS[1]);

加锁lua脚本的过程如下:


1.PNG

解锁lua脚本:

--[[
/*
*key number:2
*KEYS[1]: lock_name
*KEYS[2]: pub_chan_name,would be: hd_dlock_chan:${lock_name}
*ARGV[1]: publish content. 0 - unlock
*ARGV[2]: 锁的存活期
*ARGV[3]: 锁id
*/
]]--

-- 若锁不存在:则直接广播解锁消息,并返回1
if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('publish', KEYS[2], ARGV[1]);
    return 1;
end;
 
-- 若锁存在,但唯一标识不匹配:则表明锁被其他线程占用,当前线程不允许解锁其他线程持有的锁
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
    return nil;
end;

-- 若锁存在,且唯一标识匹配:则先将锁重入计数减1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
    -- 锁重入计数减1后还大于0:表明当前线程持有的锁还有重入,不能进行锁删除操作,但可以友好地帮忙设置下过期时期
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return 0;
else
    -- 锁重入计数已为0:间接表明锁已释放了。直接删除掉锁,并广播解锁消息,去唤醒那些争抢过锁但还处于阻塞中的线程
    redis.call('del', KEYS[1]);
    redis.call('publish', KEYS[2], ARGV[1]);
    return 1;
end;

解锁lua脚本过程如下:


2.PNG

redisson还支持递归锁。

一般情况,redis的分布式锁客户端上锁失败后,常用的做法是本地循环等待锁的释放。JAVA和C++都可以通过future来实现阻塞式的等待。
C++下用future实现的接口如下:


#include <uuid/uuid.h>
#include <unistd.h>
#include <functional>     // std::ref
#include <thread>         // std::thread
#include <future>         // std::promise, std::future

extern RedisProxy g_redis_proxy;

std::string get_sys_uuid()
{
    uuid_t uu;
    uuid_generate( uu );

    char buf[33]={0};
    for(int i=0;i<16;i++)
    {
        sprintf(buf+i*2,"%02x",uu[i]);
    }
    //printf("%s\n",buf);
    return string(buf);
}

std::string g_sys_uuid=get_sys_uuid();

/*
*key number:1
*KEYS[1]: lock_name
*ARGV[1]: lock ttl
*ARGV[2]: key id
*return value: 0 - ok, >0 - fail
*/
const std::string lua_lock_cript="\
-- 若锁不存在:则新增锁,并设置锁重入计数为1、设置锁过期时间\
if (redis.call('exists', KEYS[1]) == 0) then\
    redis.call('hset', KEYS[1], ARGV[2], 1);\
    redis.call('pexpire', KEYS[1], ARGV[1]);\
    return nil;\
end;\
 \
-- 若锁存在,且唯一标识也匹配:则表明当前加锁请求为锁重入请求,故锁重入计数+1,并再次设置锁过期时间\
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then\
    redis.call('hincrby', KEYS[1], ARGV[2], 1);\
    redis.call('pexpire', KEYS[1], ARGV[1]);\
    return nil;\
end;\
 \
-- 若锁存在,但唯一标识不匹配:表明锁是被其他线程占用,当前线程无权解他人的锁,直接返回锁剩余过期时间\
return redis.call('pttl', KEYS[1]);\
";

/*
*key number:2
*KEYS[1]: lock_name
*KEYS[2]: pub_chan_name,would be: hd_dlock_chan:${lock_name}
*ARGV[1]: publish content. 0 - unlock
*ARGV[2]: lock ttl
*ARGV[3]: key id
*/
const std::string lua_unlock_cript="\
-- 若锁不存在:则直接广播解锁消息,并返回1\
if (redis.call('exists', KEYS[1]) == 0) then\
    redis.call('publish', KEYS[2], ARGV[1]);\
    return 1;\
end;\
 \
-- 若锁存在,但唯一标识不匹配:则表明锁被其他线程占用,当前线程不允许解锁其他线程持有的锁\
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then\
    return nil;\
end;\
\
-- 若锁存在,且唯一标识匹配:则先将锁重入计数减1\
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);\
if (counter > 0) then\
    -- 锁重入计数减1后还大于0:表明当前线程持有的锁还有重入,不能进行锁删除操作,但可以友好地帮忙设置下过期时期\
    redis.call('pexpire', KEYS[1], ARGV[2]);\
    return 0;\
else\
    -- 锁重入计数已为0:间接表明锁已释放了。直接删除掉锁,并广播解锁消息,去唤醒那些争抢过锁但还处于阻塞中的线程\
    redis.call('del', KEYS[1]);\
    redis.call('publish', KEYS[2], ARGV[1]);\
    return 1;\
end;\
";

const std::string HD_DLOCK_PUB_CHAN_PRE="hd_dlock_chan:";

int dlock_subscribe(std::string chan)
{
    return g_redis_proxy.subscribe(chan);
}

int try_lock(std::string &lock_name, int ttl, int timeout)
{
    if(timeout<=0||ttl<0) {return -1;}
    uint64 howlong=0;

    do{
        uint64 t1=get_millisecond();
        std::string key_id=g_sys_uuid+to_string(getpid());
        int ret=g_redis_proxy.eval3(lua_lock_cript,1,lock_name,to_string(ttl),key_id);
        if(ret==0)
        {
            
            return 0;
        }
        uint64 t2=get_millisecond();
        howlong+=t2-t1;

        if(howlong>=timeout) {return -1;}
        
        std::chrono::milliseconds span(1000*(timeout-howlong));
        std::string pub_chan=HD_DLOCK_PUB_CHAN_PRE+lock_name;
        std::future<int> fut = std::async(dlock_subscribe, pub_chan);

        // timeout, no one release lock
        if(fut.wait_for(span) == std::future_status::timeout)
        {
            g_redis_proxy.unsubscribe(pub_chan);
            return -1;
        }
        //else someone release lock

        ret = fut.get(); //must be 0,because we send 0 only
        
        uint64 t3=get_millisecond();
        howlong+=t3-t2;
    }while(1);

}

int unlock(std::string &lock_name,int ttl)
{
    std::string key_id=g_sys_uuid+to_string(getpid());
    std::string pub_chan=HD_DLOCK_PUB_CHAN_PRE+lock_name;
    
    int ret=g_redis_proxy.eval5(lua_unlock_cript,2,lock_name,pub_chan,to_string(0),to_string(ttl),key_id);
    return ret;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容