同一个jvm里多个线程操做同一个有状态的变量,能够经过JVM内的锁保证线程安全。redis
若是是多个JVM操做同一个有状态的变量,如何保证线程安全呢?算法
这时候就须要分布式锁来发挥它的做用了数据库
分布式系统每每业务流量比较大、并发较高,对分布式锁的高可用和高性能有较高的要求。通常分布式锁的方案须要知足以下要求:缓存
利用主键惟一的特性,若是有多个请求同时提交到数据库的话,数据库会保证只有一个插入操做能够成功,那么咱们就能够认为操做成功的那个线程得到了该方法的锁,当方法执行完毕以后,想要释放锁的话,删除这条数据库记录便可安全
connection.commit()
操做来释放锁通常数据库使用innodb存储引擎,在插入数据的时候会加行级锁。从而达到是并发请求按顺序执行的效果bash
更新数据的时候带上指定版本号,若是被其余线程提早更新的版本号,则这次更新失败服务器
对数据库表侵入较大,每一个表须要增长version字段网络
高并发下存在不少更新失败并发
原子命令:SET key value NX PX millisecondsmvc
PX milliseconds 过时时间,防止加锁线程死掉不能解锁。过时时间设置过短,可能加锁线程尚未执行完正常逻辑,就到了过时时间
NX 若是没有这个key则设置,存在key返回失败
value 随机值(通常用UUID),用来实现只能由加锁线程解锁
lua脚本实现get value,delete的操做。加锁的时候设置的value是不会重复的随机值,解锁的时候必须UUID一致才能解锁
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.3.2</version>
</dependency>
复制代码
在Redis的分布式环境中,咱们假设有N个Redis master。这些节点彻底互相独立,不存在主从复制或者其余集群协调机制。咱们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。如今咱们假设有5个Redis master节点,同时咱们须要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。
假设有cluster-1,cluster-2,cluster-3总计3个cluster模式集群。若是要获取分布式锁,那么须要向这3个cluster集群经过EVAL命令执行LUA脚本,须要3/2+1=2,即至少2个cluster集群响应成功。set的value要具备惟一性,redisson的value经过UUID+threadId保证value的惟一性
1.获取当前时间(单位是毫秒)。
2.轮流用相同的key和随机值在N个节点上请求锁,在这一步里,客户端在每一个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。好比若是锁自动释放时间是10秒钟,那每一个节点锁请求的超时时间多是5-50毫秒的范围,这个能够防止一个客户端在某个宕掉的master节点上阻塞过长时间,若是一个master节点不可用了,咱们应该尽快尝试下一个master节点。
3.客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),并且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。
4.若是锁获取成功了,那如今锁自动释放时间就是最初的锁释放时间减去以前获取锁所消耗的时间。
5.若是锁获取失败了,无论是由于获取成功的锁不超过一半(N/2+1)仍是由于总消耗时间超过了锁释放时间,客户端都会到每一个master节点上释放锁,即使是那些他认为没有获取成功的锁。
须要在全部节点都释放锁就行,无论以前有没有在该节点获取锁成功。
客户端若是没有在多数节点获取到锁,必定要尽快在获取锁成功的节点上释放锁,这样就不必等到key超时后才能从新获取这个锁
开始以前,让咱们假设客户端能够在大多数节点都获取到锁,这样全部的节点都会包含一个有相同存活时间的key。可是须要注意的是,这个key是在不一样时间点设置的,因此这些key也会在不一样的时间超时,可是咱们假设最坏状况下第一个key是在T1时间设置的(客户端链接到第一个服务器时的时间),最后一个key是在T2时间设置的(客户端收到最后一个服务器返回结果的时间),从T2时间开始,咱们能够确认最先超时的key至少也会存在的时间为MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT,TTL是锁超时时间、(T2-T1)是最晚获取到的锁的耗时,CLOCK_DRIFT是不一样进程间时钟差别,这个是用来补偿前面的(T2-T1)。其余的key都会在这个时间点以后才会超时,因此咱们能够肯定这些key在这个时间点以前至少都是同时存在的。
若是一个客户端获取大多数节点锁的耗时接近甚至超过锁的最大有效时间时(就是咱们为SET操做设置的TTL值),那么系统会认为这个锁是无效的同时会释放这些节点上的锁,因此咱们仅仅须要考虑获取大多数节点锁的耗时小于有效时间的状况。在这种状况下,根据咱们前面的证实,在MIN_VALIDITY时间内,没有客户端能从新获取锁成功,因此多个客户端都能同时成功获取锁的结果,只会发生在多数节点获取锁的时间都大大超过TTL时间的状况下,实际上这种状况下这些锁都会失效
利用临时节点与 watch 机制。每一个锁占用一个普通节点 /lock,当须要获取锁时在 /lock 目录下建立一个临时节点,建立成功则表示获取锁成功,失败则 watch/lock 节点,有删除操做后再去争锁。临时节点好处在于当进程挂掉后能自动上锁的节点自动删除即取消锁。
全部取锁失败的进程都监听父节点,很容易发生羊群效应,即当释放锁后全部等待进程一块儿来建立节点,并发量很大。
上锁改成建立临时有序节点,每一个上锁的节点均能建立节点成功,只是其序号不一样。只有序号最小的能够拥有锁,若是这个节点序号不是最小的则 watch 序号比自己小的前一个节点 (公平锁)。
在锁节点下建立临时顺序节点。读节点为R+序号,写节点为W+序号。建立完节点后,获取全部子节点,对锁节点注册子节点变动的watcher监听,肯定本身的序号在全部子节点中的位置。对于读请求,没有比本身序号小的写节点,就表示得到了共享锁,执行读取逻辑。对于写请求,若是本身不是序号最小的子节点,就须要进入等待。接收到watcher通知后,重复获取锁。
共享锁羊群效应。大量的watcher通知和子节点列表获取,两个操做重复运行。集群规模比较大的状况下,会对zookeeper服务器形成巨大的性能影响和网络冲击
读请求,监听比本身小的写节点。写请求,监听比本身小的最后一个节点。