分布式锁实现方式

简介

多个实例运行下对共用数据操作这时候我们普通的多线程就无法解决了,需要分布式锁来保证不同实例之间的同一时刻操作同一对象只有一个
分布式锁一般有三种实现方式:

  1. 数据库锁;
  2. 基于Redis的分布式锁;
  3. 基于ZooKeeper的分布式锁。
分布式锁应该是怎么样的
•互斥性 可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
•这把锁要是一把可重入锁(避免死锁)
•不会发生死锁:有一个客户端在持有锁的过程中崩溃而没有解锁,也能保证其他客户端能够加锁
•这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条)
•有高可用的获取锁和释放锁功能
•获取锁和释放锁的性能要好

数据库锁

基于数据库表(悲观锁)说白了就是利用数据库表字段唯一性插入一条数据其他实例也插入同
一条数据,显然只有一个线程成功插入这个实例就是拿到锁的实例,删除这条数据就是释放锁,
那么这种方式的问题在哪里?

上面这种简单的实现有以下几个问题:
1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

改进
在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁
。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。

我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,
可以执行方法的业务逻辑,执行完方法之后,再通过以下方法解锁:
public void unlock(){   
   connection.commit();
} 


通过connection.commit()操作来释放锁。
但是还是无法直接解决数据库单点和可重入问题。


再次改进
可以加入版本好字段,这样就可以多实例读,但在做改操作时对比自己设置的版本号,不一样
则认为是备操作过了则重新来过,

总结

性能对比(乐观锁)
(1) 悲观锁实现方式是独占数据,其它线程需要等待,不会出现修改的冲突,能够保证数据的一致性,但是依赖数据库的实现,且在线程较多时出现等待造成效率降低的问题。一般情况下,对于数据很敏感且读取频率较低的场景,可以采用悲观锁的方式
(2) 乐观锁可以多线程同时读取数据,若出现冲突,也可以依赖上层逻辑修改,能够保证高并发下的读取,适用于读取频率很高而修改频率较少的场景
(3) 由于库存回写数据属于敏感数据且读取频率适中,所以建议使用悲观锁优化

基于redis的分布式锁

不用说就是它
加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time)

这个set()方法一共有五个形参:
•第一个为key,我们使用key来当锁,因为key是唯一的。
•第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。
•第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
•第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
•第五个为time,与第四个参数相呼应,代表key的过期时间。

注意:setnx()这个方法是之前老版本的方法,新的就用set就行了,还保证了团子操作

解锁:
首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)

总结

使用缓存实现分布式锁的优点
性能好,实现起来较为方便。
使用缓存实现分布式锁的缺点
通过超时时间来控制锁的失效时间并不是十分的靠谱。

基于Zookeeper实现分布式锁

基于zookeeper临时有序节点可以实现的分布式锁。大致思想即为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的
瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导
致的锁无法释放,而产生的死锁问题。

来看下Zookeeper能不能解决前面提到的问题。
•锁无法释放?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(
Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。
•非阻塞锁?使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户
端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。
•不可重入?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的
时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。
•单点问题?使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。

可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。
Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并没有缓存服务那么高。
因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。
**使用Zookeeper实现分布式锁的优点: **有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。
**使用Zookeeper实现分布式锁的缺点 : **性能上不如使用缓存实现分布式锁。 需要对ZK的原理有所了解。
*

最终总结

从理解的难易程度角度(从低到高): 数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)Zookeeper >= 缓存 > 数据库
从性能角度(从高到低): 缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低): Zookeeper > 缓存 > 数据库

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值