欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

分布式锁

程序员文章站 2022-07-05 11:51:14
...

应用场景

具体的应用场景

为什么要使用分布式锁

1.单机
同一个进程里的不同线程,访问同一个数据。

不同的线程,为什么会访问同一个数据呢?
比如,
1.单例对象
2.数据库里的数据

这些情况,都可能会访问同一个数据。


2.多机
就是不同的进程,访问同一个数据。

不同的进程,为什么会访问同一个数据呢? 比如,数据库里的数据,缓存里的数据。

缓存里的数据?之前阿里面试支付,解决幂等问题的时候,就有问到这个问题。面试官想要问的就是,分布式锁。

解决方案

首先,得明白一个问题,就是这个锁,必须是存储在所有进程都能够访问的地方。然后,具体访问的时候,同一时刻只有一个进程可以拿到这个锁。


有三种
1.基于数据库
mysql
2.基于缓存
redis
set命令
3.基于分布式协调
zookeeper存储service的目录服务。其实,也就是利用了第三方软件存储数据的功能,不管那个第三方软件是数据库、缓存、分布式协调,本质上都是为了哪一个第三方中介软件存储这个锁,使得所有进程都可以访问到这个锁,至于具体是什么软件,还真不要紧。

基于数据库mysql

其实,有好几种解决方案
1.基于键的唯一性约束
2.基于行锁

基于键的唯一性约束

比如,设计一张表,现在表里面有个方法字段,方法字段唯一性约束。
写的时候,如果有多线程访问,只有一个线程能写数据成功,其他失败。

很明显,这样会带来很多问题?比如,其他的线程不是失败,而是必须阻塞获取锁。如何阻塞?可以使用while循环。

基于排它锁/行锁

实现原理
就是查询数据库记录的时候,数据库会对当前查询上排他锁,其他查询不能查询——直到别的查询执行完毕释放锁。


步骤
1.进程1查询记录
获取到锁,执行接下来的代码。

2.进程2查询同一个记录 //比如,每个方法,弄一条记录
阻塞直到获取到锁。
因为进程1执行完以下几个步骤1.执行查询锁,相当于是获取到了锁2.执行业务代码3.释放锁connection.commit() 之后,进程2就可以获取到锁了。这个阻塞是数据库mysql本身支持的。


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

我们可以认为获得排他锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,通过connection.commit()操作来释放锁。

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

阻塞锁? for update语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功。 锁定之后服务宕机,无法释放?使用这种方式,服务宕机之后数据库会自己把锁释放掉。 但是还是无法直接解决数据库单点和可重入问题。

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

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

基于redis

基于 redis 的 setnx()、expire() 方法做分布式锁
setnx() setnx 的含义就是 SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果 key 不存在,则设置当前 key 成功,返回 1;如果当前 key 已经存在,则设置当前 key 失败,返回 0。

expire() expire 设置过期时间,要注意的是 setnx 命令不能设置 key 的超时时间,只能通过 expire() 来对 key 设置。

基于zookeeper

这种实践当中不常用。因为zookeeper主要是设计用来协调分布式服务的。

分布式锁必须实现以下几点要求

分布式锁服务一般需要能够保证:

1.排它锁
同一时刻只能有一个线程持有锁
2.阻塞
指的是,阻塞等待直到获取到锁。
具备阻塞锁特性,且能够及时从阻塞状态被唤醒。
3.可重入
锁能够可重入

4.不会发生死锁

5.锁服务保证高性能和高可用

memcache

使用memcached的add()方法,用于分布式锁。


具体细节
第四步,使用memcached的add()方法,用于分布式锁:

对于使用memcached的add()方法做分布式锁,这个在互联网公司是一种比较常见的方式,而且基本上可以解决自己手头上的大部分应用场景。

在使用这个方法之前,只要能搞明白memcached的add()和set()的区别,并且知道为什么能用add()方法做分布式锁就好。如果还不知道add()和set()方法,请直接百度吧,这个需要自己了解一下。

我在这里想说明的是另外一个问题,人们在关注分布式锁设计的好坏时,还会重点关注这样一个问题,那就是是否可以避免死锁问题???!!!

如果使用memcached的add()命令对资源占位成功了,那么是不是就完事儿了呢?当然不是!我们需要在add()的使用指定当前添加的这个key的有效时间,如果不指定有效时间,正常情况下,你可以在执行完自己的业务后,使用delete方法将这个key删除掉,也就是释放了占用的资源。但是,如果在占位成功后,memecached或者自己的业务服务器发生宕机了,那么这个资源将无法得到释放。所以通过对key设置超时时间,即便发生了宕机的情况,也不会将资源一直占用,可以避免死锁的问题。

mp.weixin.qq.com/s/UXyBhhpE3…


总结
缓存的方案,实现原理都差不多,就是API/方法是原子的/事务的,同一时间,只有一个线程操作成功。具体底层实现?待补充。

最佳实践

使用redis和memcache的原理差不多,也是互联网公司实践采用最多的方案。


常用的四种方案:

  1. 基于数据库表做乐观锁,用于分布式锁。

  2. 使用memcached的add()方法,用于分布式锁。

  3. 使用redis的setnx()、expire()方法,用于分布式锁。

  4. 使用redis的setnx()、get()、getset()方法,用于分布式锁。

  不常用但是可以用于技术方案探讨的:

  1. 使用memcached的cas()方法,用于分布式锁。 

  2. 使用redis的watch、multi、exec命令,用于分布式锁。

  3. 使用zookeeper,用于分布式锁。
复制代码

---附加---

应用场景

作用 解决什么问题

数据库事务只能保证单机事务 不能保证分布式多数据库事务 如何解决?使用分布式锁。

使用步骤

同时 只能有一个线程获取到锁。
1.单机事务的锁
编程语言层面 锁对应
数据库层面 支持单机事务

2.分布式事务的锁
同时 访问一个数据的时候 多个线程是互斥的 这样就实现了分布式锁。

如何实现/解决方案

数据库mysql

两种方法
1.数据库表
2.数据库本身的行锁

缓存redis

setnx
必须非空 才能插入成功 否则 插入失败


具体实现步骤
1.如何获取锁
set方法
2.如何释放锁
invalid方法


参考 redis.io/topics/dist… //官方文档 不知道说什么 有时间再看

协调zookooper

实现原理
1.锁目录
2.锁目录下的资源

开源组件Curator
1.获取锁
2.释放锁

参考

zhuanlan.zhihu.com/p/51042458 juejin.im/post/5bbb0d…

参考

mp.weixin.qq.com/s/UXyBhhpE3… //实践

www.infoq.cn/article/how…

www.hollischuang.com/archives/17…

zhuanlan.zhihu.com/p/42056183

wudashan.cn/2017/10/23/… //redis源码实现分布式锁

转载于:https://juejin.im/post/5d0794ba6fb9a07ea33c1668