分布式锁的技术选型及思考

锁和分布式锁

在计算机中,锁的作用是解决在并发状态下的共享资源互斥问题,保证在同一时间只有一个进程/线程可以掌握资源的控制权。

例如以下几种情况:

  1. 文件锁的实现是为了解决不同用户同时读写同一文件的并发问题而出现的,防止导致文件的内容被破坏。
  2. 使用数组实现的队列,在 push 操作的地方一般需要加锁来解决槽位的争夺问题,防止出现多次 push 冲突从而导致数据丢失问题。
  3. 对于12306来说,火车票就是他的资源,最终放票的时候需要锁来保证票、人、座位唯一对应。
  4. ……

上面的例子中其实就包含了我们通常讲的传统单机锁和我要讲的分布式锁。

单机环境下,资源竞争者都是来自机器内部((进程/线程),那么实现锁的方案只需要借助单机资源就可以了,比如借助磁盘、内存、寄存器来实现。

但是对于分布式环境下,资源竞争者生存环境更复杂了,原有依赖单机的方案不再发挥作用,这时候就需要一个大家都认可的协调者出来,帮助解决竞争问题,那这个协调者称之为分布式锁。

上面这个例子就像两个职员产生的矛盾,只要公司的领导出面就可以解决。而当两个公司产生竞争矛盾的时候,就需要司法机关出面,是同一个道理。

简单的说,分布式锁就是解决分布式环境下资源竞争问题的手段。

分布式锁的应用场景

所有分布式环境下会出现资源竞争的地方都需要分布式锁的协调,除了上面介绍的 12306 放票,还有类似共享文档平台编辑问题、王者荣耀选择英雄、全局自增主键等应用需要用到。简单介绍一下在类似公司内部 Wiki 等多人协作编辑平台的使用场景。

Wiki 中的多人在线编辑

场景1:清明节前,团队要求我们在 Wiki 登记自己的休假情况,假设我们在 id=1 这个文档上记录我们的休假时间和联系电话。A、C 两个同学同时开始编辑,并且 A 和 C 在同一时间提交了结果,他们在提交前文档是空的。服务需要如何处理这两个请求呢?以谁的为准呢?会不会产生覆盖现象导致 A 的记录丢失了?

场景2:另一个 case,我是 Z 同学,在我前面别人都已经填完了,我有一个陋习,喜欢在保存的时候连续按3-5下 Ctrl+s,而每一个 Ctrl+s 都会触发一个请求,但是每个请求处理大概1s钟,但是实际请求都在 20ms 内发出去了。

问题同上面,如何保证不重复的追加记录呢?

假设你的存储服务和存储架构是这样的:

一般的处理代码是这样的:

    //根据docid获取文件内容,从分布式文件系统取,时间不可控
    nowFileContent = getFileByDocId(docId)    //do something,类似diff,追加操作
    newFileContent = doSomeThing()    //存储到文件系统
    setNewFileContent(docId,newFileContent)

对于场景1讲到的 A、C 两个请求同时到达代码段,但是由于网络原因,A 先拿到文档内容,C 在 A 写入前读到文件内容,所以最终的结果是两者会丢失一个写入。

所以需要对读写操作做一次加锁,保证事务的完整、一致。

下图是《现代操作系统》中的插图,这里的效果也希望如此。

Wiki 这类场景属于长耗时事务的资源处理问题,锁的出现保证不会因为事务中的读写间跨度耗时大导致写覆盖的情况,使得请求排队,顺序处理。

解决方案选择

我遇到的问题也是类 Wiki 这类长事务的问题,遇到问题第一想法是去看网上的解决方案。

网上 MySQL、ZK、Redis 各种实现方式很多,我需要选择哪种?怎么选择?我需要权衡哪些方面?

以前看分布式书的时候,一个被提到很多次的词是:trade-off,我理解是取舍或者是权衡吧。

作为一个 Web 开发者,我需要考虑的主要包含下面几个部分:

  1. 实现我的功能是否 OK,耗时是否满足在线需求?
  2. 实现难度、学习成本;
  3. 运维成本。

那么按照这几个标准来看一下现在的可选方案:

选好了方案,下面就是实现了。如果我们最终实现了这个锁,对它的要求是什么呢?实现MySQL 单主架构,写都会到 master,有瓶颈。ZK 的方式需要自己搭建、运维,而且需要堆机器,利用率不高。最终采用了 Redis 来实现,流量/存储都可以扩容,运维也不需要自己。

  1. lock 实现必须要是原子操作,同时保证任何时候只有一个竞争者是独占的;
  2. unlock 必须是原子的,同时保证只有自己可以解锁自己;
  3. 不能出现死锁,当进程挂掉之后不影响其他的加锁行为;
  4. 支持 Twemproxy 模式下的架构和单机;
  5. 耗时可以接受。

基于上述要求我的实现如下(只提供了大致,删除了敏感信息):

<?phpclass LockUtility{    const DEFAULT_UNLOCK_TIME = 4 ;    const COMMON_REDISKEY_PREFIX = 'xxxxx' ;    /**
     * @brief  
     *
     * @param $ukey  需要加锁的key
     * @param $unlockTime  锁持有时长
     *
     * @return   
     */
    public function __construct($ukey,$unlockTime=self::DEFAULT_UNLOCK_TIME){        $this->_objRedis   = RedisFactory::getRedis();        $this->_redisKey   = self::COMMON_REDISKEY_PREFIX.$ukey;        $this->_unLockTime = $unlockTime ;        //为单次加锁生成唯一guid
        $this->_guid       = genGuid();   
    }    /**
     * @brief 对给定的key进行加锁处理 
     *
     * @return   
     *
     *        true  表示加锁成功
     *        
     *        抛出异常则表示加锁未成功,根据业务选择自己的care的级别
     *        异常错误码 :
     *        1.网络错误:  ErrorCodes::REDIS_ERROR        视业务严谨度,这个错误是否忽略
     *        2.锁被占用:  ErrorCodes::LOCK_IS_USED       明确确定锁被别人占有
     */

    public function lock(){        /*  
         *  设置锁的过程需要是原子的,所以采用了set来操作
         *         SET key value [EX seconds] [PX milliseconds] [NX|XX]
         *         Redis 2.6.12 版本开始支持通过set 指定参数完成setexnx功能
         *
         *  php 语法  : $redis->set('key', 'value', Array('xx', 'px'=>1000));
         *
         */
        $setRet = $this->_objRedis->set($this->_redisKey,$this->_guid,array('nx', 'ex' => $this->_unLockTime));        //返回false表示请求锁失败
        if(false === $setRet){            //锁被占用,抛异常
            throw new Exception("get Lock Failed!Locking",Constants_ErrorCodes::LOCK_IS_USED);
        }        //redis返回null,是网络、机器授权、语法错误等等
        if(is_null($setRet)){            //网络错误、异常
            throw new Exception("Request Redis Failed",Constants_ErrorCodes::REDIS_ERROR);    
        }        return $setRet ;
    }    /**
     * @brief  解除对某个key的锁定,原则上不需要关心返回值,可以多次调用
     *
     * @return  
     *         1  redis会话成功,并且成功删除了key
     *         0  redis会话成功,但是待删除的key已经不存在
     * 
     */
    public function unlock(){        //Reids 2.6 版本增加了对 Lua 环境的支持,解决了长久以来不能高效地处理 CAS (check-and-set)命令的缺点
        $luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" ; 
        $delRet = $this->_objRedis->eval($luaScript,array($this->_redisKey,$this->_guid),1);        if(is_null($delRet)){            //redis返回null,是网络、机器授权、语法错误等等
            throw new Exception("Request Redis Failed",Constants_ErrorCodes::REDIS_ERROR);    
        }        return $delRet ;
    }
}

代码写出来之后是否解决了上面的问题呢?我们来看一下单机和集群 Redis 方案下的使用。

原文发布于微信公众号 - GitChat精品课(CSDN_Tech)

原文发表时间:2018-04-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏杨建荣的学习笔记

Oracle 12C打补丁的简单尝试(r10笔记第55天)

最近在服务器盘点的时候,发现测试环境还是值得整合一下,因为服务器资源老旧,整体配置不高,服务器资源使用率不高,业务要求不高,多个实例分散在多台服务器上,要考虑灾...

3798
来自专栏芋道源码1024

【追光者系列】HikariCP 连接池配多大合适(第一弹)?

首先声明一下观点:How big should HikariCP be? Not how big but rather how small!连接池的大小不是设置...

2070
来自专栏程序你好

微服务和传统中间件平台

微服务与部署在中间件平台(esb、应用服务器)上的传统服务有何不同?什么是微服务体系结构模式,它解决了什么问题?本文将讨论所有这些重要的主题,并描述如何管理、管...

1282
来自专栏我的安全视界观

[一起玩蛇】Python代码审计中的器II

3027
来自专栏CSDN技术头条

【问底】许鹏:使用Spark+Cassandra打造高性能数据分析平台(一)

【导读】笔者(许鹏)看Spark源码的时间不长,记笔记的初衷只是为了不至于日后遗忘。在源码阅读的过程中秉持着一种非常简单的思维模式,就是努力去寻找一条贯穿全局的...

3178
来自专栏腾讯移动品质中心TMQ的专栏

【浅谈Chromium中的设计模式(一)】——Chromium中模块分层和进程模型

“EP”(中文:工程生产力)是目前项目中提升研发能力的一个很重要的衡量指标。笔者重点学习了Chromium产品是如何从代码和设计层面来保证快速高效的工程生产力。...

5128
来自专栏FreeBuf

点击一张图片背后的风险

* 本文原创作者:mscb,本文属FreeBuf原创奖励计划,未经许可禁止转载 你相信吗?仅仅是因为你点击了某个你一只在访问网站里的一张图片,导致你的用...

2507
来自专栏张善友的专栏

MongoDB核心贡献者:不是MongoDB不行,而是你不懂!

近期MongoDB在Hack News上是频繁中枪。许多人更是声称恨上了MongoDB,David mytton就在他的博客中揭露了MongoDB许多现存问题。...

24410
来自专栏Android机动车

Android开发者的UI自动化测试上手指南

开发人员测试自己所开发软件的行为就像学生在完成考试后对自己的成绩进行评估,所以可能会出现下面的问题:

1422
来自专栏沃趣科技

ASM 翻译系列第一弹:基础知识 ASM AU,Extents,Mirroring 和 Failgroups

原作者:Bane Radulovic 译者: 魏兴华 审核: 魏兴华 ASM Allocation Units 在ASM磁盘组中,最基本空间分配单位...

3577

扫码关注云+社区

领取腾讯云代金券