分布式锁的三种实现⽅式,数据库分布式锁,Redis分布式锁,Zookeeper分布式锁各位⼩伙伴⼉, 上篇我们介绍了Java中的7类锁, 现在还有⼀个重头戏, 那就是分布式锁, 我们接着上篇的标题,继续探索~
8. 锁
8.1 为什么需要分布式锁
⾸先我们先了解⼀下分布式锁的使⽤场景, 然后再来理解为什么需要分布式锁, 那么我们举两个例⼦进⾏阐述:
银⾏转账问题: A在上海,B在北京同时在建⾏转账给杭州C,A转账时,会修改C处服务器的表,B不能在此刻转账,同理,B转账时,A不能做处理,A,B的转账操作时同步,必须保证数据的⼀致性,这就需要分布式锁来进⾏处理.
取任务问题: 某服务提供⼀组任务,A系统请求随机从任务组中获取⼀个任务;B系统请求随机从任务组中获取⼀个任务。 在理想的情况下,A从任务组中挑选⼀个任务,任务组删除该任务,B从剩下的的任务中再挑⼀个,任务组删除该任务。 同样的,在真实情况下,如果不做任何处理,可能会出现A和B挑中了同⼀个任务的情况。
真实开发中, 集模式下对某⼀个共享变量进⾏多线性同步访问:
1. 上图可以看到,变量A存在JVM1、JVM2、JVM3三个JVM内存中(这个变量A主要体现是在⼀个类中的⼀个成员变量,是⼀个有状
态的对象,例如:UserController控制器中的⼀个整形类型的成员变量).
2. 如果不加任何控制的话,变量A同时都会在JVM分配⼀块内存,三个请求发过来同时对这个变量操作,显然结果是不对的!即使不是
同时发过来,三个请求分别操作三个不同JVM内存区域的数据,变量A之间不存在共享,也不具有可见性,处理的结果也是不对的!
这种情况下就要应⽤分布式锁来解决了.
8.2 为什么分布式系统中不能⽤普通锁呢?普通锁和分布式锁有什么区别吗?
普通锁
1. 单⼀系统那个, 同⼀个应⽤程序是有同⼀个进程, 然后多个线程并发会造成数据安全问题, 他们是共享同⼀块内存的, 所以在内存某个
地⽅做标记即可满⾜需求.
2. 例如synchronized和volatile+cas⼀样对具体的代码做标记, 对应的就是在同⼀个内存区域作了同步的标记.
分布式锁
1. 分布式系统中, 最⼤的区别就是不同系统中的应⽤程序都在各⾃机器上不同的进程中处理的, 这⾥的线程不安全可以理解为多进程造成
的数据安全问题, 他们不会共享同⼀台机器的同⼀块内存区域, 因此需要将标记存储在所有进程都能看到的地⽅.
2. 例如zookeeper作分布式锁,就是将锁标记存储在多个进程共同看到的地⽅,redis作分布式锁,是将其标记公共内存,⽽不是某个进
程分配的区域.
8.3 分布式锁应该具备哪些条件
在分析分布式锁的三种实现⽅式之前,先了解⼀下分布式锁应该具备哪些条件:
在分布式系统环境下,⼀个⽅法在同⼀时间只能被⼀个机器的⼀个线程执⾏;
⾼可⽤的获取锁与释放锁;
⾼性能的获取锁与释放锁;
具备可重⼊特性;
具备锁失效机制,防⽌死锁;
具备⾮阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
8.4 分布式锁的三种实现⽅式
⽬前⼏乎很多⼤型⽹站及应⽤都是分布式部署的, 分布式场景中的数据⼀致性问题⼀直是⼀个⽐较重要的话题.
分布式的CAP理论告诉我们任何⼀个分布式系统都⽆法同时满⾜⼀致性(Consistency)、可⽤性(Availability)和分区容错性(Partition tolerance), 最多只能同时满⾜两项.
分布式锁, 是⼀种思想, 它的实现⽅式有很多种. ⽐如, 我们将沙滩当做分布式锁的组件, 那么它看起来应该是这样的:
1. 加锁: 在沙滩上踩⼀脚,留下⾃⼰的脚印,就对应了加锁操作。其他进程或者线程,看到沙滩上已经有脚印,证明锁已被别⼈持有,则
等待
2. 解锁: 把脚印从沙滩上抹去,就是解锁的过程
3. 锁超时: 为了避免死锁,我们可以设置⼀阵风,在单位时间后刮起,将脚印⾃动抹去
因此应运⽽⽣了三种实现分布式锁的⽅式:
1. 基于数据库实现分布式锁;
2. 基于缓存(Redis等)实现分布式锁;
3. 基于Zookeeper实现分布式锁;
尽管有这三种⽅案,但是不同的业务也要根据⾃⼰的情况进⾏选型,他们之间没有最好只有更适合!
8.4.1 数据库的分布式锁
基于表记录实现分布式锁
基于数据库的实现⽅式的核⼼思想是:
在数据库中创建⼀个表, 表中包含⽅法名等字段, 并在⽅法名字段上创建唯⼀索引. 想要执⾏某个⽅法, 就使⽤这个⽅法名向表中插⼊数据, 成功插⼊则获取锁, 执⾏完成后删除对应的⾏数据释放锁.
1. 创建⼀个表
2. 想要执⾏某个⽅法, 就使⽤这个⽅法名向表中插⼊数据:因为我们对method_name 做了唯⼀性约束, 这⾥如果有多个请求同事提交到数据库的话, 数据库会保证只有⼀个操作可以成功, 那么我们就可以认为操作成功的那个现场恒获得了该⽅法的锁, 可以执⾏⽅法体内容.
3. 成功插⼊则获取锁, 执⾏完成后删除对应的⾏数据释放锁
基于表记录实现分布式锁的特点
1. 这种锁没有失效时间, ⼀旦释放锁的操作失败就会导致锁记录⼀直在数据库中, 其他线程⽆法获得锁. 这个缺陷也很好解决, ⽐如可以做
⼀个定时任务去定时清理.
2. 这种锁的可靠性依赖于数据库, 建议设置备库, 避免单点, 进⼀步提⾼可靠性.
3. 这种锁是⾮阻塞的, 因为插⼊数据失败之后会直接报错, 想要获得锁就需要再次操作. 如果需要阻塞式的, 可以来个for循环或while循环,
直⾄INSERT成功再返回.
4. 这种锁也是⾮可重⼊的, 因为同⼀个线程在没有释放锁之前⽆法再次获得锁, 因为数据库中已经存在同⼀份记录了. 想要实现可重⼊锁,
可以在数据库中添加⼀些字段, ⽐如获得锁的主机信息、线程信息等, 那么在再次获得锁的时候可以先查询数据, 如果当前的主机信息和线程信息等能被查到的话, 可以直接把锁分配给它.
基于乐观锁实现分布式锁
系统认为数据的更新在⼤多数情况下是不会产⽣冲突的,只在数据库更新操作提交的时候才对数据作冲突检测。如果检测的结果出现了与预期数据不⼀致的情况,则返回失败信息.
乐观锁⼤多数是基于数据版本(version)的记录机制实现的. 即为数据增加⼀个版本标识.
1. 在基于数据库表的版本解决⽅案中, ⼀般是通过为数据库表添加⼀个 “version”字段来实现读取出数据时, 将此版本号⼀同读出, 之后
更新时, 对此版本号加1.
2. 在更新过程中, 会对版本号进⾏⽐较, 如果是⼀致的, 没有发⽣改变, 则会成功执⾏本次操作; 如果版本号不⼀致, 则会更新失败.
基于乐观锁的优点 在检测数据冲突时并不依赖数据库本⾝的锁机制, 不会影响请求的性能, 当产⽣并发且并发量较⼩的时候只有少部分请求会失败.
基于乐观锁的缺点 需要对表的设计增加额外的字段, 增加了数据库的冗余, 另外, 当应⽤并发量⾼的时候, version值在频繁变化, 则会导致⼤量请求失败, 影响系统的可⽤性.
综合数据库乐观锁的优缺点, 乐观锁⽐较适合并发量不⾼, 并且写操作不频繁的场景.
基于悲观锁实现分布式锁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 '锁定的⽅法名', `desc ` varchar (255) NOT NULL COMMENT '备注信息', `update_time ` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP , PRIMARY KEY (`id `), UNIQUE KEY `uidx_method_name ` (`method_name `) USING BTREE ) ENGINE =InnoDB AUTO_INCREMENT =3 DEFAULT CHARSET =utf8 COMMENT ='锁定中的⽅法';
1
2
3
4
5
6
7
8
9INSERT INTO method_lock (method_name , desc ) VALUES ('methodName', '测试的methodName');
1delete from method_lock where method_name ='methodName';
1
除了通过增删操作数据库表中的记录来实现分布式锁, 我们还可以借助数据库中再带的锁来实现分布式锁.
在查询语句后⾯增加For Update, 数据库会在查询过程中给数据库表增加悲观锁(也成排他锁), 当某条记录被加上悲观锁后, 其他线程也就⽆法再该⾏上增加悲观锁了.
1. 悲观锁与乐观锁相反, 总是假设最坏的情况, 它认为数据的更新在⼤多数情况下是会产⽣冲突的.
2. 在使⽤悲观锁的同时, 我们需要注意⼀下锁的级别. 搜索引擎的不同也会带了锁级别的不同.
如果存储引擎是InnoDB, 在加锁的时候只有明确地指定主键(或索引)的才会执⾏⾏锁(只锁住被选取的数据), 否则Mysql将会执⾏表锁(将整个数据表单给锁住).
使⽤悲观锁实现分布式锁特点
1. 在悲观锁中, 每⼀次⾏数据的访问都是独占的, 只有当正在访问该⾏数据的请求事务提交以后, 其他请求才能依次访问该数据, 否则将阻
塞等待锁的释放.
2. 悲观锁可以严格保证数据访问的安全.
3. 但是缺点也明显, 即每次请求都会额产⽣加锁的开销, 且未获取到锁的请求将会阻塞等待锁的释放, 在⾼并发环境下, 容易造成⼤量请求
阻塞, 影响系统可⽤性,
4. 悲观锁使⽤不当还可能产⽣死锁的情况
单例模式的几种实现方式8.4.2 Redis的分布式锁
1) 单节点Redis分布式锁
加锁 加锁实际上就是在Redis中, 给Key键设置⼀个值, 为避免死锁, 并给定⼀个过期时间. Set lock_key random_value NX PX 5000
1. random_value是客户端⽣成的唯⼀的字符串.
2. NX 代表只在键不存在时, 才对键进⾏设置操作.
3. PX 5000设置键的过期时间为5000毫秒
这样, 如果上⾯的命令执⾏成功, 则证明客户端获取到了锁.
解锁 解锁的过程就是将Key键删除, 但也不能乱删, 不能说客户端1的请求将客户端2的锁给删掉. 通过random_value的唯⼀标识来判别哪个客户端. 删除的时候先输⼊要删除的random_value, 然后判断当
前random_value与先输⼊的是否相等, 是的话就删除Key, 解锁成功.
单机模式Redis分布式锁优缺点: 实现⽐较容易, 如果是单机模式也容易满⾜需求.但因为是单机单例单实例部署, 如果Redis服务宕机, 那么所有需求获取分布式锁的地⽅均⽆法获取锁, 将全部阻塞, 需要做好降级处理.当锁过期后, 执⾏任务的进程还没有执⾏完, 但是锁因为⾃动过期已经解锁,可能被其它进程重新加锁, 这就造成多个进程同时获取到了锁, 这需要额外的⽅案来解决这种问题.
2) 集模式的Redis分布式锁 Redlock
Redlock算法是什么
针对Redis集架构,redis的作者antirez提出了Redlock算法,来实现集架构下的分布式锁。
Redlock算法并不复杂,我们先简单描述⼀下,假设我们Redis分⽚下,有三个Master的节点,这三个Master,⼜各⾃有⼀个Slave,现在客户端想获取⼀把分布式锁:
1. 记下开始获取锁的时间 startTime
2. 按照A->B->C的顺序,依次向这三台Master发送获取锁的命令。客户端在等待每台Master回响应时,都有超时时间
timeout。举个例⼦,客户端向A发送获取锁的命令,在等了timeout时间之后,都没收到响应,就会认为获取锁失败,继续
尝试获取下⼀把锁
3. 如果获取到超过半数的锁,也就是 3/2+1 = 2把锁,这时候还没完,要记下当前时间endTime
计算拿到这些锁花费的时间 costTime = endTime - startTime,如果costTime⼩于锁的过期时间expireTime,则认为获
取锁成功
4. 如果获取不到超过⼀半的锁,或者拿到超过⼀半的锁时,计算出costTime>=expireTime,这两种情况下,都视为获取锁失
败
5. 如果获取锁失败,需要向全部Master节点,都发⽣释放锁的命令,也就是那段Lua脚本
Redlock优缺点:
1. Redlock是Redis的作者antirez给出的集模式的Redis分布式锁, 它基于N个完全独⽴的Redis节点.
2. 部分节点宕机, 依然可以保证锁的可⽤性.
3. 当某个节点宕机后, ⼜⽴即重启了, 可能会出现两个客户端同时持有同⼀把锁, 如果节点设置了持久化, 出现这种情况的⼏率会
降低.
4. 和单机模式锁相⽐, 实现难度要⼤些.
3) 集模式的Redis分布式锁 Redisson(基于Redlock)
Redisson是⼀个基于Java编程框架netty进⾏扩展了的Redis.
1. Redisson是架设在Redis基础上的⼀个Java驻内存数据⽹格, 可以理解为是⼀套开源框架.充分的利⽤了Redis键值数据库提供的⼀系
列优势, 基于Java实⽤⼯具包中的常⽤接⼝, 为使⽤者提供了⼀系列具有分布式特性的常⽤⼯具类.
2. 进⼀步简化了分布式环境中程序相互之间的协作.相对于Jedis⽽⾔, Redisson更强的是实现类分布式锁, ⽽且包含各种类型的锁.
Redisson适⽤于: 分布式应⽤, 分布式缓存, 分布式会话管理, 分布式服务(任务, 延迟任务, 执⾏器), 分布式Redis客户端. ⽬前操作Redisson 有三种⽅式:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论