• 售前

  • 售后

热门帖子
入门百科

超级具体,结合分布式理论讲解分布式锁各种实现(redis,zookeeper,mysql

[复制链接]
123457443 显示全部楼层 发表于 2022-1-8 18:19:53 |阅读模式 打印 上一主题 下一主题
分布式理论

1、分布式编程是解决您可以利用多台计算机在单台计算机上解决的同一问题的艺术- 通常,由于问题不再适合单台计算机
2、分布式系统的出现是为了用自制的、平凡的呆板完成单个计算机无法完成的计算、存储任务。其目标是利用更多的呆板,处理
更多的数据。

3、分布式系统的核心定理CAP,该定理指出了这三个性子:
(1) 一致性:全部节点同时看到雷同的数据。
(2) 可用性:节点故障不会制止幸存者继承利用。
(3) 分区容错性:尽管由于网络和/或节点故障导致消息丢失, 系统仍继承运行

4,CAP定理的运用
只有两个可以同时满意。可以将其绘制为一个漂亮的图表,从三个属性中选择两个属性为我们提供了三种范例的系统,它们对应于不同的交织点,如图:
该定理指出中心部分(具有全部三个属性)是不可实现的。然后我们得到三种不同的系统范例:
(1) CA(一致性+可用性)。示例包罗完全严格的仲裁协议,例如两阶段提交。
(2) CP(一致性+分区容错性)。示例包罗多数仲裁协议,此中少数分区不可用,例如Paxos。
(3) AP(可用性+分区容错性)。示例包罗利用辩论解决的协议,例如Dynamo。

5,设计本事:分区和复制
数据集在多个节点之间分布的方式非常重要。为了进行任何计算,我们必要定位数据,然后对其采取行动。
有两种根本技术可以应用于数据集。它可以拆分到多个节点(分区)以允许更多的并行处理。它也可以被复制或缓存在不同的节点上,以减少客户端和服务器之间的间隔和更大的容错(复制)。


  • 分区
    分区是将数据集分别为更小的不同的独立集;这用于减少数据集增长的影响,由于每个分区都是数据的一个子集。
    分区通过限制要检查的数据量和在同一分区中定位相干数据来提高性能
    分区通过允许分区独立失败来提高可用性,增加在捐躯可用性之前必要失败的节点数目
  • 复制
    复制是在多台呆板上复制雷同的数据;这允许更多的服务器到场计算。

什么是锁?

在并发编程中,常常会遇到多个线程访问同一个共享资源,这时间我们要保证数据的一致性,那么就要用到了锁的概念,给资源加上锁,拿到锁全部权的人才能够进行利用共享资源,没有拿到锁的线程必要等候,等锁的全部权放开。
Java中实现锁的工具类有: synchronized和JDK以后才引用Lock接口锁. 如:ReentrantLock(可重入锁),ReadWriteLock (读写锁) ReentrantReadWriteLock (可以选择获取什么锁)
在单机多线程的java程序中,我们可以利用堆内存中的变量作为标志,由于多线程是共享堆内存的,堆内存中的变量对于各个线程都是可见的。
但是在分布式系统的java程序中Java提供的锁就无法解决多线程问
题了


分布式锁概念

与分布式锁相对应的是「单机锁」,我们在写多线程程序时,避免同时利用一个共享变量产生数据问题,通常会利用一把锁来「互斥」,以保证共享变量的精确性,其利用范围是在「同一个进程」中。
假如换做是多个进程,必要同时利用一个共享资源,怎样互斥呢?例如,如今的业务应用通常都是分布式架构,为了保证数据的最终一致性,必要很多的技术方案来支持,比如分布式事务、分布式锁等。
分布式锁的引入:一个应用会摆设多个进程,那这多个进程假如必要修改MySQL 中的同一行记录时,为了避免操反叛序
导致数据错误,此时,我们就必要引入「分布式锁」来解决这个问题了。
想要实现分布式锁,必须借助一个外部系统,全部进程都去这个系统上申请「加锁」,而这个外部系统,必须要实现「互斥」的本事,即两个请求同时进来,只会给一个进程返回乐成,另一个返回失败(或等候)。
这个外部系统,可以是MySQL,也可以是Redis 或Zookeeper。但为了寻求更好的性能,我们通常会选择利用Redis或Zookeeper 来做。

分布式锁的特点



  • 互斥性:保证在分布式摆设的应用集群中,同一个方法在同一时间只能被一台呆板上的一个线程实行。
  • 可重入性:这把锁要是一把可重入锁(避免死锁),雷同于ReentrantLock,同一服务节点的雷同线程, 允许重复多次加锁;
  • 阻塞性:支持阻塞和非阻塞: 和ReentrantLock 一样支持lock 和trylock 以及tryLock(long timeOut)。
  • 高可用:有高可用的获取锁和释放锁功能
  • 锁超时:支持锁超时,实时释放,防止锁死。
  • 公平锁与非公平锁:公平锁是指按照请求加锁的次序得到锁,非公平锁请求加锁是无序的。

分布式的实现方式



  • 数据库
    创建一张表, 通过对此表的insert/delete 达到获取, 释放锁的目标; 必要解决DB单点故障, 释放锁失败, 堵塞锁等环境;
    长处:借助DB, 轻易明确
    缺点:性能开销大, 有些粗笨, 大概导致系统越来越复杂
  • Redis
    集群redis中通过setnx与expire, 或者setnx与getset设置锁的逾期时间, 实现分布式锁;
    长处: 有效解决单点问题, 性能高,实现起来较为方便;
    缺点:setnx与expire无法确保事务, 一个客户端堵塞好久导致expire主动实行,
    Redlock:是redis的作者设计的分布式锁方案
    长处: •可用性高,大多数节点正常即可。 •单Redis 节点的分布式锁在
    failover 时锁失效问题不复存在。
    缺点: 设计比较复杂,个人实现比较贫困,最好借用已经实现了的库
    (Redission…)
  • Zookeeper
    长处:有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题
    实现较为简朴
    缺点:性能上不如利用缓存实现的分布式锁,由于每次在创建锁和释放锁的过程中,都要动态创建、烧毁临时节点来实现锁功能必要对Zookeeper的原理有所了解。
4,Etcd

数据库

1,创建一张表
  1. CREATE TABLE `dl_lock` (
  2. `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  3. `lock_name` varchar(64) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '锁名',
  4. `desc` varchar(1024) CHARACTER SET utf8 NOT NULL DEFAULT '备注信息' COMMENT '描述信息',
  5. `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间(当前时间)',
  6. PRIMARY KEY (`id`) USING BTREE,
  7. UNIQUE INDEX `uidx_method_name`(`lock_name`) USING BTREE
  8. ) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '锁定中的方法' ROW_FORMAT =
  9. Dynamic;
复制代码
2,表结构

3,唯一索引(uidx_method_name)

4,代码实现
  1. /**
  2. * 非阻塞获取锁
  3. *
  4. * @param lockName  锁的名称
  5. * @param desc        描述
  6. * @return 成功:1
  7. */
  8. public boolean  lock(String lockName, String desc) { try {
  9. //加锁,成功则返回true,失败则证明已经加锁或发送其他异
  10. int  result = insert(lockName, desc); if (result == 1) {
  11. return true;
  12. }
  13. } catch (DuplicateKeyException  e) {
  14. log.info("锁已经被其他线程获取,获取锁失败");
  15. } catch (Exception e) {
  16. log.info("sql  exception,获取锁失败");
  17. }
  18. return false;
  19. }
复制代码
  1. /**
  2. * 阻塞获取锁
  3. *
  4. * @param lockName  锁的名称
  5. * @param desc        描述
  6. * @return 成功:1
  7. */
  8. public void blockLock(String lockName, String desc) {
  9. while (true) {
  10. //获取锁
  11. if (lock(lockName, desc)) {
  12.    return;
  13. }
  14. //阻塞当前线程3秒LockSupport.parkNanos(3 * 1000 * 1000);
  15. }
  16. }
复制代码
  1. /**
  2. * 删除记录
  3. *
  4. * @param  lockName  锁名,唯一
  5. * @return
  6. */
  7. public int  delete(String lockName) {
  8. Map<String, Object> param  = new HashMap<>();
  9. param.put("lockName", lockName);
  10. String sql  = "delete from dl_lock  where lock_name
  11. = :lockName";
  12. return namedParameterJdbcTemplate.update(sql, param);
  13. }
复制代码
  1. /**
  2. * 锁释放,删除锁记录
  3. *
  4. * @param  lockName  锁名,唯一
  5. * @return
  6. */
  7. public int  release(String lockName) {
  8. return delete(lockName);
  9. }
复制代码
5,应用
  1. public void apply() {
  2. String lockName  = "test02_lock_name";
  3. //获取锁,如果没有获取到锁,会阻塞直到获取到锁dlLockTemplate.blockLock(lockName, "测试锁02");
  4. try {
  5. log.info(Thread.currentThread().getName()+"业务开始执行。。。。。。。");
  6. //业务实现
  7. TimeUnit.SECONDS.sleep(3);
  8. } catch (Exception e) {
  9. //业务异常
  10. } finally {
  11. //业务执行完毕后,释放锁
  12. dlLockTemplate.release(lockName);
  13. log.info(Thread.currentThread().getName()+"业务执行结束。。。。。。。");
  14. }
  15. }
复制代码
6,是否满意分布式锁的全部要求?
(1) 互斥性:保证在分布式摆设的应用集群中,同一个方法在同一时间只能被一台呆板上的一个线程实行。
答: 满意
(2) 可重入性:这把锁要是一把可重入锁(避免死锁),雷同于ReentrantLock,同一服务节点的雷同线程,允许重复多次加锁;
答:可以实现,利用锁的节点信息判定是否是重入锁。
(3) 阻塞性:支持阻塞和非阻塞: 和ReentrantLock 一样支持lock 和trylock 以及tryLock(long timeOut)。答:支持
(4) 高可用:有高可用的获取锁和释放锁功能答:高可用
(5) 锁超时:支持锁超时,实时释放,防止锁死。
答:支持锁超时释放,但必要通过定时任务去查询锁的失败日志去释放,不够实时。
(6) 公平锁与非公平锁:公平锁是指按照请求加锁的次序得到锁,非公平锁请求加锁是无序的。答:可以实现,比较复杂,通过表记录来实现公平锁的次序性。
基于数据库锁做分布式锁

在查询语句反面增加for update,数据库会在查询过程中给数据库表增加排他锁 (注意:InnoDB 引擎在加锁的时间,只有通过索引进行检索的时间才会利用行级锁,否则会利用表级锁。这里我们希望利用行级锁,就要给要实行的方法字段名添加索引,值得注 意的是,这个索引肯定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话发起把参数范例也 加上。)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。
基于redis的setnx实现

命令:setnx key value ex 10 nx
1,超时获取锁方法(加锁,自旋,超时后会获取失败)
  1. public boolean tryLock(String lockKey, String valueForRequestId, long timeout) {
  2. long start = System.currentTimeMillis();
  3. boolean locked = false; while (true) {
  4. //超时,锁获取失败
  5. if (System.currentTimeMillis() - start > timeout) { break;
  6. } //加锁,成功返回true,失败返回false
  7. Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, valueForRequestId,  timeout, TimeUnit.SECONDS);
  8. if (Boolean.TRUE.equals(result)) {
  9. locked = true; break;
  10. } else {
  11. //500毫秒后重试
  12. LockSupport.parkNanos(TimeUnit.NANOSECONDS.convert(500, TimeUnit.MILLISECONDS));
  13. }
  14. }
  15. return locked;
  16. }
复制代码
2,两种解锁方法
  1. /**
  2. *        通过lua脚本锁
  3. *
  4. *        @param lockKey 锁key
  5. *        @return 释放结果
  6. */
  7. public boolean releaseLock(String lockKey, String valueForRequestId) {
  8. if (lockKey == null || valueForRequestId == null)
  9. return false;
  10. DefaultRedisScript<Long> redisScript = new DefaultRedisScript();
  11. //用于解锁的lua脚本位置
  12. redisScript.setLocation(new ClassPathResource("unlock.lua")); redisScript.setResultType(Long.class);
  13. //没有指定序列化方式,默认使用上面配置的
  14. Object result = redisTemplate.execute(redisScript, Arrays.asList(lockKey), valueForRequestId);
  15. return result.equals(Long.valueOf(1));
  16. }
复制代码
  1. /**
  2. * 非原子解锁,可能解别人锁,不安全
  3. */
  4. public boolean unlock(String lockKey, String valueForRequestId) { if (lockKey == null || valueForRequestId == null)
  5. return false;
  6. boolean releaseLock = false;
  7. String requestId = redisTemplate.opsForValue().get(lockKey); if (valueForRequestId.equals(requestId)) {
  8. releaseLock = redisTemplate.delete(valueForRequestId);
  9. }
  10. return releaseLock;
  11. }
复制代码
Lua脚本
  1. if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1])
  2. else
  3. return 0
  4. end
复制代码
必要注意的问题
(1) 设置逾期时间
(2) 保证加锁和设置逾期时间的原子性
(3) Value值利用特定值(进程ID,IP地址,docker ID之类)进行区分,防止误删
(4) 尽大概的利用lua脚本
存在的问题
(1)单体性能堪忧,集群,主从等无法防止单节点故障造成锁丢失
是否满意分布式锁的全部问题?
(1) 互斥性:保证在分布式摆设的应用集群中,同一个方法在同一时间只能被一台呆板上的一个线程实行。
答:不满意, 极端环境,假如客户A持有锁,但客户A阻塞住而且锁逾期了,这时客户B进来之后同样会乐成持有锁, 此时客户A和客户B都拥有了这把锁,无法满意互斥性。
解决方案:在客户A持有锁的时间,假如客户A没有实行完业务或者阻塞环境下,必要给客户A持有的锁进行续命, 也就是常说的锁续命
(2) 可重入性:这把锁要是一把可重入锁(避免死锁),雷同于ReentrantLock,同一服务节点的雷同线程,允许重复多次加锁;
答:可以实现,利用锁的节点信息判定是否是重入锁。
(3) 阻塞性:支持阻塞和非阻塞: 和ReentrantLock 一样支持lock 和trylock 以及tryLock(long timeOut)。答:支持
(4) 高可用:有高可用的获取锁和释放锁功能答:高可用
(5) 锁超时:支持锁超时,实时释放,防止锁死。答:支持锁超时释放,可以设置逾期时间。
(6) 公平锁与非公平锁:公平锁是指按照请求加锁的次序得到锁,非公平锁请求加锁是无序的。答:可以实现,比较复杂,通过记录来实现公平锁的次序性。
通过Redisson实现分布式锁

Redisson是架设在Redis底子上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了Redis键值数据库提供的一系列上风,基于Java 实用工具包中常用接口,为利用者提供了一系列具有分布式特性的常用工具类。使得原本作为和谐单机多线程并发程序的工具包得到了和谐分布式多机多线程并发系统的本事,大大低沉了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。
Redisson提供了主从,集群,单实例,哨兵等设置。
而且Redisson内部代码实现了很多细节,比如内部的看门狗。
而且也实现了多种锁。

通过Redisson实现分布式锁

RedLock的出现

基于setnx实现分布式锁大概会出现丢锁的征象

   1,客户A,拿到了锁lockKey
2, master要把锁数据lockKey同步给slave,这时master宕机了
3,主从切换,slave晋升为master
4,客户B必要获取锁,到master获取锁,同样拿到了锁这时间,同一把锁,两个人同时拿到。
  

  • 无论redis是主从+哨兵还是集群都存在如许的风险。由于这个问题,
    Redis 之父antirez 提出了redisson的RedLock(红锁)算法

  • RedLock的长处
(1) 可用性高,大多数节点正常即可。
(2) 单Redis 节点的分布式锁在failover 时锁失效问题不复存在
RedLock的缺点
(3)设计比较复杂,个人实现比较贫困,最好借用已经实现了的库
(Redission…)


  • RedLock算法
Redlock为了解决CAP的cp,数据一致性,采用有n个redis节点,n为奇数(官方推荐5个实例),来保证他们大多数环境下都不会同时宕机,这些节点是完全独立的,因此我们不利用复制或任何其他隐式和谐系统。我们已经描述了如安在单个实例中安全地获取和释放锁。我们理所固然地认为算法会在单个实例中利用这种方法来获取和释放锁。在我们的示例中,我们设置了N=5,这是一个公道的值,因此我们必要在不同的计算机或假造机上运行 5 个Redis 主节点,以确保它们以险些独立的方式失败。
为了获取锁,客户端实行以下利用:
(1),它以毫秒为单元获取当前时间。
(2),它实验次序获取全部N 个实例中的锁,在全部实例中利用雷同的键名和随机值。在第 2 步中,当在每个实例中设置锁时,客户端利用一个比总锁主动释放时间更小的超时来获取它。例如,假如主动释放时间为 10 秒,则超时大概在~ 5-50 毫秒范围内。这可以防止客户端长时间处于阻塞状态,试图与已关闭的Redis 节点通讯:假如实例不可用,我们应该尽快实验与下一个实例通讯。
(3),客户端通过从当前时间中减去步骤 1 中得到的时间戳来计算获取锁所用的时间。 当且仅当客户端能够在大多数实例(至少 3 个)中获取锁,而且获取锁所用的总时间小于锁有效时间,则认为该锁已获取。
(4),假如得到了锁,则其有效时间被视为初始有效时间减去颠末的时间,如步骤 3 中计算的那样。
(5),假如客户端由于某种原因获取锁失败(或者它无法锁定N/2+1 个实例或有效时间为负),它将实验解锁全部实例(纵然是它认为没有乐成获取锁的实例)。



  • RedLock的方案
Redlock 的方案基于 2 个条件:
(1) 不再必要摆设从库和哨兵实例,只摆设主库
(2) 但主库要摆设多个,官方推荐至少 5 个实例
注意:不是摆设Redis Cluster,就是摆设 5 个简朴的Redis 实例。
Redisson 实现方式(红锁RedLock)
github Redisson:
https://github.com/redisson/redisson
https://github.com/redisson/redisson/wiki/2.-Configuration
RedLock:
https://redis.io/topics/distlock
Redisson

1,Redission利用
(1)添加maven设置
  1. <dependency>
  2. <groupId>org.redisson</groupId>
  3. <artifactId>redisson</artifactId>
  4. <version>3.16.1</version>
  5. </dependency>
复制代码
Redisson客户端设置
可以有集群,主从,哨兵,单节点等不同设置的客户端。但他们会丢失锁。
红锁的设置算法则不会出现锁丢失:
设置三个客户端,redissonSingleClient1,redissonSingleClient2,
redissonSingleClient3,像下面这一样,注入三个Bean,以client1为例子。
  1. /**
  2. * redisson客户端单机模式1
  3. */
  4. @Bean("client1")
  5. public RedissonClient redissonSingleClient1() {
  6. Config config1 = new Config();
  7. config1.useSingleServer()
  8. //可以用"rediss://"来启用SSL连接
  9. .setAddress("redis://192.168.145.132:6379");//...other config add return Redisson.create(config1);
  10. }
复制代码
通过三个客户端Rlock,构造一把红锁RedissonRedLock。
  1. private RedissonRedLock redLock; @BeforeEach
  2. public void init() {
  3. String lockKey = "lock_key";
  4. RLock lock1 = redissonClient1.getLock(lockKey);
  5. RLock lock2 = redissonClient2.getLock(lockKey);
  6. RLock lock3 = redissonClient3.getLock(lockKey);
  7. redLock = new RedissonRedLock(lock1, lock2, lock3);
  8. }
复制代码

实现分布式锁的一个非常重要的点就是set的value要具有唯一性, redisson的value是怎样保证value的唯一性呢?答案是UUID+threadId。源码在Redisson.java和RedissonLock.java中:

锁的核心代码,根据lua脚步实现

应用
  1. public void apply() { try {
  2. //尝试等待10秒,加锁3秒
  3. boolean lock = redLock.tryLock(10, 3, TimeUnit.SECONDS); log.info(Thread.currentThread().getName() +"redLock:{}",lock);
  4. if (lock) {
  5. log.info(Thread.currentThread().getName() + " 业务执行开始。。。。。。。。。");
  6. //业务实现TimeUnit.SECONDS.sleep(3);
  7. }
  8. } catch (Exception e) {
  9. } finally { redLock.unlock();
  10. log.info(Thread.currentThread().getName() + "业务执行结束。。。。。。。。。");
  11. }
  12. }
复制代码
分布式专家Martin 对于Redlock 的质疑?




  • 分布式锁的目标是什么?
    Martin认为,RedLock仍旧没有解决锁失效的问题,认为这种算法,就像是给患者服用了重复剂量的药物
    一样。
  • 锁在分布式系统中会遇到的问题

  • 分布式系统的NPC问题导致出现的问题
  • 假设时钟精确的是不公道的
    Redlock 必须「强依靠」多个节点的时钟是保持同步的,一旦有节点时钟发生错误,那这个算法模子就失效了。Redlock 的算法是建立在「同步模子」底子上的,但同步模子的假设,在分布式系统中是有问题的。
  • 提出fecing token 的方案,保证精确性
这个模子流程如下:
1,客户端在获取锁时,锁服务可以提供一个「递增」的token
2,客户端拿着这个token 去利用共享资源
3,共享资源可以根据token 拒绝「后来者」的请求
Martin 的结论:
1、Redlock 不正经:它对于效率来讲,Redlock 比较重,没必要这么做,而对于精确性来说,Redlock 是不够安全的。
2、时钟假设不公道:该算法对系统时钟做出了危险的假设(假设多个节点呆板时钟都是一致的),假如
不满意这些假设,锁就会失效。
3、无法保证精确性:Redlock 不能提供雷同fencing token 的方案,所以解决不了精确性的问题。为了精确性,请利用有「共识系统」的软件,例如Zookeeper。
Redis 作者Antirez 的反驳 :


  • 解释时钟问题
    Redis 作者表示,Redlock 并不必要完全一致的时钟,只必要大要一致就可以了,允许有「毛病」。
  • 解释网络延迟、GC 问题
    在RedLock算法的第三步是可以避免的,假如是获取锁利用共享资源的过程出现问题,那这不止是RedLock的问题,任何其他锁服务,例如zookeeper也有问题
  • 质疑fencing token 机制
    1, 这个方案必须要求要利用的「共享资源服务器」有拒绝「旧token」的本事。
    2,既然资源服务器都有了「互斥」本事,那还要分布式锁干什么?
    3, Redlock 已经提供了随机值( UUID),利用这个随机值,也可以达到与fecing token 同样的结果
Zookeeper实现分布式锁

ZooKeeper 是一个分布式的,开放源码的分布式应用程序和谐服务,是 Google 的Chubby 一个开源的实现。它提供了简朴原始的功能,分布式应用可以基于它实现更高级的服务,比 如分布式同步,设置管理,集群管理,命名管理,队列管理。它被设计为易于编程,利用文件系统目录树作为数据模子。服务端跑在 java 上,提供java 和C 的客户端API。
官网地址:http://ZooKeeper.apache.org/
官网API 地址:http://ZooKeeper.apache.org/doc/r3.4.10/api/index.html
ZooKeeper 作为一个集群提供数据一致的和谐服务,天然,最好的方式就是在整个集群中的各服务节点进行数据的复制和同步数据复制的好处:
1、容错:一个节点出错,不至于让整个集群无法提供服务。
2、扩展性:通过增加服务器节点能提高 ZooKeeper 系统的负载本事,把负载分布到多个节点上。
3、高性能:客户端可访问本地 ZooKeeper 节点或者访问就近的节点,依次提高用户的访问速率。
特点:
1、 最终一致性:client 岂论毗连到哪个Server,展示给它都是同一个视图,这是 ZooKeeper 最重要的性能。
2、 可靠性:具有简朴、坚固、良好的性能,假如消息 m 被到一台服务器接受,那么它将被全部的服务器接受。
3、 实时性:ZooKeeper 保证客户端将在一个时间间隔范围内得到服务器的更新信息,或者 服务器失效的信息。但由于网络延时等原因,
ZooKeeper 不能保证两个客户端能同时得到刚更新的数据,假如必要最新数据,应该在读数据之前调用 sync()接口。
4、 等候无关(wait-free):慢的或者失效的 client 不得干预快速的client 的请求,使得每个client 都能有效的等候
5、 原子性:更新只能乐成或者失败,没有中心状态。
6、 次序性:包罗全局有序和偏序两种:全局有序是指假如在一台服务器上消息 a 在消息b 前发布,则在全部Server 上消息a 都将在消息
b 前被发布;偏序是指假如一个消息 b 在消息a 后被同一个发送者发布,a 必将排在b 前面。
Zookeeper集群的推举策略
设置多个实例共同构成一个集群对外提供服务以达到水平扩展的目标,每个服务器上的数据是雷同的,每一个服务器均可以对外提供读和写的服务。
Zookeeper角色分为Leader和Follower,Leader通过推举产生,Leader产生后,其余节点主动为Follower。推举机制为过半机制
Zookeeper的四种角色:


Zookeeper集群的推举策略(Zab协议)
推举过程中,zookeeper对客户端是制止访问的,所以只满意CAP中的CP,满意一致性

Zookeeper文件系统
Zookeeper提供了一个雷同文件系统的数据结构。其实zookeeper自己就是一个文件系统+通知机制的系统。
每个子目录项如NameService 都被称作为znode,和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除子znode,唯一的不同在于znode是可以存储数据的。
有四种范例的znode:
1、PERSISTENT-持久化目录节点
客户端与zookeeper断开毗连后,该节点仍旧存在
2、 PERSISTENT_SEQUENTIAL-持久化次序编号目录节点
客户端与zookeeper断开毗连后,该节点仍旧存在,只是Zookeeper给该节点名称进行次序编号
3、EPHEMERAL-临时目录节点
客户端与zookeeper断开毗连后,该节点被删除
4、EPHEMERAL_SEQUENTIAL-临时次序编号目录节点
客户端与zookeeper断开毗连后,该节点被删除,只是Zookeeper给该节点名称进行次序编号

根据Zookeeper的有序节点(编号),临时节点(主动删除)和变乱监听特质,可以很方便的实现分布式锁。
变乱监听:当节点数据或结构厘革时,Zookeeper会通知客户端,当前Zookeeper有以下四种变乱:
1,节点创建;
2,节点删除;
3,节点数据修改;
4,子节点变更。

ZooKeeper 实现分布式锁的重要流程:
1,当第一个线程进来时会去父节点上创建一个临时的次序节点。
2,第二个线程进来发现锁已经被持有了,就会为当前持有锁的节点注册一个watcher 监听器。
3,第三个线程进来发现锁已经被持有了,由于是次序节点的缘故,就会为上一个节点去创建一个watcher
监听器。
4,当第一个线程释放锁后,删除节点,由它的下一个节点去占有锁。
Curator框架实现分布式锁
Curator是Netflix公司开源的一套zookeeper客户端框架,解决了很多Zookeeper客户端非常底层的细节开辟工作,包罗
毗连重连、反复注册Watcher和NodeExistsException非常等等。Patrixck Hunt(Zookeeper)以一句“Guava is to Java that Curator to Zookeeper”给Curator予高度评价。
1,引入pom
  1. <dependency>
  2. <groupId>org.apache.zookeeper</groupId>
  3. <artifactId>zookeeper</artifactId>
  4. <version>3.7.0</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.apache.curator</groupId>
  8. <artifactId>curator-recipes</artifactId>
  9. <version>2.12.0</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.apache.curator</groupId>
  13. <artifactId>curator-framework</artifactId>
  14. <version>2.12.0</version>
  15. </dependency>
复制代码
2,创建客户端毗连
  1. /**
  2. *        zkAddr zk地址 ip:port,ip:port,ip:port
  3. *        timeOut 连接超时ms
  4. *        namespace 所有的操作都是在 /namespace 下的节点操作
  5. *        acl Access Control List(访问控制列表)。Znode被创建时带有一个ACL列表<br>
  6. */
  7. private String zkAddr="192.168.145.132:2181";
  8. private int timeOut=10000;
  9. private String namespace="zk_lock"; @Bean
  10. public CuratorFramework curatorFramework() throws Exception { RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory
  11. .builder()
  12. .connectString(this.zkAddr)
  13. .namespace(StringUtils.isEmpty(namespace) ? “” : namespace)
  14. .connectionTimeoutMs(this.timeOut)
  15. .retryPolicy(retryPolicy);
  16. CuratorFramework client= builder.build();
  17. client.start(); //启动客户端
  18. client.blockUntilConnected(5, TimeUnit.SECONDS);
  19. return client;
  20. }
复制代码
分布式锁对象InterProcessMutex:
(1) Client:Zookeeper客户端
(2) path: 锁空间的根节点路径

获取锁方法,this.internalLock(-1,null)方法的参数会永世阻塞:

释放锁:

1,在zk_lock下创建路径为test_lock的锁,结果如右图所示
  1. public void apply() throws Exception {
  2. //创建分布式锁,根节点为test_lock
  3. InterProcessLock test =zookeeperLock.getInterProcessLock("/test_lock");
  4. //获取锁
  5. test.acquire();
  6. //业务实现try {
  7. TimeUnit.SECONDS.sleep(100);
  8. } catch (Exception e) {
  9. }finally {
  10. //释放锁test.release();
  11. }
  12. }
复制代码

1,启动三个线程去调用观察
  1. @Test
  2. public void test01() throws Exception {
  3. for (int i = 0; i < 3; i++) {
  4. new Thread(this::apply).start();
  5. }
  6. }
复制代码
加锁和释放锁的过程:
出现三个临时次序节点,正常环境下是,00000000,00000001,00000002次序释放锁,也
就是删除节点。



2,分布式锁利用的问题
zookeeper实现的分布式锁,解决分布式问题,不可重入问题,利用起来也较为简朴。但是,存在以下问题:
(1)效率低,频仍的创建和删除节点。
由于每次在创建锁和释放锁的过程中,都要动态创建、烧毁瞬时节点来实现锁功能。各人知道,ZK中创建和删除节点只能通过Leader服务器来实行,然后Leader服务器还必要将数据同步到全部的Follower呆板上,如许频仍的网络通讯, 性能的短板是非常突出的。
所以,在高性能,高并发的场景下,不发起利用ZooKeeper的分布式锁。而由于ZooKeeper的高可用特性,所以在并发量不是太高的场景,推荐利用ZooKeeper的分布式锁。
几种方式对比

无论哪种方式都无法完美的实现。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满意,所 以,根据不同的应用场景选择最适合自己的才是王道。
(1),从明确的难易水平角度(从低到高)
数据库> 缓存> Zookeeper
(2),从实现的复杂性角度(从低到高)
Zookeeper >= 缓存> 数据库
(3),从性能角度(从高到低)
缓存> Zookeeper >= 数据库
(4),从可靠性角度(从高到低)
Zookeeper > 缓存> 数据库
2,分布式锁带来的问题?
(1),将并发酿成串行,低沉了系统的访问效率 解决方案:详细的解决方案要根据业务去做优化。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

帖子地址: 

回复

使用道具 举报

分享
推广
火星云矿 | 预约S19Pro,享500抵1000!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

草根技术分享(草根吧)是全球知名中文IT技术交流平台,创建于2021年,包含原创博客、精品问答、职业培训、技术社区、资源下载等产品服务,提供原创、优质、完整内容的专业IT技术开发社区。
  • 官方手机版

  • 微信公众号

  • 商务合作