「分布式技术」分布式锁的最佳实践

参考&鸣谢

分布式最佳实践:分布式锁

几种分布式锁的实现方式

分布式锁的几种实现方式~

文章目录

  • 「分布式技术」分布式锁的最佳实践
    • 一、为什么需要分布式锁
    • 二、实现方式
      • 基于数据库实现
        • 乐观锁实现
        • 悲观锁实现
        • InnoDB排他锁
      • 基于Redis :AP架构
      • 基于Zookeeper :CP 架构
    • 三、三种方案的比较
    • 四、总结

一、为什么需要分布式锁

在传统的单体服务中,我们经常会遇到多线程对于单一资源的抢占导致的线程安全问题以及对数据库数据操作的一致性问题,如果是在单体系统中,我们可以很方便的使用编程语言提供的锁以及数据库事务来解决这些问题。

一旦单体系统转为分布式架构,那么本地事务和线程锁就无法满足跨进程的锁效果;分布式锁则是用于进程间同步访问共享资源的一种方式,通过全局共享来实现全局锁的效果,保证数据的一致性。

总的来说,在分布式系统中,当我们期望一个操作(一个请求、一个方法、一个数据库操作…)在整个系统中同一时间只能有一个线程执行,那我们就需要用到分布式锁; 抽象来看就是两个场景:

  • 单一资源的数据变更:比如对共享存储数据(数据库、缓存…)进行修改,多线程的互斥
  • access token:对于多个资源的原子性操作,期望整个业务逻辑就是单一线程执行保持一致性,在入口处就锁住

分布式锁应该具备的特性:

  • 原子性:在分布式系统中,一个方法在同一时间只能被一个线程执行
  • 阻塞性:在没获取到锁时可以进行阻塞也可以返回失败
  • 高可用:能够正确的获取锁和释放锁,且具备锁失效的能力
  • 高性能:获取锁与释放锁的性能保障
  • 可重入:能够具备可重入特性

二、实现方式

基于数据库实现

乐观锁实现

先去干,能不能干,能不能干成先不管,这就是乐观心态。在开发过程中,乐观锁用的非常多,比如典型的 CAS ;在不加锁的情况下保证数据的一致性。

使用方式也很简单,只需要在表中添加一个版本号的字段,每次对数据进行修改的时候,通过版本来确定是否能够更新 update xx set version = OLD_VERSION+1 where id = ID and version = OLD_VERSION , 如果更新不成功,客户端可以选择是否重试。当然,需要加上索引。

可见这种方式的优势其实很明显,不加锁,使用简单。但也有一些局限性

  • 只能支持单数据更新的一致性(对于数据的插入可以通过唯一索引来解决
  • 由于是乐观锁(先干,在检查),也就意味着可能活干完咯,发现更新不了,浪费了计算资源
  • 无法支持 access token

悲观锁实现

先自我审查自己能不能干,能不能干成,如果答案是no,那么就等着(阻塞)或先溜(返回),这就是悲观心态。悲观锁在 access token 模式更加适用。

使用方式同样很好理解(这只是基于数据库的悲观锁的一种实现方式)

  1. 有一张 资源锁 表,表中包含 字段,并需要加上唯一索引
  2. 当有线程想要获取某个锁时,只需要在 资源锁 表中插入一条数据
  3. 如果插入成功,表示获取锁成功,插入失败则表示锁已经被占用
  4. 业务执行完释放锁,删除对应的锁记录即可
// 1. 创建资源锁表
CREATE TABLE `resource_lock` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',`lock_name` varchar(64) NOT NULL COMMENT '锁名',`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,PRIMARY KEY (`id`),UNIQUE KEY `uidx_method_name` (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;//2. 获取锁(插入成本表示获取到锁
INSERT INTO resource_lock (lock_name) VALUES ('lockName');//3. 释放锁
delete from resource_lock where lock_name ='lockName';
复制代码

乍一看好像很简单,如果程序一直保证正确执行,这种方式好像也行,但没如果… 对于一个分布式系统,服务宕机是会出现的,所以还需要考虑一些新的可能发生的问题

  • 没有失效机制:持有锁的线程所在的服务宕机了,还没来的及释放锁怎么办? 可以通过在表中新增过期时间,写一个定时任务定期删除过期锁
  • 不可重入:需要在表中新增线程信息,重入的时候先查询是否存在锁
  • 不支持锁阻塞:需要编写相应的逻辑
  • 基于数据库实现,那么数据库的可用性就需要得到保证,而且在并发大的时候,对于数据库的性能的影响问题

这么一分析…为了确保悲观锁的功能完整性,实现也会越来越复杂…

InnoDB排他锁

除了可以通过增删操作数据表中的记录以外,其实还可以借助数据中自带的锁来实现分布式的锁。

我们还用刚刚创建的那张数据库表。可以通过数据库的排他锁来实现分布式锁。 基于MySql的InnoDB引擎,可以使用以下方法来实现加锁操作:

public boolean lock(){connection.setAutoCommit(false)while(true){try{result = select * from methodLock where method_name=xxx for update;if(result==null){return true;}}catch(Exception e){}sleep(1000);}return false;
}

在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁(这里再多提一句,InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给method_name添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。

我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,再通过以下方法解锁:

public void unlock(){connection.commit();
}

通过connection.commit()操作来释放锁。

这种方法可以有效的解决上面提到的无法释放锁和阻塞锁的问题。

  • 阻塞锁? for update语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。
  • 锁定之后服务宕机,无法释放?使用这种方式,服务宕机之后数据库会自己把锁释放掉。

但是还是无法直接解决数据库单点和可重入问题。

这里还可能存在另外一个问题,虽然我们对method_name 使用了唯一索引,并且显示使用for update来使用行级锁。但是,MySql会对查询进行优化,即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。如果发生这种情况就悲剧了。。。

还有一个问题,就是我们要使用排他锁来进行分布式锁的lock,那么一个排他锁长时间不提交,就会占用数据库连接。一旦类似的连接变得多了,就可能把数据库连接池撑爆


基于Redis :AP架构

什么是CP架构和AP架构?

我们知道一个分布式系统不可能同时满足数据一致性(consistency)、服务可用性(availability)、分区容错性(partition-tolerance)。

现实情况下,我们面对的是一个不可靠的网络、有一定概率宕机的设备,这两个因素都会导致Partition,因而分布式系统实现中 P 是一个必须项,而不是可选项。

对于分布式系统工程实践,CAP理论更合适的描述是:在满足分区容错的前提下,没有算法能同时满足数据一致性和服务可用性。

因此,我们需要在C和A之间进行取舍:

  • CP架构(刚性事务):如果要满足数据的强一致性,就必须在一个服务数据库锁定的同时,对分布式服务下的其他服务数据资源同时锁定。等待全部服务处理完业务,才可以释放资源。此时如果有其他请求想要操作被锁定的资源就会被阻塞,这样就是满足了CP。达到了强一致性和弱可用性。
  • AP架构(柔性事务):如果要满足服务的的强可用性,每个服务就可以各自独立执行本地事务,而无需相互锁定其他服务的资源。在各个服务的事务尚未完全处理完毕时,如果去访问数据库,可能会遇到各个节点数据不一致的情况。然后我们还需要一些措施,使得经过一段时间后,各个节点的数据最终达到一致性。这样就是满足了AP。达到了弱一致性(最终一致性)和强可用性。

既然想到存储用缓存来做,那必然想到的第一个就是 Redis 了,Redis 也很给力,可以很好的支撑分布式锁的能力,提供了比较好用的命令

  • setnx: 当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。
  • expire: 为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。

大致的流程如下图

  1. client A 和 client B 同时执行 setnx("lock",UUID) 尝试获取到锁,Redis 的实现保证了只会有一个 client 成功,假如 client A 运气好成功了
  2. client A 紧接着马上设置一个过期时间expire("lock",10)
  3. client A 继续执行业务逻辑
  4. 执行完业务逻辑后释放锁

如果程序能够正常走,好像也没什么问题…但我们知道分布式架构中,网络是不可靠的,如果在设置过期时间前 client B 挂掉咯,那就 GG 了,因为没有设置过期时间,那就成死锁了… 就像下面这样

所以我们需要保证 setnxexpire 的原子性。在 Redis 2.6.12 之后增强了 setnx 命令,可以同时设置过期时间,从而保证原子性。

解决了死锁问题,再来看看过期时间的问题,我们如何判断我们应该设置多长时间的过期时间?

  • 设置短了,业务逻辑可能还没执行完,锁被释放了,被其它线程获取执行
  • 设置长了,需要业务逻辑处理完了自己释放锁(同样会存在线程挂掉的情况)

其实我们想要到达一种效果,如果能够自动续期,锁快要过期了,但是业务操作还没有处理完,就自动对锁进行续期。Java 中的 Redisson 客户端就通过 watch dog 机制(守护线程)来支持这个功能。

通过 Redisson 客户端获取锁时会创建一个守护线程,通过守护线程来定期 check 过期时间,如果业务逻辑还在运行,那么就会续时。如果程序宕机,那么守护线程也会一起挂掉,redis 中的锁也将不会再次续时,最后过期。从而自动实现续期且不会出现死锁的问题。

简单回顾一下,我们解决了

  • 获取锁和设置过期时间的原子性问题
  • 过期时间自动续时的问题

在单机模式下看起来已经没什么问题了。而在生产环境下一般都会是集群模式,比如哨兵模式。得益于 Redis 的 AP 架构,选择了可用性,使得其性能非常好,但也正是因为AP架构,可能会导致数据丢失的情况。

  1. client A 获取锁成功
  2. master 节点在同步锁信息到 slave 节点时,master 宕机,信息没有向 slave 节点同步成功
  3. slave 节点通过选举成为 master 节点
  4. client B 再次获取相同的锁,发现 slave 节点上并没有其它线程占用,所以也获取到了锁
  5. client A 和 client B 获取到了相同的锁

当然,这个是非常极端的情况下会出现的问题;虽然 Redis 之父 Antirez 提出来了分布式锁的一种 「健壮」 的实现算法 RedLock,但依旧还是会有新的问题,比如节点奔溃重启、时钟跳跃…

总的来看,基于 Redis 实现分布式锁是很常用的,性能也比较高,满足绝大部分业务场景,如果我们能够接受非常极端情况下带来的锁丢失问题,Redis 分布式锁是个很好的选择。


基于Zookeeper :CP 架构

Zookeeper 是一种提供「分布式服务协调」的中心化服务,是以 Paxos 算法为基础实现的。Zookeeper 采用的是 CP 架构,选择了强一致性,这也就意味着不会像 Redis 那样出现数据丢失的情况(主从切换时),但为了实现强一致性,那么性能肯定是要比 Redis 差一些。

使用 Zookeeper 来实现分布式锁是比较简单的

  1. client 会在 Zookeeper 中创建一个临时节点,比如`/zk/lock
  2. 如果获取成功,那么 client 会创建一个 session 保持和 Zookeeper 临时节点的关联
  3. client 处理业务逻辑
  4. client 处理完业务逻辑后删除 临时节点,关闭 session

如果 client 宕机,那么 session 就会结束,临时节点也会自动删除,其它 client 就可以创建 lock 节点。

session 的维护是依赖于 client 的定时心跳来维护的,也就是说,如果 client 没有及时的给 Zookeeper 发送心跳检查,那么 Zookeeper 就会认为这个 session 已经过期了,就会删除调临时节点。比如出现长时间的 GC 或者长时间的网络延迟,都可能会导致临时节点被删除的可能。

对于 Zookeeper 来说,实现分布式锁从使用者角度来看比较简单,不需要考虑太多的东西,比如过期时间的设置。但维护成本会比较高,性能相对 Redis 也会差一些,以及可能会出现长时间失联导致的节点数据丢失的问题。


三、三种方案的比较

上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。

从理解的难易程度角度(从低到高)

数据库 > 缓存 > Zookeeper

从实现的复杂性角度(从低到高)

Zookeeper >= 缓存 > 数据库

从性能角度(从高到低)

缓存 > Zookeeper >= 数据库

从可靠性角度(从高到低)

Zookeeper > 缓存 > 数据库


四、总结

  1. 优先使用基于数据库的乐观锁
  2. 如果期望更高的性能且能够接受极少数情况的锁丢失,那么优先选择 Redis
  3. 如果期望尽可能的避免锁丢失,优先选择 Zookeeper,且考虑 GC 时间和 心跳检查的设置
  4. 在分布式系统中极端情况下,分布式锁都不太可靠,所以需要我们在业务层面的入口也相应的隔离,在真的发生了锁丢失导致的数据不一致的情况做对应的补偿

「分布式技术」分布式锁的最佳实践相关推荐

  1. 一致 先验分布 后验分布_「分布式技术」分布式事务最终一致性解决方案,下篇...

    各位志同道合的朋友们大家好,我是一个一直在一线互联网踩坑十余年的编码爱好者,现在将我们的各种经验以及架构实战分享出来,如果大家喜欢,就关注我,一起将技术学深学透,我会每一篇分享结束都会预告下一专题 上 ...

  2. 微服务架构之「 容器技术 」

    点击上方"方志朋",选择"设为星标" 做积极的人,而不是积极废人 现在一聊到容器技术,大家就默认是指 Docker 了.但事实上,在 Docker 出现之前,P ...

  3. 「SAP技术」SAP SD微观研究之根据销售订单查询到该订单发货的批次

    「SAP技术」SAP SD微观研究之根据销售订单查询到该订单发货的批次 事务代码VL06O, 点击'List Outbound Deliveries',进入如下界面, 输入相关查询参数,执行, 点击' ...

  4. 「SAP技术」SAP MM ME2N报表能按‘order acknowledgement‘查询采购订单

    「SAP技术」SAP MM ME2N报表能按'order acknowledgement'查询采购订单 执行事务代码ME2N, 点击'Dynamic Selections'按钮 ,进入如下界面, 找到 ...

  5. 「SAP技术」SAP MM 采购信息记录新价格不能体现在采购订单新的ITEM上?

    「SAP技术」SAP MM 采购信息记录新价格不能体现在采购订单新的ITEM上? 下午,一个同行朋友问我一个问题,说她修改了某个物料的采购价格,然后去一个老的采购订单上增加一个新的item,deliv ...

  6. 「SAP技术」SAP WM 如何根据TR号码查询TO号码?

    「SAP技术」SAP WM 如何根据TR号码查询TO号码? 今日下午,收到K项目的仓库部门用户的提问,如何通过TR号码查找TO单号码.笔者想到了LT22 & LT23等TO查询的报表里,是可以 ...

  7. 「SAP技术」SAP MM 批次管理的物料创建DN时无存储地点就不能输入批次值?

    「SAP技术」SAP MM 批次管理的物料创建DN时无存储地点就不能输入批次值? 1, 如下交货单80018169(SO#10002993),行项目里storage location为空,batch字 ...

  8. 「SAP技术」 SAP MM 已启用质检物料创建PO时候'STOCK TYPE'没有默认为X?

    「SAP技术」 SAP MM 已启用质检物料创建PO时候'STOCK TYPE'没有默认为X? Part I:SAP 标准行为 1, 物料11002335, QM视图里 01 inspection t ...

  9. 「SAP技术」SAP MM 不能向被分配了工厂代码的供应商采购服务?

    「SAP技术」SAP MM 不能向被分配了工厂代码的供应商采购服务? 近日收到A项目的业务团队报的错,说是试图创建一个服务采购订单,却失败了.报错如下, Procurement w/o materia ...

最新文章

  1. 论Java程序的运行机制
  2. WPF 模仿QQ音乐首页歌单效果
  3. 工作118:封装一个带有对话框的button组件
  4. C++ 编码规范建议
  5. Python爬虫编程实践 Task04
  6. Ubuntu下安装NetBeans步骤和相关问题的解决方法
  7. 使用webpack前端重构感受
  8. 04_使用域名访问后台管理系统(Nginx)
  9. Linux高手必看的10本经典书籍
  10. Ubuntu安装Adobe Reader
  11. SIRS传染病模型求解及MATLAB实现
  12. 操作系统-页面置换算法
  13. 小白篇之RTMP编码器辅助OBS实现多个平台推流直播
  14. Java NIO Selector , SelectionKey , SocketChannel , ServerSocketChannel
  15. 分布式系统之道:Lamport 逻辑时钟
  16. ipv6审核被拒绝的解决方案
  17. 人脸识别 Face Recognition 入门
  18. baidu+app+per+android,超级授权管理 SuperSU v2.82 SR5 for Android
  19. 省赛选拔-A 警察抓小偷
  20. 盘点一下国内智能巡检机器人TOP5

热门文章

  1. spring中的@mapper(componentModel = “spring“)
  2. 最值得经常翻看的五本经管畅销书
  3. 小解:offsetWidth与offsetLeft
  4. 地铁bas服务器系统,浅谈城市轨道交通BAS系统的发展
  5. 展讯平台-LCD驱动
  6. 如何判断电路板上贴片电容的参数
  7. Go语言基础(1)-- GO语言了解
  8. Ubuntu需要做的几件事
  9. RetaGNN: 面向整体序列推荐的关系型时态注意图神经网络 WWW2021
  10. (十二)Alian 的 Spring Cloud 自定义Archetype