应用场景
具体的应用场景
为什么要使用分布式锁
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设置超时时间,即便发生了宕机的情况,也不会将资源一直占用,可以避免死锁的问题。
总结
缓存的方案,实现原理都差不多,就是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.hollischuang.com/archives/17…
wudashan.cn/2017/10/23/… //redis源码实现分布式锁