redis分布式锁


1. redis分布式锁的原理

Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。如果设置成功,返回 1 。 设置失败,返回 0 。

基于上面这个命令,我们可以用于设计分布式锁。对于多个分布式节点,通过对同一个key执行setnx命令,如果某个节点执行setnx命令成功,那么该节点获取到分布式锁,其他节点因为该key已经存在了,所以执行setnx会失败,也就是获取锁失败,需要等待获取到锁的节点释放,才能获取到锁;

2. 上锁

利用setnx命令能够方便的判定,当前节点是否能够获取到锁;但是为了避免该节点在获取到锁之后,因为各种异常,导致该分布式锁没有释放(比如程序崩溃了),所以一般会给该key加个过期时间,这样即便获取到锁的节点崩溃了,其他节点也会在key过期之后,也能够有机会获取到锁;

所有一般会使用SET key value PX milliseconds NX 命令

127.0.0.1:6379> set test_key 11 PX 100000 NX
OK
127.0.0.1:6379> get test_key
"11"
127.0.0.1:6379> ttl test_key
(integer) 92
127.0.0.1:6379> pttl test_key
(integer) 87046

3. 解锁

解锁的话,最直接的想法就是,直接del相应的key;但是这有很大的问题,列个简单的场景

节点A获取到锁,但是因为获取到锁之后执行的逻辑很很耗时间,导致该锁的key过期了;这个时候另外一个节点B获取到锁了,然后开始处理逻辑,在B释放锁之前,A先执行完逻辑,要释放锁了,直接del key;这个时候另外一个节点C获取到锁,然后节点B和节点C就存在并发问题了

所以一般会在上锁的时候,将key的值,设置相应请求者id(requestor_id, requestor_id 可以是即时生成的uuid或者就是一个很大的随机数),然后解锁的时候,先判断下该key的值,还是不是requestor_id,如果还是的话,说明该锁还是被该节点拥有,可以执行解锁操作;否则,说明该锁已经被其他节点所获取,不能再执行解锁操作了;
伪代码如下

if redis_client.get("lock_key") == requestor_id then
    redis_client.del("lock_key")
end

但是先get然后del毕竟是两次请求,没法保证其原子性;幸好redis支持通过eval命令执行lua脚本

127.0.0.1:6379> eval "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
 1 test_key 1111
(integer) 1

在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令(其实就是因为redis是单线程的,命令都是串行执行的,整个lua脚本的执行被看成是单个命令执行,所以lua脚本内部的多个redis命令的执行肯定是顺序执行的)

4. 代码示例

代码示例使用的redis_client是笔者自己写的c++版本的redis_client redis_cpp

redis_lock

/**
 *
 * redis_lock.hpp
 *
 * the distribute lock implement by redis
 *
 * @author  :   yandaren1220@126.com
 * @date    :   2018-12-28
 */

#ifndef __ydk_rediscpp_redis_utils_redis_lock_hpp
#define __ydk_rediscpp_redis_utils_redis_lock_hpp

#include <cstdint>
#include <string>
#include <chrono>
#include <thread>
#include <redis_cpp/detail/redis_reply_util.hpp>
#include <redis_cpp/detail/sync/redis_sync_operator.hpp>

namespace redis_cpp
{
namespace utils {

// redis distribute lock
class redis_lock
{
protected:
    enum {
        try_lock_interval = 100,        // in milliseconds
    };
protected:
    detail::redis_sync_operator*    m_redis_op;
    std::string                     m_lock_key;
    std::string                     m_lock_requestor_id;
    int32_t                         m_key_expired_time;
    int32_t                         m_locked_time_out;

public:
    /** 
     * @brief 
     * @param op        : redis_operator
     * @param lock_key  : the redis key
     * @param requestor_id : the lock requestor
     * @param key_expire_time : the key ttl in milliseconds
     * @param locked_time_out : try get the lock time out( in milliseconds)
     */
    redis_lock(detail::redis_sync_operator* op, 
        const std::string& lock_key, 
        const std::string& requestor_id, 
        int32_t key_expire_time, 
        int32_t locked_time_out)
        : m_redis_op(op)
        , m_lock_key(lock_key)
        , m_lock_requestor_id(requestor_id)
        , m_key_expired_time(key_expire_time)
        , m_locked_time_out(locked_time_out){
    }

    ~redis_lock() {
    }

    bool    lock() {
        int32_t timeout = m_locked_time_out;
        while (timeout >= 0) {

            // get lock
            if (m_redis_op->setnxpx(m_lock_key.c_str(), m_lock_requestor_id, m_key_expired_time)) {
                return true;
            }

            // sleep for a while
            std::this_thread::sleep_for(std::chrono::milliseconds(try_lock_interval));
            timeout -= try_lock_interval;
        }

        return false;
    }

    bool    unlock() {
        std::string del_lock_script = 
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

        std::vector<std::string> keys;
        std::vector<std::string> args;

        keys.push_back(m_lock_key);
        args.push_back(m_lock_requestor_id);
        auto reply  = m_redis_op->eval(del_lock_script.c_str(), keys, args);
        return reply && reply->to_integer() == 1;
    }
};


// redis distribute auto lock
class redis_auto_lock
{
protected:
    bool        m_locked;   
    redis_lock  m_locker;
public:
    redis_auto_lock(detail::redis_sync_operator* op,
        const std::string& lock_key,
        const std::string& requestor_id,
        int32_t key_expire_time,
        int32_t locked_time_out)
            : m_locked(false)
            , m_locker(op, lock_key, requestor_id, key_expire_time, locked_time_out){

        m_locked = m_locker.lock();
    }

    ~redis_auto_lock() {
        if (m_locked) {
            m_locker.unlock();
            m_locked = false;
        }
    }

    bool    islocked() {
        return m_locked;
    }
};

}
}

#endif

test code

#include <iostream>
#include <strstream>
#include <vector>
#include <redis_cpp.hpp>
#include <utils/redis_lock.hpp>
#include <utility/asio_base/thread_pool.hpp>

static uint32_t get_cur_time() {
    return std::chrono::duration_cast<std::chrono::duration<uint32_t, std::milli>>(
        std::chrono::high_resolution_clock::now().time_since_epoch()).count();
}

void redis_lock_test() {
    using namespace redis_cpp;
    using namespace redis_cpp::detail;

    utility::asio_base::thread_pool pool(2);
    pool.start();

    std::string redis_uri = "redis://foobared@127.0.0.1:6379/0";

    standalone_sync_client_pool client_pool(redis_uri.c_str(), 1, 2, &pool);
    base_sync_client* sync_client = &client_pool;
    redis_sync_operator client(sync_client);

    while (true) {
        std::string lock_key;
        std::string requestor_id;
        int32_t     expired_time;
        int32_t     time_out;

        printf("input params:\n");

        std::cin >> lock_key >> requestor_id >> expired_time >> time_out;

        uint32_t start_time = get_cur_time();
        printf("try start get locker: %u\n", start_time);
        redis_cpp::utils::redis_auto_lock locker(&client, lock_key, requestor_id, expired_time, time_out);
        uint32_t end_time = get_cur_time();
        uint32_t cost = end_time - start_time;
        bool locked = locker.islocked();
        printf("time: %u, cost: %d, locked: %d\n", end_time, cost, locked);
    }
}

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