美文网首页
redisson分布式锁实现思路

redisson分布式锁实现思路

作者: 小怪兽狂殴奥特曼 | 来源:发表于2021-03-11 11:53 被阅读0次

    分布式锁要满足四个基本的特点:
    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;
    }
    

    相关文章

      网友评论

          本文标题:redisson分布式锁实现思路

          本文链接:https://www.haomeiwen.com/subject/klvnqltx.html