1.为何用分布式锁redis
咱们在开发应用的时候,若是须要对某一个共享变量进行多线程同步访问的时候,可使用咱们学到的Java多线程的18般武艺进行处理,而且能够完美的运行,毫无Bug!spring
注意这是单机应用,也就是全部的请求都会分配到当前服务器的JVM内部,而后映射为操做系统的线程进行处理!而这个共享变量只是在这个JVM内部的一块内存空间!数据库
后来业务发展,须要作集群,一个应用须要部署到几台机器上而后作负载均衡,大体以下图:服务器
上图能够看到,变量A存在JVM一、JVM二、JVM3三个JVM内存中(这个变量A主要体现是在一个类中的一个成员变量,是一个有状态的对象,例如:UserController控制器中的一个整形类型的成员变量),若是不加任何控制的话,变量A同时都会在JVM分配一块内存,三个请求发过来同时对这个变量操做,显然结果是不对的!即便不是同时发过来,三个请求分别操做三个不一样JVM内存区域的数据,变量A之间不存在共享,也不具备可见性,处理的结果也是不对的!多线程
若是咱们业务中确实存在这个场景的话,咱们就须要一种方法解决这个问题!并发
为了保证一个方法或属性在高并发状况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的状况下,可使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。在单机环境中,Java中提供了不少并发处理相关的API。可是,随着业务发展的须要,原单体单机部署的系统被演化成分布式集群系统后,因为分布式系统多线程、多进程而且分布在不一样机器上,这将使原单机部署状况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就须要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!负载均衡
2.分布式锁的实现方式分布式
2.1 基于数据库的实现高并发
1.解决方式:设计一个独立的表结构来进行分布式锁信息的存储,在相对应获取锁的时候进行添加锁信息并持有锁,直到对应的业务操做结束以后进行锁的释放工做工具
2.设计实现:
方案一:
列名 | 类型 | 描述 |
---|---|---|
id | bigint | 主键 |
method_name | varchar(64) | 方法名,创建惟一性索引 |
desc | varchar(1024) | 备注信息 |
update_time | datetime | 更新时间 |
out_date_time | datetime | 失效时间 |
实现原理:
获取锁:获取锁的时候,对象对表结构进行添加数据,用来标志锁信息。
释放锁:删除对应的数据,标志锁的释放
方案二:
列名 | 类型 | 描述 |
---|---|---|
id | bigint | 主键 |
method_name | varchar(64) | 方法名,创建惟一性索引 |
desc | varchar(1024) | 备注信息 |
update_time | datetime | 更新时间 |
version | int | 版本号 |
state | int | 状态:0未分配,1已分配 |
out_date_time | datetime | 失效时间 |
(1) 获取锁
① 第一次获取的时候,相对应表结构没有数据,则加入一条数据,state为1,version为1,表明锁
② 若是数据库表中有对应锁的数据,先判断是否为未分配的锁信息,若是是,更新数据为已分配状态。
(2) 释放锁:更新state状态为未分配表明释放锁
3. 存在问题:
1. 数据库服务宕机的问题:数据库主从配置?
2. 并发低,锁记录包含不少冗余信息
2.2 基于redis的实现
2.2.1 基于redis的实现
1.命令介绍:
将key的值设置为value,当且key不存在的时候
若是key存在,则setNX没有效果
GETSET key value:将给key的值设为value,并返回key的旧值(old value)。
GET key:获取key对应的值
EXPIRE key seconds:给定key设置生存时间,当生存时间到的时候, key将被自动删除
实现方式:
加锁:
复制代码
采用redis中setNX的功能来进行锁信息的保存,进行持有锁的操做,其中锁key值为锁的名字,value采用uuid或者requestId来进行保存。当setNX成功以后,设定相对应的过时时间,来实现redis锁的自动释放功能
解锁
复制代码
(1) 超时自动解锁
(2) 手动解锁:当业务代码执行完毕以后,经过相对应的key进行原有的锁的获取,经过del进行删除动做。
存在问题:
1. Redis锁的setNX和Expire不是原子操做,可能会发生setNx执行成功以后应用宕机,致使死锁。(可行的解决办法,锁的值设置为过时时间,获取锁的时候,比对锁是否过时了,过时的锁用GETSET****从新设值)
2. Redis锁为了不单点问题,须要作主备或者集群,当主设置完锁而且数据没有同步到备的时候,主挂掉了,备切换成为主,这个时候新的主里面是没有这个锁的,会有并发问题。(归根结底这个问题是因为Redis是ap模型致使的,因而引入了下面基于cp****模型的分布式锁)
2.2.2 借助工具类Redisson进行分布式锁开发
1.原理:Redisson锁的原理其实就是redis的实现方式,只是Redisson提供 了更方便的封装,让咱们进行使用
2.redisson锁介绍
1.可重入锁(Reentrant Lock)
2.公平锁(Fair Lock)
3.联锁(MultiLock)
4.红锁(Red Lock)
5.读写锁(ReadWriteLock)
3.redisson分布式锁实现
1.加锁
1.经过redissonClient获取对应Rlock锁对象,而后根据Rlock提供的方法进行锁操做,具体Rlock提供的锁方法以下:
void lock(Long leaseTime,TimeUnit unit )
boolean tryLock(Long waitTime,Long leaseTime,TimeUnit unit)
2.解锁
根据Rlock提供的unlock进行解锁
存在问题:
一、 相较于简单的Redis锁,redisson锁种类更多,可是相应的须要依赖不少其余组件,比较重
二、 由于基于ap模型的redis,因此也存在数据不一致的问题
2.3 基于zookeeper的实现
1.zookeeper节点四种类型:
1.持久节点(PERSISTENT)
2.持久顺序节点(PERSISTENT_SEQUENTIAL)
3.临时节点(EPHEMERAL)
4.临时顺序节点(EPHEMERAL_SEQUENTIAL)
2.实现原理:
1. 获取锁
首先,在Zookeeper当中建立一个持久节点ParentLock。当第一个客户端想要得到锁时,须要在ParentLock这个节点下面建立一个临时顺序节点 Lock1。
以后,Client1查找ParentLock下面全部的临时顺序节点并排序,判断本身所建立的节点Lock1是否是顺序最靠前的一个。若是是第一个节点,则成功得到锁。
这时候,若是再有一个客户端 Client2 前来获取锁,则在ParentLock下载再建立一个临时顺序节点Lock2。
Client2查找ParentLock下面全部的临时顺序节点并排序,判断本身所建立的节点Lock2是否是顺序最靠前的一个,结果发现节点Lock2并非最小的。
因而,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。
Client3查找ParentLock下面全部的临时顺序节点并排序,判断本身所建立的节点Lock3是否是顺序最靠前的一个,结果一样发现节点Lock3并非最小的。
因而,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3一样抢锁失败,进入了等待状态。
这样一来,Client1获得了锁,Client2监听了Lock1,Client3监听了Lock2。这偏偏造成了一个等待队列,很像是Java当中ReentrantLock所依赖的
2. 释放锁
释放锁分为两种状况:
1.任务完成,客户端显示释放
当任务完成时,Client1会显示调用删除节点Lock1的指令。
2.任务执行过程当中,客户端崩溃
得到锁的Client1在任务执行过程当中,若是Duang的一声崩溃,则会断开与Zookeeper服务端的连接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。
因为Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会马上收到通知。这时候Client2会再次查询ParentLock下面的全部节点,确认本身建立的节点Lock2是否是目前最小的节点。若是是最小,则Client2瓜熟蒂落得到了锁。
同理,若是Client2也由于任务完成或者节点崩溃而删除了节点Lock2,那么Client3就会接到通知。
最终,Client3成功获得了锁。
存在问题:
1. 写并发不高,由于zk写入数据的时候,是要过半节点写入成功才算写成功
2. 读并发相较于redis锁也有很大差距
2.4 基于etcd的实现
2.5 基于consul的实现