在打算机中,锁的浸染是办理在并发状态下的共享资源互斥问题,担保在同一韶光只有一个进程/线程可以节制资源的掌握权。
例如以下几种情形:
文件锁的实现是为理解决不同用户同时读写同一文件的并发问题而涌现的,防止导致文件的内容被毁坏。

利用数组实现的行列步队,在 push 操作的地方一样平常须要加锁来办理槽位的争夺问题,防止涌现多次 push 冲突从而导致数据丢失问题。
对付12306来说,火车票便是他的资源,终极放票的时候须要锁来担保票、人、座位唯一对应。
……
上面的例子中实在就包含了我们常日讲的传统单机锁和我要讲的分布式锁。
单机环境下,资源竞争者都是来自机器内部((进程/线程),那么实现锁的方案只须要借助单机资源就可以了,比如借助磁盘、内存、寄存器来实现。
但是对付分布式环境下,资源竞争者生存环境更繁芜了,原有依赖单机的方案不再发挥浸染,这时候就须要一个大家都认可的折衷者出来,帮助办理竞争问题,那这个折衷者称之为分布式锁。
上面这个例子就像两小我员产生的抵牾,只要公司的领导出面就可以办理。而当两个公司产生竞争抵牾的时候,就须要法律机关出面,是同一个道理。
大略的说,分布式锁便是办理分布式环境下资源竞争问题的手段。
分布式锁的运用处景
所有分布式环境下会涌现资源竞争的地方都须要分布式锁的折衷,除了上面先容的 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 开拓者,我须要考虑的紧张包含下面几个部分:
实现我的功能是否 OK,耗时是否知足在线需求?
实现难度、学习本钱;
运维本钱。
那么按照这几个标准来看一下现在的可选方案:
实现办法功能哀求实现难度学习本钱运维本钱
MySQL 的方案借助表锁/行锁实现知足基本哀求不难熟习小量OK、大量影响现有业务、1主多从架构,未便利扩容
通过 ZK 创建数据节点的办法实现知足哀求熟习 ZK API 即可须要学习重,须要堆机器,有跨机房要求
Redis 利用 setnxex基本哀求不难熟习扩容方便、现有做事
MySQL 单主架构,写都会到 master,有瓶颈。ZK 的办法须要自己搭建、运维,而且须要堆机器,利用率不高。终极采取了 Redis 来实现,流量/存储都可以扩容,运维也不须要自己。
实现
选好了方案,下面便是实现了。如果我们终极实现了这个锁,对它的哀求是什么呢?
lock 实现必须假如原子操作,同时担保任何时候只有一个竞争者是独占的;
unlock 必须是原子的,同时担保只有自己可以解锁自己;
不能涌现去世锁,当进程挂掉之后不影响其他的加锁行为;
支持 Twemproxy 模式下的架构和单机;
耗时可以接管。
基于上述哀求我的实现如下(只供应了大致,删除了敏感信息):
_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 明确确定锁被别人霸占 /publicfunctionlock(){/
设置锁的过程须要是原子的,以是采取了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){//锁被占用,抛非常thrownewException(\"大众get Lock Failed!Locking\"大众,Constants_ErrorCodes::LOCK_IS_USED); }//redis返回null,是网络、机器授权、语法缺点等等if(is_null($setRet)){//网络缺点、非常thrownewException(\"大众Request Redis Failed\"大众,Constants_ErrorCodes::REDIS_ERROR); }return$setRet ; }/ @brief解除对某个key的锁定,原则上不须要关心返回值,可以多次调用 @return 1 redis会话成功,并且成功删除了key 0 redis会话成功,但是待删除的key已经不存在 /publicfunctionunlock(){//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,是网络、机器授权、语法缺点等等thrownewException(\"大众Request Redis Failed\"大众,Constants_ErrorCodes::REDIS_ERROR); }return$delRet ; }}
代码写出来之后是否办理了上面的问题呢?我们来看一下单机和集群 Redis 方案下的利用。
单机 Redis 架构
对付上图的单点架构,读写不分离。
那么上面的代码对付上面哀求是否知足?
lock 采取了set + nx + ex 参数 + redis 单线程可以担保 lock 是个原子操作,加锁成功即成功,失落败即失落败,知足哀求1和哀求3去世锁处理,超时 key 失落效;
unlock 采取 Lua 担保了 compare and del 这个操作是原子的,同时办理了自己删除自己的需求;
耗时上呢?都是一次要求,可以接管,同机房在 ms 级。
Twemproxy 模式下的多地域多分片主从架构
Twemproxy 是对 Redis/Memcache 的代理,紧张卖力根据 key 路由到分片的功能,存在它不支持的操作,例如 keys 。不支持的缘故原由是它须要遍历所有分片才能完成操作,对付大略的 set/get 还是路由到相应的分片,事情事理同等。
对付 Lua 脚本呢? Lua 脚本是怎么路由的?支持吗?
我们利用 eval 来实行的时候,我创造我们集群的文档里这么写:
必须至少有一个 key 在 script 后面。命令将发往第一个 key 所在的分片。
也便是说利用 eval 来完成事情,命令是发向第一个 key 的,而我们的第一个 key 便是我们要处理的 key,以是这套代码在集群模式也是支持的。
但是对付集群来说,现在都是采取的终极同等性、单地域主多地域从、写走主地域的模式。
那么便是说写要求是跨地域的?这个我利用了多一步操作读来优化,由于读不跨地域、写跨地域,但是99%以上的要求主从延时都没这么大,当然99%这个比例是我预测的。
详细代码如下:
functionlock(){//首先采取exist来看指定key是不是存在了if($objRedis->exist($key)){//key存在一定是被占了,抛非常}//if not exist,并不能代表这个锁真的没被占用,可能是主从延时,这时候复用上面的代码更安全,减少一次跨机房写}
利用把稳事变如下:
利用时候须要掌握好自己的 lockTime,须要长于你的事务实行韶光;
上层在获取锁失落败的时候,须要自己去选择是壅塞还是抛弃这次要求,让用户端重试。
目前待办理问题有:
如果你的进程由于 CUP 急急而被挂起,而且挂起的韶光超过了你设置的锁的失落效韶光,是不是仍旧会涌现问题?
如果集群模式一个分片挂了,会发生什么?
你有什么办法办理吗?欢迎留言谈论。
总结一下我这次的分享,紧张有以下几点总结:
分布式锁是指分布式业务环境下须要的锁,对支持锁的做事没有哀求要分布式;
锁实际上是一个资源折衷者的角色,管理并发态下的资源掌握权;
方案选择就像投资,须要考虑投入产出比;
Redis 单机和集群方案有自己的优化点,根据场景做优化;