11 张图深入理解分布式锁原理(一)
什么是分布式锁?它能干什么
单体系统中,在高并发场景下想要访问共享资源的时候,我们需要通过加锁的方式来保证共享资源并发的安全性,确保在同一时刻只有一个线程对共享资源进行操作。相信大家对于 Java 提供的 synchronized 关键字以及 Lock 锁都不陌生,在实际的项目中大家都使用过。如下图所示,在同一个 JVM 进程中,Thread1 获得锁之后,对共享资源进行操作,其他线程未获得锁的线程只能等待 Thread1 释放后才能进行对应的操作。
但是随着业务的不断发展,原先的单体应用被拆分为多个微服务,每个微服务又会部署多个实例,于是就形成了当下的微服务架构。处理共享资源的请求来自不同的服务实例,也就是在不同的 JVM 进程中。原先的单体服务中的加锁方式在分布式场景下不能满足共享资源的并发访问要求。因此我们需要一种适用于分布式场景下的共享资源安全的处理机制,此时应对这种问题的分布式锁就应运而生了。
既然 JVM 进程管不到其他服务实例的线程,那么可以借助于外部组件能力来实现不同服务实例对于共享资源的统一管控,这种能力我们可以称之为分布式锁。因此分布式锁的本质就是在不同服务实例之外建立一种获取锁的机制,形成一种并发互斥能力来确保不同线程对于共享资源的并发安全,从而实现在微服务架构中同一时刻只有一个线程可以对共享资源进行操作。对于分布式锁来说,实际就是需要一个外部的状态存储系统来实现原子化的排他性操作。
通过对于分布式锁的需求分析,总结了如下的分布式锁四大特性,分别是多节点、加锁速度快、排他性以及锁过期实现机制。
分布式锁实现方案
2.1 基于数据库的分布式锁实现方案
2.1.1实现原理
通过数据库的方式实现分布式锁的效果,实际就是借助于数据库的唯一性约束特性或者 for update 来实现。这里以唯一性约束来举个栗子,在电商领域的库存服务负责对商品的库存进行扣减,首先创建一张专门存放锁信息的锁表,那么库存服务在进行库存操作之前,先向数据库中的锁表插入一条锁资源数据。
create table ‘distributed_lock’ (
‘id’ BIGINT NOT NULL AUTO_INCREMENT,
‘resource_lock_key‘ varchar(64) NOT NULL
PRIMARY KEY(‘id’),
UNIQUE KEY ‘uk_resource_lock_key‘ (‘resource_lock_key‘) USING BTREE
)
大致的交互流程如下:
1、当库存服务进行手机库存扣减的时候,首先先向数据库中的锁表当中插入一条资源锁信息;
2、如果插入成功,则表示库存服务 1 可以对手机库存进行库存扣减操作;
3、此时库存服务 2 也要对库存进行操作,于是同样插入数据到锁表中;
4、但是由于锁表设置了唯一性约束,锁信息插入失败,库存服务进行等待;
5、库存服务 1 执行完库存扣减之后,删除锁表的信息;
6、库存服务 2 尝试插入资源锁信息,发现可以插入成功,继续执行后续操作。
2.1.2 方案分析
基于数据库的实现方式,看起来还是比较容易理解的。但是实际上还是有一些问题存在的,我们一起来分析下。
1、性能问题:由于是插入数据数据需要落盘存储,如果平凡进行读写的话会影响数据库性能,另外由于使用唯一键进行判断也会一定程度上影响数据库性能,因此数据库方案适用于并发量不到的简单场景;
2、数据库如果单点部署的话会存在单点故障问题,如果数据库出现故障,可能会导致平台中的业务异常;
3、死锁问题:在上文介绍中,包含了插入数据库的获取锁的步骤,还包含了删除锁信息的释放锁的过程,但是如果库存服务 1 在加锁之后挂掉了,无法进行锁的释放,而其他服务又无法获取到锁就会造成死锁的问题。当然了我们可以通过一个定时任务去检查锁表中是不是有过时的锁资源。但是这样无疑增加了分布式锁实现的复杂性。
4、不支持可重入:如果想要实现可重入锁,还需要增加主机、线程名等字段来进行标注,通过这几个字段来判断和当前信息是否一致,如果一致则认为已经获取到了锁。
鉴于以上的这些问题,有没有其他的分布式实现方案可以避免上述存在的问题呢?我们再往下来看。
2.2 基于 Redis 的分布式锁实现方案
2.2.1 基于 sentnx 命令的实现原理
Redis 作为一块高性能的数据库中间件,经常被当做缓存在项目中使用。因此通过 Redis 实现分布式锁,也是比较常见的实现方案。一样的道理,通过 Redis 实现分布式锁也需要通过它实现锁的互斥的能力。实际上就是利用了 sentnx(set if not exists)命令。同时该命令是否能够设置成功,决定服务是否可以拿到对应的分布式锁。
127.0.0.1:6379> setnx stockLock 10.12.35.12_stockService
(integer) 1
如上图所示,大致的加锁以及释放锁的过程其实和数据库的分布式锁方案还是比较类似的。只不过将其中向数据库插入数据的步骤替换成了向 Redis 获取锁的步骤,由于 Redis 是基于内存进行操作的,因此性能上比基于数据库的分布式锁方案更好一点。
2.2.2 原理分析
上述基于 Redis 的方案的方案在性能上具有优势,我们再来分析下,这个使用命令的方式有没有什么问题。实际上和前面的数据库方案类似,Redis 也会有死锁问题,当获取锁之后如果库存服务 1 挂掉了,库存服务 2 就获取不到锁了。因此我们要对其进行优化。那么问题的本质是如何让锁可以释放,因此我们需要在设置锁的时候加上过期时间,这样即使库存服务 1 挂了,无法主动释放锁,那么到了过期时间后锁失效,库存服务 2 依然可以获取锁,不会再造成死锁问题。
文章转自公众号:慕枫技术笔记