我们的系统需要什么样的分布式锁?
2020-07-06 14:53:24针对共享资源的互斥访问历来是很多业务系统需要解决的问题。在分布式系统中,通常会采用分布式锁这一通用型解决方案。本文将就分布式锁的实现原理、技术选型以及阿里云存储的具体实践进行论述。
针对共享资源的互斥访问历来是很多业务系统需要解决的问题。在分布式系统中,通常会采用分布式锁这一通用型解决方案。本文将就分布式锁的实现原理、技术选型以及阿里云存储的具体实践进行论述。
一 从单机锁到分布式锁
在单机环境中,当共享资源自身无法提供互斥能力的时候,为了防止多线程/多进程对共享资源的同时读写访问造成的数据破坏,就需要一个第三方提供的互斥的能力,这里往往是内核或者提供互斥能力的类库,如下图所示,进程首先从内核/类库获取一把互斥锁,拿到锁的进程就可以排他性的访问共享资源。演化到分布式环境,我们就需要一个提供同样功能的分布式服务,不同的机器通过该服务获取一把锁,获取到锁的机器就可以排他性的访问共享资源,这样的服务我们统称为分布式锁服务,锁也就叫分布式锁。
单机锁到分布式锁
由此抽象一下分布式锁的概念,首先分布式锁需要是一个资源,这个资源能够提供并发控制,并输出一个排他性的状态,也就是: 锁 = 资源 + 并发控制 + 所有权展示
以常见的单机锁为例:
SpinLOCK 和 Mutex 都是一个 Bool 资源,通过原子的 CAS 指令:当现在为 0 设置为 1,成功的话持有锁,失败的话不持有锁,如果不提供所有权的展示,例如 AtomicInteger,也是通过资源(Interger)+ CAS,但是不会明确的提示所有权,因此不会被视为一种锁,当然,可以将“所有权展示”这个更多地视为某种服务提供形式的包装。
单机环境下,内核具备“上帝视角”,能够知道进程的存活,当进程挂掉的时候可以将该进程持有的锁资源释放,但发展到分布式环境,这就变成了一个挑战,为了应对各种机器故障、宕机等,就需要给锁提供了一个新的特性:可用性。
如下图所示,任何提供三个特性的服务都可以提供分布式锁的能力,资源可以是文件、KV 等,通过创建文件、KV 等原子操作,通过创建成功的结果来表明所有权的归属,同时通过 TTL 或者会话来保证锁的可用性。
分布式锁的特性和实现
二 分布式锁的系统分类
根据锁资源本身的安全性,我们将分布式锁分为两个阵营:
基于异步复制的分布式系统,存在数据丢失(丢锁)的风险,不够安全,往往通过 TTL 的机制承担细粒度的锁服务,该系统接入简单,适用于对时间很敏感,期望设置一个较短的有效期,执行短期任务,丢锁对业务影响相对可控的服务。
基于 paxos 协议的分布式系统,通过一致性协议保证数据的多副本,数据安全性高,往往通过租约(会话)的机制承担粗粒度的锁服务,该系统需要一定的门槛,适用于对安全性很敏感,希望长期持有锁,不期望发生丢锁现象的服务。
三 阿里云存储分布式锁
阿里云存储在长期的实践过程中,在如何提升分布式锁使用时的正确性、保证锁的可用性以及提升锁的切换效率方面积累比较多的经验。
1 严格互斥性
互斥性作为分布式锁最基本的要求,对用户而言就是不能出现“一锁多占”,那么存储分布式锁是如何避免该情况的呢?
答案是,服务端每把锁都和唯一的会话绑定,客户端通过定期发送心跳来保证会话的有效性,也就保证了锁的拥有权。当心跳不能维持时,会话连同关联的锁节点都会被释放,锁节点就可以被重新抢占。这里有一个关键的地方,就是如何保证客户端和服务端的同步,在服务端会话过期的时候,客户端也能感知。
如下图所示,在客户端和服务端都维护了会话的有效期的时间,客户端从心跳发送时刻(S0)开始计时,服务端从收到请求(S1)开始计时,这样就能保证客户端会先于服务端过期。 用户在创建锁之后,核心工作线程在进行核心操作之前可以判断是否有足够的有效期,同时我们不再依赖墙上时间,而是基于系统时钟来对时间进行判断,系统时钟更加精确,且不会向前或者向后移动(秒级别误差毫秒级,同时在 NTP 跳变的场景,最多会修改时钟的速率)。
存储场景的使用方式
在分布式锁互斥性上,我们是不是做到完美了?并非如此,还是存在一种情况,业务基于分布式锁服务的访问互斥会被破坏。
我们来看下面的例子:如下图所示,客户端在时间点(S0)尝试去抢锁,在时间点(S1)在后端抢锁成功,因此也产生了一个分布式锁的有效期窗口。在有效期内,时间点(S2)做了一个访问存储的操作,很快完成,然后在时间点(S3)判断锁的有效期依旧成立,继续执行访问存储操作,结果这个操作耗时良久,超过了分布式锁的过期时间,那么可能这个时候,分布式锁已经被其他客户端抢占成功,进而出现两个客户端同时操作同一批数据的可能性,这种可能性是存在的,虽然概率很小。
越界场景
针对这个场景,具体的应对方案是在操作数据的时候确保有足够的锁有效期窗口,当然如果业务本身提供回滚机制的话,那么方案就更加完备,该方案也在存储产品使用分布式锁的过程中被采用。
还有一个更佳的方案,即,存储系统本身引入 IOFence 能力。这里就不得不提 Martin Kleppmann 和 redis 的作者 antirez 之间的讨论了。redis 为了防止异步复制导致的锁丢失的问题,引入了 redlock,该方案引入了多数派的机制,需要获得多数派的锁,最大程度的保证了可用性和正确性,但仍然有两个问题:
墙上时间可以通过非墙上时间 MonoticTime 来解决(redis 目前仍然依赖墙上时间),但是异构系统只有一个系统并没有办法保证完全正确。如下图所示,Client1 获取了锁,在操作数据的时候发生了 GC,在 GC 完成时候丢失了锁的所有权,造成了数据不一致。
异构系统无法做到完全正确性
因此需要两个系统同时协作来完成一个完全正确的互斥访问,在存储系统引入 IOFence能力,如下图所示,全局锁服务提供全局自增的 token,Client 1 拿到锁返回的 token 是 33,并带入存储系统,发生 GC,当 Client 2 抢锁成功返回 34,带入存储系统,存储系统会拒绝 token 较小的请求,那么经过了长时间 full gc 重新恢复后的 Client 1 再次写入数据的时候,因为存储层记录的token 已经更新,携带 token 值为 33 的请求将被直接拒绝,从而达到了数据保护的效果(chubby 的论文中有讲述,也是 Martin Kleppmann 提出的解决方案)。
引入IOFence能力
这与阿里云分布式存储平台盘古的设计思路不谋而合,盘古支持了类似 IO Fence 的写保护能力,引入 Inline File 的文件类型,配合 SealFile 操作,这就有着类似 IO Fence 的写保护能力。首先,SealFile 操作用来关闭已经打开的 cs 上面的文件,防止旧的 Owner 继续写数据;其次,InlineFile 可以防止旧的 Owner 打开新的文件。这两个功能事实上也是提供了存储系统中的 Token 支持。
2 可用性
存储分布式锁通过持续心跳来保证锁的健壮性,让用户不用投入很多精力关注可用性,但也有可能异常的用户进程持续占据锁。针对该场景,为了保证锁最终可以被调度,提供了可以安全释放锁的会话加黑机制。
当用户需要将发生假死的进程持有的锁释放时,可以通过查询会话信息,并将会话加黑,此后,心跳将不能正常维护,最终导致会话过期,锁节点被安全释放。这里我们不是强制删除锁,而是选用禁用心跳的原因如下:
3 切换效率
当进程持有的锁需要被重新调度时,持有者可以主动删除锁节点,但当持有者发生异常(如进程重启,机器宕机等),新的进程要重新抢占,就需要等待原先的会话过期后,才有机会抢占成功。默认情况下,分布式锁使用的会话生命期为数十秒,当持有锁的进程意外退出后(未主动释放锁),最长需要经过很长时间锁节点才可以被再次抢占。
客户端和服务各自维护过期时间
要提升切换精度,本质上要压缩会话生命周期,同时也意味着更快的心跳频率,对后端更大的访问压力。我们通过对进行优化,使得会话周期可以进一步压缩。
同时结合具体的业务场景,例如守护进程发现锁持有进程挂掉的场景,提供锁的 CAS 释放操作,使得进程可以零等待进行抢锁。比如利用在锁节点中存放进程的唯一标识,强制释放已经不再使用的锁,并重新争抢,该方式可以彻底避免进程升级或意外重启后抢锁需要的等待时间。
四 结语
分布式锁提供了分布式环境下共享资源的互斥访问,业务或者依赖分布式锁追求效率提升,或者依赖分布式锁追求访问的绝对互斥。同时,在接入分布式锁服务过程中,要考虑接入成本、服务可靠性、分布式锁切换精度以及正确性等问题,正确和合理的使用分布式锁,是需要持续思考并予以优化的。
参考文章
How to do distributed locking – Martin KleppmannIs Redlock safe? – antirezchubby 论文 – google
怎么用组策略锁定WindowsXP系统分区?

一、限制驱动器的使用 如果我们不想让别人使用我们的驱动器,来查看我们比较重要和隐私的文件,或是修改删除系统文件,如安装系统的C盘或是存有一些重要文件的盘符,我们可以通过以下操作来限制某个重要驱动器使用。 1、点击开始,在菜单中点击“运行”,系统会打开一个“运行”对话框,在此对话框中输入“”,点击确定或按回车键。 系统会自动弹出一个“组策略”的窗口。 2、在“组策略”窗口中,我们可以看到在左边的窗口的“‘本地计算机’策略下面有“计算机配置”和“用户配置”两个选项,我们这里选择“用户配置”选项,并依次打开“用户配置”、“管理模板”、“WINDOWS组件”、“WINDOWS资源管理器”选项。 3、这时我们会发现,在整个窗口的右边窗口中,出现了一些选项供我们选择,那么接下来我们在右边的设置窗口中,选择“防止从‘我的电脑’访问驱动器”项,并在这个选项上单击鼠标右键,选择“属性”,接着出现“防止从‘我的电脑’访问驱动器的属性”设置窗口,在其中有三个选项,分别是“未配置”“已启用”“已禁用”,我们系统默认的为“未配置”选项,接下来我们要对这个对话框中的参数进行设置了。 4、选择“已启用”后可以看到,在下面就会出现选择驱动器的下拉列表,如果希望限制某个驱动器的使用,只要选中该驱动器就可以了。 比如我们要限制C盘的使用,选中“仅限制驱动器C”,就可以了。 如果希望关闭所有驱动器,可以选择“限制所有驱动器”。 选择好要限制别人访问的驱动器,最后点确定或应用按钮即完成了整个设置过程。 设置完成后,让我们试试看是不是我们设置的驱动器受到了限制。 打开设置受限的驱动器,系统自动弹出禁止使用打开驱动器的提示框。 由此看出,我们的设置生效了。 即便是使用DIR命令、运行对话框和网络驱动器对话框,也无法查看此驱动器的目录和数据。 二、隐藏驱动器 如果我们想彻底隐藏我们的驱动器,方法也非常简单,为了方便使大家彻底弄明白,我们也进行详细的介绍一下操作步骤: 1、点击开始,在菜单中点击“运行”,系统会打开一个“运行”对话框,在此对话框中输入“”,点击确定或按回车键。 系统会自动弹出一个“组策略”的窗口。 2、选择“用户配置”选项,并依次打开“用户配置”、“管理模板”、“WINDOWS组件”、“WINDOWS资源管理器”选项。 3、在“WINDOWS资源管理器”右边的设置窗口中,选择“隐藏我的电脑中这些指定的驱动器”项同样在它属性中,有三项选择,我们选择“已启用”项,然后在下面的下拉列表中选择需要隐藏的相应驱动器。 设置完成后,我们回到WIN桌面下,打开我的电脑,这时是不是会发现我们设置好的驱动器盘符不见,到这里,整个设置过程就完成了。 通过以上两个简单的设置,我们便可以对我们电脑中的私人文件和重要数据放心的存放了。 也不必再为别人随意用我们光驱而感到心痛了。 需要注意是,虽然我们隐藏了驱动器,但用户仍然能够从其它程序访问这些驱动器和驱动器中的数据。 比如使用WORD的“打开”对话框,能够打开我们所隐藏的驱动器。 同时使用WINXP的磁盘管理工具也能对隐藏的驱动器进行查看和修改。 这和限制驱动器使用是一样的
怎么用系统加密文件夹?
命令法1、按“win+R”组合键打开“运行”对话框,输入“cmd”打开命令提示符窗口。 然后输入“md c:\jm..\”并按回车键,在C盘(根据实际目录)下新建一个名为“jm.”的文件夹。 这个文件夹就是我们用来存放隐藏文件的文件夹。 2、再次打开“运行”,输入上面新建的加密文件夹“c:\jm..\”,打开后把要隐藏的文件或文件夹放在里面。 现在,包括你在内的任何人都无法通过双击打开它,双击之后会弹出一个错误的方框。 只有通过打开“运行”对话框的途径打开它。 如果要删除它,只要将里面的全部内容删掉,再回到命令提示符输入“fd c:\jm..\”即可。
java中的这些名词都是什么?
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是一组在Java应用中访问命名和目录服务的API。 命名服务将名称和对象联系起来,使得我们可以用名称访问对象。 目录服务是一种命名服务,在这种服务里,对象不但有名称,还有属性。 jms即Java消息服务(Java Message Service)应用程序接口是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。 Java消息服务是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。 jms同时也可以指Journal of Marketing Science,《营销科学学报》的简称。 此外,佳木斯、姐妹们的拼音缩写也是jms。 JTA,即Java Transaction API,译为Java事务API。 JTA允许应用程序执行分布式事务处理——在两个或多个网络计算机资源上访问并且更新数据。 JDBC驱动程序的JTA支持极大地增强了数据访问能力。 JAF,即为JavaBeans Activation Framework的缩写。 Mail API 的所有版本都需要 JavaBeans Activation Framework 来支持任意数据块的输入及相应处理。 功能似乎不多,但目前许多浏览器和邮件工具中都能找到这种基本的 MIME 型支持。 文件就是JAF的框架jar包。 JAF是一个专用的数据处理框架,它用于封装数据,并为应用程序提供访问和操作数据的接口。 JAF的主要作用在于让java应用程序知道如何对一个数据源进行查看、编辑和打印等操作。 对于通过JAF封装的数据,应用程序通过JAF提供的接口可以完成如下功能: 1、访问数据源中的数据. 2、获知数据源的数据类型. 3、获知可对数据进行的各种操作. 4、用户对数据执行某种操作时,自动创建执行该操作的软件部件的实例对象. javaMail API可以利用JAF从某种数据源中读取数据和获知数据的MIME类型,并用这些数据生成MIME消息中的消息体和消息类型。 RMI是Java的一组拥护开发分布式应用程序的API。 RMI使用Java语言接口定义了远程对象,它集合了Java序列化和Java远程方法协议(Java Remote Method Protocol)。 简单地说,这样使原先的程序在同一操作系统的方法调用,变成了不同操作系统之间程序的方法调用,由于J2EE是分布式程序平台,它一RMI机制实现程序组件在不同操作系统之间的通信。 比如,一个EJB可以通过RMI调用Web上另一台机器上的EJB远程方法。 RMI(Remote Method Invocation,远程方法调用)是用Java在JDK1.1中实现的,它大大增强了Java开发分布式应用的能力。 Java作为一种风靡一时的网络开发语言,其巨大的威力就体现在它强大的开发分布式网络应用的能力上,而RMI就是开发百分之百纯Java的网络分布式应用系统的核心解决方案之一。 其实它可以被看作是RPC的Java版本。 但是传统RPC并不能很好地应用于分布式对象系统。 而Java RMI 则支持存储于不同地址空间的程序级对象之间彼此进行通信,实现远程对象之间的无缝远程调用。 SOAP:简单对象访问协议,简单对象访问协议(SOAP)是一种轻量的、简单的、基于 XML 的协议,它被设计成在 WEB 上交换结构化的和固化的信息。 SOAP 可以和现存的许多因特网协议和格式结合使用,包括超文本传输协议( HTTP),简单邮件传输协议(SMTP),多用途网际邮件扩充协议(MIME)。 它还支持从消息系统到远程过程调用(RPC)等大量的应用程序。
发表评论