澳门美高梅手机网站浅谈Redis分布式锁实现

法斯特DFS-Nginx扩大模块源码分析

在分布式系统当中, Redis锁是叁个很常用的工具. 举个很普遍的例子正是:
某些接口供给去查询数据库的多寡, 不过请求量却又非常的大,
所以大家一般会加一层缓存, 并且设定过期时间.
可是此地存在一个标题便是当并发量极大的情况下, 在缓存过期的眨眼间间,
会有雅量的呼吁穿透去数据库请求数据, 造成缓存雪崩效应.
这时候要是有锁的体制, 那么就能够操纵单个请求去立异缓存.

1. 背景

在超越二分之一业务场景中,往往须要为法斯特DFS存储的公文提供http下载服务,而即便法斯特DFS在其storage及tracker都放置了http服务,
但品质表现却适得其反;
作者余庆在新生的版本中追加了依据当前主流web服务器的恢弘模块(包蕴nginx/apache),其用旨在于应用web服务器直接对本机storage数据文件提供http服务,以升高公文下载的天性。

 

实在对于Redis锁的见识, 网上早已有很多了, 只是超过55%都以基于Java来促成的,
那里给出一个PHP完结的版本. 那里考虑的只是单机安顿Redis的景况,
相对会不难好通晓, 而且也愈发的实用. 借使有分布式Redis安顿的情事,
能够参见下Redlock算法的达成.

2. 轮廓介绍

有关法斯特DFS的架构原理不再赘述,有趣味能够参见:http://code.google.com/p/fastdfs/wiki/Overview 

 

2.1 参考架构

应用FastDFS整合Nginx的参阅架构如下所示

澳门美高梅手机网站 1

证实:
在每一台storage服务器主机上配置Nginx及法斯特DFS扩张模块,由Nginx模块对storage存款和储蓄的公文提供http下载服务,
仅当当前storage节点找不到文件时会向源storage主机发起redirect或proxy动作。 
注:图中的tracker只怕为多个tracker组成的集群;且当前FastDFS的Nginx扩充模块帮忙单机多少个group的图景

 

 

2.2 多少个概念

storage_id:指storage
server的id,从法斯特DFS4.x版本起始,tracker能够对storage定义一组ip到id的照射,以id的款式对storage进行田管。而文件名写入的不再是storage的ip而是id,这样的方式对于数据迁移12分便利。 
storage_sync_file_max_delay:指storage节点同步3个文书最大的光阴推移,是二个阈值;借使当明天子与文件创立时间的反差超越该值则认为同步已经到位。 
anti_steal_token:指文件ID防盗链的不二法门,法斯特DFS接纳token认证的情势开展文件防盗链检查。

 

核心须求

3. 落到实处原理

贯彻3个分布式锁定, 大家足足要考虑它能满足一下的这么些供给:

 3.1 源码包表明

下载后的源码包相当小,仅包蕴以下文件:

 ngx_http_fastdfs_module.c   //nginx-module接口实现文件,用于接入fastdfs-module核心模块逻辑
 common.c                    //fastdfs-module核心模块,实现了初始化、文件下载的主要逻辑
 common.h                    //对应于common.c的头文件
 config                      //编译模块所用的配置,里面定义了一些重要的常量,如扩展配置文件路径、文件下载chunk大小
 mod_fastdfs.conf            //扩展配置文件的demo

 

  • 互斥, 正是要在其余的随时, 同1个锁只好够有2个客户端用户锁定.

  • 不会死锁, 即使持有锁的客户端在具备时期崩溃了,
    然而也不会潜移默化一连的客户端加锁

  • 什么人加锁什么人解锁, 很好明白, 加锁和平化解锁的总得是同壹个客户端

3.2 初始化

澳门美高梅手机网站 2

 

3.2.1 加载配置文件

指标文件:/etc/fdfs/mod_fastdfs.conf

 

3.2.2 读取增加模块配置

有些关键参数包涵:

      group_count           //group个数
      url_have_group_name   //url中是否包含group
      group.store_path      //group对应的存储路径
      connect_timeout       //连接超时
      network_timeout       //接收或发送超时
      storage_server_port   //storage_server端口,用于在找不到文件情况下连接源storage下载文件(该做法已过时)
      response_mode         //响应模式,proxy或redirect
      load_fdfs_parameters_from_tracker //是否从tracker下载服务端配置

加锁

3.2.3 加载服务端配置

根据load_fdfs_parameters_from_tracker参数分明是还是不是从tracker获取server端的布局音讯

  • load_fdfs_parameters_from_tracker=true:
  1. 调用fdfs_load_tracker_group_ex解析tracker连接配置 ;
  2. 调用fdfs_get_ini_context_from_tracker连接tracker获取配置消息;
  3. 获取storage_sync_file_max_delay阈值
  4. 获取use_storage_id
  5. 如果use_storage_id为true,则连接tracker获取storage_ids映射表(调用方法:fdfs_get_storage_ids_from_tracker_group)
  • load_fdfs_parameters_from_tracker=false:
  1. 从mod_fastdfs.conf加载所需安顿:storage_sync_file_max_delay、use_storage_id;
  2. 如果use_storage_id为true,则根据storage_ids_filename获取storage_ids映射表(调用方法:fdfs_load_storage_ids_from_file)

 


笔者们这里运用的是Predis那一个那几个PHP的客户端, 别的客户端也是同理.
先来探望代码:

3.3 下载进程

澳门美高梅手机网站 3

 

3.3.1 解析访问路径

    得到group和file_id_without_group多少个参数;

 

class RedisTool {    
    const LOCK_SUCCESS = 'OK';    
    const IF_NOT_EXIST = 'NX';    
    const MILLISECONDS_EXPIRE_TIME = 'PX';    
    const RELEASE_SUCCESS = 1;    /**
     * 尝试获取锁
     * @param $redis       redis客户端
     * @param $key         锁
     * @param $requestId   请求id
     * @param $expireTime  过期时间
     * @return bool        是否获取成功
     */
    public static function tryGetLock($redis, $key, 
$requestId, $expireTime) {
        $result = $redis->set(
            $key, 
            $requestId, 
            self::MILLISECONDS_EXPIRE_TIME, 
            $expireTime, 
            self::IF_NOT_EXIST
        );        
        return self::LOCK_SUCCESS === (string)$result;
    }
}

 3.3.2 防盗链检查

  • 根据g_http_params.anti_steal_token布置(见http.conf文件),判断是或不是进行防盗链检查;
  • 使用token的章程贯彻防盗链,
    该办法供给下载地址带上token,且token具有时效性(由ts参数指明);

检查措施:

   md5(fileid_without_group + privKey + ts) = token; 同时ts没有超过ttl范围 (可参考JavaClient CommonProtocol)

调用方法:fdfs_http_check_token 
有关法斯特DFS的防盗链可参照: http://bbs.chinaunix.net/thread-1916999-1-1.html

 

 

3.3.3 获取文件元数据

依照文件ID 获取元数据音信, 包含:源storage
ip,文件路径、名称,大小
 
代码

    if ((result=fdfs_get_file_info_ex1(file_id, false, &file_info)) != 0)...

fdfs_get_file_info_ex1 的兑现中,存在一个取巧的逻辑: 
  当获得文件的ip段之后,依旧须求鲜明该段落是storage的id还是ip。 
代码

  fdfs_shared.func.c
  -> fdfs_get_server_id_type(ip_addr.s_addr) == FDFS_ID_TYPE_SERVER_ID
  ...
       if (id > 0 && id <= FDFS_MAX_SERVER_ID) {
          return FDFS_ID_TYPE_SERVER_ID;
       } else  {
         return FDFS_ID_TYPE_IP_ADDRESS;
       }

 

看清标准为ip段的整数值是或不是在 0 到
-> FDFS_MAX_SERVER_ID(见tracker_types.h)之间; 
其中FDFS_MAX_SERVER_ID = (1 << 24) –
1,该做法利用了ipv4地址的特色(由4\
柒个二进制位组成),即ipv4地址数值务必大于该阈值*

概念一些Redis的操作符作为常量, 加锁的代码其实很简短, 一行代码即可.
简单解释下那么些set方法的两个参数:

3.3.4 检查本麻芋果件是或不是存在

调用trunk_file_stat_ex1获得当半夏件音信,该措施将落到实处:

  1. 识别当前文件是trunkfile依然singlefile
  2. 取得文件句柄fd
  3. 假设文件是trunk格局则还要也将有关新闻(偏移量/长度)一并获取

代码

    if (bSameGroup)
    {
            FDFSTrunkHeader trunkHeader;
        if ((result=trunk_file_stat_ex1(pStorePaths, store_path_index, \
            true_filename, filename_len, &file_stat, \
            &trunkInfo, &trunkHeader, &fd)) != 0)
        {
            bFileExists = false;
        }
        else
        {
            bFileExists = true;
        }
    }
    else
    {
        bFileExists = false;
        memset(&trunkInfo, 0, sizeof(trunkInfo));
    }
  • 首先个key是锁的名字, 那一个由具体育工作作逻辑控制, 保险唯一即可

  • 其次个是伸手ID, 大概倒霉明白.
    那样做的目标根本是为着保障加解锁的唯一性.
    那样我们就足以知晓该锁是哪位客户端加的.

  • 其多个参数是二个标识符, 标识时间戳以皮秒为最小单位

  • 切实的逾期时间

  • 本条参数是NX, 表示当key不存在时大家才实行set操作

3.3.5 文件不存在的处理

  • 拓展有效检查

反省项有二:

A. 源storage是本机或然当前时间与文件创立时间的距离一度超越阈值,报错;

代码

     if (is_local_host_ip(file_info.source_ip_addr) || \
        (file_info.create_timestamp > 0 && (time(NULL) - \
            file_info.create_timestamp > '''storage_sync_file_max_delay''')))

 

B. 若是是redirect后的现象,同样报错;

就算是由别的storage节点redirect过来的请求,其url参数中会存在redirect一项

在通过有效检查之后将展开代理或重定向处理

  • 重定向情势

配置项response_mode = redirect,此时服务端再次回到重回302响应码,url如下:

http:// {源storage地址} : {当前port} {当前url} {参数"redirect=1"}(标记已重定向过)

 

代码

      response.redirect_url_len = snprintf( \
                response.redirect_url, \
                sizeof(response.redirect_url), \
                "http://%s%s%s%s%c%s", \
                file_info.source_ip_addr, port_part, \
                path_split_str, url, \
                param_split_char, "redirect=1");

 

注:该格局下需求源storage配备公开访问的webserver、同样的端口(一般是80)、同样的path配置。

  • 代理格局

配置项response_mode =
proxy,该方式的干活原理就好像反向代理的做法,而单纯使用源storage地址作为代理proxy的host,其他部分保持不变。 
代码

       if (pContext->proxy_handler != NULL)
        {
            return pContext->proxy_handler(pContext->arg, \
                    file_info.source_ip_addr);
        }
        //其中proxy_handler方法来自ngx_http_fastdfs_module.c文件的ngx_http_fastdfs_proxy_handler方法
        //其实现中设置了大量回调、变量,并最终调用代理请求方法,返回结果:
        rc = ngx_http_read_client_request_body(r, ngx_http_upstream_init);  //执行代理请求,并返回结果

PS. 请求的唯一性ID生成格局很多, 能够参考下那么些chronos.
该库涉及到Thrift的EvoquePC调用, 恐怕上手会比较劳顿,
下回给出3个简短的PHP达成.

3.3.6 输出本守田件

当本半夏件存在时,将一贯出口。

  • 依照是不是trunkfile得到文件名,文件名长度、文件offset;

代码

    bTrunkFile = IS_TRUNK_FILE_BY_ID(trunkInfo);
    if (bTrunkFile)
    {
        trunk_get_full_filename_ex(pStorePaths, &trunkInfo, \
                full_filename, sizeof(full_filename));
        full_filename_len = strlen(full_filename);
        file_offset = TRUNK_FILE_START_OFFSET(trunkInfo) + \
                pContext->range.start;
    }
    else
    {
        full_filename_len = snprintf(full_filename, \
                sizeof(full_filename), "%s/data/%s", \
                pStorePaths->paths[store_path_index], \
                true_filename);
        file_offset = pContext->range.start;
    }

 

  • 若nginx开启了send_file开关而且如今为非chunkFile的状态下品尝运用sendfile方法以优化品质;

代码

    if (pContext->send_file != NULL && !bTrunkFile)
    {
        http_status = pContext->if_range ? \
                HTTP_PARTIAL_CONTENT : HTTP_OK;
        OUTPUT_HEADERS(pContext, (&response), http_status)
        ......
        return pContext->send_file(pContext->arg, full_filename, \
                full_filename_len, file_offset, download_bytes);
    }

 

  • 不然使用lseek 情势随机走访文件,并出口相应的段;

做法:使用chunk形式循环读,输出… 
代码

    while (remain_bytes > 0)
    {
        read_bytes = remain_bytes <= FDFS_OUTPUT_CHUNK_SIZE ? \
                 remain_bytes : FDFS_OUTPUT_CHUNK_SIZE;
        if (read(fd, file_trunk_buff, read_bytes) != read_bytes)
        {
            close(fd);
            ......
            return HTTP_INTERNAL_SERVER_ERROR;
        }

        remain_bytes -= read_bytes;
        if (pContext->send_reply_chunk(pContext->arg, \
            (remain_bytes == 0) ? 1: 0, file_trunk_buff, \
            read_bytes) != 0)
        {
            close(fd);
            return HTTP_INTERNAL_SERVER_ERROR;
        }
    }

 

其间chunk大小见config文件配置: -DFDFS_OUTPUT_CHUNK_SIZE=’256*1024′

简言之表达下方面包车型客车那段代码, 设置NX保险了只好有2个客户端获取到锁,
知足互斥性; 参加了晚点时间, 保险在客户端崩溃后不会导致死锁;
请求ID的效益是用来标识客户端,
那样客户端在解锁的时候能够展开校验是或不是同二个客户端.

 

 

4. 增加阅读

基于Referer达成防盗链: 
http://www.cnblogs.com/wJiang/archive/2010/04/04/1704445.html

FastDFS使用FAQ: 
http://bbs.chinaunix.net/thread-1920470-1-1.html

法斯特DFS-Nginx扩大的安顿参考: 
http://blog.csdn.net/poechant/article/details/7036594

法斯特DFS配置、安顿资料整理-CSDN博客: 
http://blog.csdn.net/poechant/article/details/6996047

关于C语言open和fopen区别 
http://blog.csdn.net/hairetz/article/details/4150193

解锁

当锁拥有的客户端实现了对共享能源的操作后, 释放锁要求用到Lua脚本,
也很简短:

 

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

 

PHP代码:

class RedisTool {    
    const RELEASE_SUCCESS = 1;   
    public static function releaseLock($redis, $key, $requestId) {
        $lua = "if redis.call('get', KEYS[1]) == ARGV[1] then 
                return redis.call('del', KEYS[1]) 
            else 
                return 0 
            end";
        $result = $redis->eval($lua, 1, $key, $requestId);
        return self::RELEASE_SUCCESS === $result;
    }
}

没悟出一个简约的解锁操作也要用到Lua脚本,
待会会说说普遍的几种错误解锁的情势. 其实为啥要用Lua脚本来达成,
首尽管为着确认保障原子性. Redis的eval能够确定保证原子性,
首要依然源于Redis的表征, 能够看看官网的介绍

 

澳门美高梅手机网站 4

 

 

科学普及错误

  1. 荒唐加锁

 

public static function wrong1($redis, $key, 
$requestId, $expireTime) {
    $result = $redis->setnx($key, $requestId);    
    if ($result == 1) {        
        // 这里程序挂了或者expire操作失败,则无法设置过期时间,将发生死锁
        $redis->expire($key, $expireTime);
    }
}

 

那是相比广泛的一种错误完结, 先通过setnx加锁,
然后在通过expire设置过期时间. 这样乍一看和地点的不都平等吧? 其实不然,
这是两条Redis命令, 不抱有原子性, 假若在setnx之后先后挂了,
会使得锁没有安装过期时间, 那样就会时有爆发死锁定.

 

  1. 不当加锁

    public static function wrong2($redis, $key, expireTime) {
       $expires = floor(microtime(true) 一千) + $expireTime;        
       // 若是当前锁不设有,再次来到加锁成功
       if ($redis->setnx($key, $expires) == 1) {        
           return true;
       }    
       //借使锁存在,获取锁的过期时间
       $currentValue = floor($redis->get($key));    
       if ($currentValue != null &&
           $currentValue < floor(microtime(true)
    一千)) {        
           // 锁已过期,获取上三个锁的晚点时间,并设置以后锁的超时时间
           $oldValue = floor($redis->getSet($key, $expires));        
           if ($oldValue != null && $oldValue === $currentValue) {            
               // 考虑并发的情形,唯有设置值和近日值相同,它才有职责加锁
               return true;
           }
       }    
       // 其余情况,一律重返加锁失利    return false;
    }

以此事例落成原理是接纳setnx来加锁,
假诺锁已经存在的话则收获锁的过期时间还要与如今的年月比较,
过期则设置新的岁月, 并且返回加锁成功. 尽管那样也能够加锁,
可是会存在几个难题:

  • 因为时间是客户端生成的,
    这样就须要求保险在分布式环境下客户端的光阴必供给一并

  • 当锁过期后, 多少个客户端同时施行getSet方法, 即使能够保障互斥性,
    只适合那么些锁的逾期时间在高并发可能八线程的景色下有一定的大概被其余客户端给覆盖

  • 锁没有客户端的标识, 那样任何1个客户端都能够解锁

 

  1. 错误解锁

    public static function wrongRelease1($redis, $key) {
       $redis->del([$key]);
    }

这是最特异的错误了, 那样的做法没看清锁的拥有者,
会使得其余三个客户端都足以解锁, 甚至会把人家的锁给解决了.

 

  1. 不当解锁

    public static function wrongRelease2($redis, $key, $requestId) {        
       // 判断加锁与解锁是否同叁个客户端
       if ($requestId === $redis->get($key)) {            
           // 若在那儿,那把锁突然不是那些客户端的,则会误解锁
           $redis->del([$key]);
       }
    }

上面包车型客车解锁也是平昔不保证原子性, 注释说的很明亮了,
有这样的光景来复现: 客户端A加锁成功后一段时间再来解锁,
在进行删除del操作的时候锁过期了,
而且那时候又有别的客户端B来加锁(那时候加锁是放任自流成功的,
因为客户端A的锁过期了), 那是客户端A再举行删除del操作,
会把客户端B的锁给清了.

 

 

总结

诸如此类就基本上完成了3个简练的基于Redis的分布式锁.
其实分布式锁的落到实处远比想象的复杂, 越发是在多机计划Redis的景观下.
当然实现的方式也不光囊括Redis, 还是能够用Zookeeper来落成.
随着对分布式系统的深远精晓, 能够再来逐步地考虑那个问题.

发表评论

电子邮件地址不会被公开。 必填项已用*标注