浅谈分布式锁的实现

为什么要使用锁

业务在同一时刻只能有一个实例在运行,比如活动开奖、耗时数据库操作等场景,通常都以保证业务执行正常、节约服务器资源或者提高程序健壮性为目的。

引子 —— 文件锁实现锁

借助文件系统自带的锁机制 —— 排它锁 来实现,即一个进程在启动时获得一个文件的排它锁,并在自己的整个运行期间都保留句柄资源而不释放锁,使得另一个进程实例在启动时想要获得同一文件时失败,从而保证在 单台机器上同一时刻至多只有一个实例 在运行
PHP代码实现如下:

<?php
$filename = __FILE__ . '.lock';
$handle = fopen($filename, 'w');
if (!flock($handle, LOCK_EX | LOCK_NB)) {
    // 获取排它锁不成功,说明已经有其他进程获取到文件锁,视作已有实例在执行,当前进程退出
    echo "当前已经有进程在执行,本进程不执行", PHP_EOL;
    exit();
}
/* 业务逻辑 */

在这里讲一个本人在实践中遇到的一个小坑
上述示例代码中没有采用面向对象的程序设计,习惯面向对象编码的同学喜欢将上述程序过程整合到一个类里头,注意这个时候需要保证 $handle 在你的方法结束之后还没有被释放,如下面这样的写法就是有问题的:

<?php
/* 这段示例代码是有问题的,是个错误示范,可不要在自己的项目中使用噢 */
class Locker {
    public static function doLock() {
        $filename = __FILE__ . '.lock';
        $handle = fopen($filename, 'w');
        if (!flock($handle, LOCK_EX | LOCK_NB)) {
            // 获取排它锁不成功,说明已经有其他进程获取到文件锁,视作已有实例在执行,当前进程退出
            echo "当前已经有进程在执行,本进程不执行", PHP_EOL;
            exit();
        }
    }
}

Locker::doLocker();
/* 业务逻辑 */

因为 doLocker() 方法执行完毕之后,句柄 $handle 作为局部变量会被立即会收掉,所以排它锁也会被释放掉,进程锁就直接失效了。

  • 优点
    稳定!本人在不计其数的业务中使用过文件锁,两年多程序员职业生涯还没有在此方面翻过车。只要磁盘不出问题,文件锁还是很给力的;

  • 缺点
    从上述示例可见,文件锁依赖本次文件系统,只能在单个操作系统中产生作用。
    由于业务的横向扩展,通常情况下一套业务需要部署在多台服务器上,此时文件锁便不能满足需求了。
    如果要实现跨越操作系统的锁限制,则必须引入 外部存储方案;

几个分布式锁实现方案

从理论上来说,任何第三方的存储都能够实现本地文件系统功能。只不过各种操作需要走网络IO,在稳定性、速率上同本地文件系统会存在一定的差距。下面来研究一下几个比较常用的外部存储方案来实现分布式锁。

MySQL

论及最常用的外部存储方案,MySQL作为从大学时期就接触的数据库选型,必然首当其冲。使用数据库实现分布式锁也有两种方式:仅将数据库作为存储数据库排它锁

仅将数据库作为存储

  • 基本思路

    1. 定义锁的标识符,这个标识符作为数据库表中的表征字段(主键),以此字段查询表中是否存在对应记录,若存在,则说明存在锁,则稍后再来检查;否则执行下一步;
    2. 执行 INSERT 语句插入锁信息,此时可能有好几个进程同时执行插入语句,但是由于插入字段中会含有主键,所以只有会一条 INSERT语句执行成功,执行成功的实例获得排它锁,则开始执行自己的业务逻辑;其他执行失败的实例则稍后再来检查,从第一步重新开始;
    3. 业务逻辑执行完毕之后,执行 DELETE 语句删除掉数据库中的锁记录从而释放锁;
    4. 另外起一个维护锁的业务来定期删除掉数据库中过期的锁记录,防止因为程序意外退出而没有删除掉锁记录造成死锁;
  • 存在问题
    上述业务虽然能够满足简单的一些需求,但是还是存在问题:

    1. 需要保证MySQL服务的高可用;
    2. 必须使用轮询的方式去检查和获得锁,轮询间隔时间长了,业务执行中断到重新启动业务之间存在空档期变长了,不能忍受中断时间过长的业务不适用;轮询间隔短了,MySQL操作将会变得频繁,一旦业务增多,将会出现性能问题;
    3. 最后一步中的死锁清理存在误判风险,存在业务还未执行完毕锁就被清除的情况,从而导致同一时刻运行多个实例;

利用数据库的排它锁

采用数据库排它锁必须满足两个条件:

  1. 数据库表格使用 InnoDB 引擎;
  2. 必须使用到表格主键,否则会将整个表格都锁住;
  • 表格设计:
字段 类型 注释
name VARCHAR(100) Primary Key 名称
info VARCHAR(100) - 名称
created_at INT(10) UNSIGNED - 创建时间

构造语句如下:

CREATE TABLE `business_lock` (
    `name` VARCHAR(100) NOT NULL COMMENT '名称',
    `info` VARCHAR(100) NOT NULL COMMENT '信息',
    `created_at` INT(10) UNSIGNED NOT NULL COMMENT '创建时间',
    PRIMARY KEY (`name`)
)
COMMENT='业务锁'
ENGINE=InnoDB
;
  • 基本思路

    1. 假定锁名称为 lock ,则先到数据库中查询是否存在 name = 'lock' 的记录,不存在则 INSERT 一条,再向下执行;否则直接向下执行;
    2. 执行数据库语句:
      START TRANSACTION; SELECT * FROM `business_lock` WHERE `name` = 'lock' FOR UPDATE;
      
      执行之后,由于数据行锁的作用,只有有一个实例会直接返回记录信息,其他的实例都会进入阻塞状态;
    3. 执行业务逻辑,执行完毕之后,只用 COMMIT 语句释放行锁;
  • 优点

    1. 使用了MySQL行锁带来的阻塞特性,使得实例A挂掉,实例B马上可以接替A的工作,期间空档期会缩短;
    2. 不用轮询MySQL;
  • 缺点

    1. 业务增多并且MySQL的排它锁长期不释放,会导致MySQL的连接变多,占据大量的MySQL连接池资源;

Redis

除了MySQL这样的关系型数据库之外,我们用得最多的就是 RedisRedis 作为非关系型的内存数据库,在执行速度上比MySQL要快上不少。Redis实现分布式锁本质上和上面介绍的第一种MySQL方案是一样的。

单点 Redis

  • 基本思路

    1. 定义锁的标识符,并生成 token
    2. 以标识符作为 key 执行 setnx 设置值为 tokenexpire语句 (用LUA封装,保证原子性),若数据库中无记录,则会执行成功,表示实例获得锁;否则执行失败表示锁已经被其他实例已经获得锁,不继续向下执行;
    3. 执行业务逻辑,实例结束之前执行获取 key 对应的内容,如果内容和 token 相同,则执行删除(get 和 del 操作用LUA进行封装来保证原子性);
  • 优点

  1. 执行效率高,而且自带过期操作,开发友好;
  • 缺点
  1. 强依赖Redis,单点Redis风险高,挂掉之后造成实例都不会进行;
  2. 存在 key 过期之后实例还没执行完毕的情况,有概率在同一时刻会执行多个实例;

RedLock

Redis Distlock

Zookeeper

ZooKeeper是一个高可用的分布式数据管理与系统协调框架,在Paxos算法的加持之下,该框架在分布式的环境中可以保持非常强的数据一致性,从而可以帮助解决很多分布式问题。
我们可以简单地将它看成是一个远程的小文件服务,而每个小文件又支持状态变化的监听和通知,它的数据模型如下:

/-
 |--- locks/
      |--- mylock0000001
      |--- mylock0000002
 |--- service/
 |--- users/

上面根路径下的各种路径和节点都是由我们自己手动创建的。
在上面的每个节点上,我们都可以新增监听器,当zookeeper发现节点发生变化时(增、改、删),都会通知到监听它的客户端。

如何使用Zookeeper实现分布式锁

利用Zookeeper实现分布式锁也有两种基本思路:

  1. 利用节点名称唯一性实现共享所,和文件锁有些类似;
    由于和文件锁比较类似,原理不再赘述。不过要提的是,当节点(锁)被释放时,zookeeper会通知到所有监听这个节点的客户端,从而各个客户端开始竞争,最终只有一个客户端获得锁。虽然实现简单,但锁释放时唤醒了所有的客户端,产生了「惊群效应」,故在性能上不是很客观。
  2. 利用临时顺序节点实现共享锁;
    Zookeeper 还支持一个很厉害的特性:临时节点和顺序节点。
    临时节点顾名思义,就是临时创建的节点,客户端创建的该类节点,在客户端和服务连接断开时就会删除;
    而顺序节点就是在节点名称最后自动加上后缀,这个后缀在节点所在路径中时自增的。
    依靠这两种特性,聪明而伟大的开发者就想到了一种比较好的监听流程
 ↑:表示监听


Instance1 -> /locks/0000001
 ↑
Instance2 -> /locks/0000002
 ↑
Instance3 -> /locks/0000003
 ↑
Instance4 -> /locks/0000005
 ↑
......

上面这段示例中InstanceX表示运行实例,/locks/000000X为获得的节点路径,节点路径后缀最小的节点获得锁,其他的每一个实例都去监听所有节点中比自己次小(比自己小的节点中最大的节点)的节点的变化。
当1号实例释放了锁,那么2号实例就会得到通知,再扫描一下所有节点,判断到自己是最小的节点了,于是便获得了锁,后续的节点按照这个逻辑类推;
如果在1号实例释放前3号实例突然意外挂了,4号节点得到通知,扫描一下所有节点发现自己并不是最小的,于是开始监听2号节点的变化,所以整个监听链路是稳健的。
该方法每次锁释放时只会通知到一个客户端,所以不会有「惊群效应」。

总结

总之,设计分布式锁无非就是处理如下这三个方面的问题:

  1. 获得锁;
    • 数据库用行锁或者用唯一约束字段实现;
    • Redis用 setnx 实现;
    • Zookeeper用 最小节点 实现;
  2. 释放锁;
    • 正常情况下客户端会主动释放,如删掉数据库的某条数据,释放行锁等;
    • 异常情况下,需要借助过期锁清理机制释放锁,而zookeeper和客户端之间就存在心跳,如果客户端意外退出,心跳检测可以立即发现,从而服务端主动清锁;
  3. 释放锁通知:分为客户端主动获取 和 服务端主动告知,前者需要客户端做轮询操作,在时效性上不如后者;

从这几点看,zookeeper在实现分布式锁最为简单。
后续我会再研究一下zookeeper的性能。

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