一、为什么会有分布式锁:
在分布式系统中,同一个业务可能有多个tomcat,那么这个业务中存储的静态变量,就会存在于多个jvm内存中。高并发情况下不做处理,就会出现数据不一致的问题。
引入分布式锁来解决这个问题,分布式锁需要有一下特性:
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
二、分布式锁的实现方式:
有三种方式可以实现分布式锁:1.数据库的排它锁来实现 2. redis的setnx来实现分布式锁 3.zookeeper来实现分布式锁
1.数据库来实现:在数据中创建一张表来实现分布式锁。如下:
分布式锁的实现方式有三种:
1.数据库(mysql)来实现:利用数据库的排它锁来实现
、、、
DROP TABLE IF EXISTS method_lock
;
CREATE TABLE method_lock
(
id
int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
method_name
varchar(64) NOT NULL COMMENT '锁定的方法名',
state
tinyint NOT NULL COMMENT '1:未分配;2:已分配',
update_time
timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
version
int NOT NULL COMMENT '版本号',
PRIMARY KEY (
id), UNIQUE KEY
uidx_method_name(
method_name`) USING BTREE)
占有锁
update t_resoure set state=2, version=2, update_time=now() where method_name='methodName' and state=1 and version=2;
、、、
如果没有更新影响到一行数据,则说明这个资源已经被别人占位了。
缺点:
1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
解决方案:
1、数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
2、没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
3、非阻塞的?搞一个while循环,直到insert成功再返回成功。
4、非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
2.通过redis来实现分布式锁:加锁 、解锁
1.用setnx(SET IF NOT EXIST)来加锁 :setnx method_name value 当执行成功,返回1 说明加锁成功。返回0则说明加锁失败。执行完业务后通过del 删除key 即可释放锁。
2.但是如果业务执行过程中获取锁后挂掉,来不及释放锁,那么就会造成死锁。解决办法:设置过期时间, setnx method_name value , expire key 3000 , del key . 存在问题:获取锁后还没来得及设置过期时间,程序崩了。(三个操作,不具备原子性)还是会造成死锁。 通过:set method_name value NX PX 3000 .
3.在高并发情况下,一个线程获取锁,设置完过期时间后,业务执行时间过长,超过过期时间。自动释放锁,在当前业务执行完后,再通过del释放锁,实际释放的是另一个线程的锁。解决方案:
设置value的值为当前线程的ID ,调用del的时候,先获取value的值,如果使当前线程则释放锁,否则不释放锁。 问题:还是存在两个或多个线程同时访问某一个资源
4.通过创建当前线程的守护线程来解决
在redis集群的情况下,引入redisson 框架来实现分布式锁:
1、RedissonManager类,管理redisson的初始化等操作。
2、DistributedRedisLock类,提供锁和解锁方法 (acquire 获取锁)(release释放锁)
通过zookeeper来实现分布式锁。
Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode。
1.持久节点 (PERSISTENT)
默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在 。
2.持久节点顺序节点(PERSISTENT_SEQUENTIAL)
所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号:
3.临时节点(EPHEMERAL)
和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除:
4.临时顺序节点(EPHEMERAL_SEQUENTIAL)
顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。
Zookeeper分布式锁的原理
Zookeeper分布式锁恰恰应用了临时顺序节点。具体如何实现呢?让我们来看一看详细步骤:
获取锁
首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。
之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。
这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2。
Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。
于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。
这时候,如果又有一个客户端Client3前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock3。
Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。
于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态。
这样一来,Client1得到了锁,Client2监听了Lock1,Client3监听了Lock2。这恰恰形成了一个等待队列,很像是Java当中ReentrantLock所依赖的
释放锁
释放锁分为两种情况:
1.任务完成,客户端显示释放
当任务完成时,Client1会显示调用删除节点Lock1的指令
2.任务执行过程中,客户端崩溃
获得锁的Client1在任务执行过程中,如果Duang的一声崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。
由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小,则Client2顺理成章获得了锁。
同理,如果Client2也因为任务完成或者节点崩溃而删除了节点Lock2,那么Client3就会接到通知。最终,Client3成功得到了锁。
方案:
可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。
Curator提供的InterProcessMutex是分布式锁的实现。acquire方法用户获取锁,release方法用于释放锁。
缺点:
1.性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。
2.其实,使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的情况,由于网络抖动,客户端可ZK集群的session连接断了,那么zk以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。就可能产生并发问题。这个问题不常见是因为zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。(所以,选择一个合适的重试策略也比较重要,要在锁的粒度和并发之间找一个平衡。)
三种方案的比较
上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。
从理解的难易程度角度(从低到高)
数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)
Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)
缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)
Zookeeper > 缓存 > 数据库
原文链接:https://blog.csdn.net/wuzhiwei549/article/details/80692278
网友评论