暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

反向操作-Eurka的读写锁

阿哲是哲学的哲 2021-02-05
705

文章开始前, 先跪求一波关注 谢谢各位看官

上片文章俺留了个小坑, 今晚趁着网抑云时刻睡不着, 把坑给填了把 .  上期地址:  Collection 太快受不了 - 集合迭代稳定性

一. Eurka 中为何读写锁反向写?

    1. 咱们先回顾一下代码
    //所有注册实例集合
    private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
    = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
    //被修改过的实例集合,用于增量更新实例是读取
    private ConcurrentLinkedQueue<RecentlyChangedItem> recentlyChangedQueue = new ConcurrentLinkedQueue<RecentlyChangedItem>();


    // 注册方法使用了读锁
    public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication){
    read.lock();
    ...
    gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap); // 往注册表内添加实例
    ...
    recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel)); //添加最近修改队列
    }
    // 取消实例的注册使用读锁
    protected boolean internalCancel(String appName, String id, boolean isReplication) {
    read.lock();
    Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
    Lease<InstanceInfo> leaseToCancel = null;
    if (gMap != null) {
    leaseToCancel = gMap.remove(id); // 这里开始去注册表中移除 实例
    }
    .....
    recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel)); //添加最近修改队列
    }
    //更新实例的状态。使用读锁
    public boolean statusUpdate(String appName, String id,
    InstanceStatus newStatus, String lastDirtyTimestamp,
    boolean isReplication) {
    read.lock();
    Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
    Lease<InstanceInfo> lease = null;
    if (gMap != null) {
    lease = gMap.get(id); //获取要修改的实例
    }
    ....
    lease.serviceUp(); // 对要修改的实例进行某个状态的修改
    ....
    recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel)); //添加最近修改队列
    }
    // 下线状态赋值给实例
    public boolean deleteStatusOverride(String appName, String id,
    InstanceStatus newStatus,
    String lastDirtyTimestamp,
    boolean isReplication) {
    read.lock();
    // 对实例进行状态修改 , 修改为下线状态
    recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel)); //添加最近修改队列
    }


    //获取增量实例 (这里是用的写锁)
    public Applications getApplicationDeltas(){
    Applications apps = new Applications();
    write.lock();
    ....
    Iterator<RecentlyChangedItem> iter = this.recentlyChangedQueue.iterator(); //返回最近改变的队列, 即为增量的实例
    //开始对 recentlyChangedQueue 最近改变了的队列进行遍历, 最终将改变过的实例 也就是增量返回
    ......
    return apps;
    }


    //获取全量实例 (这里没有加锁)
    @Deprecated
    public Applications getApplications(boolean includeRemoteRegion) {
    ....
    Applications apps = new Applications();
    //对注册表进行遍历
    for (Entry<String, Map<String, Lease<InstanceInfo>>> entry : registry.entrySet()) {
    Application app = null;
    if (entry.getValue() != null) {
    for (Entry<String, Lease<InstanceInfo>> stringLeaseEntry : entry.getValue().entrySet()) {
    Lease<InstanceInfo> lease = stringLeaseEntry.getValue();
    ......
    app.addInstance(decorateInstanceInfo(lease));
    }
    }
    if (app != null) {
    apps.addApplication(app);
    }
    ....
    return apps;
    }
    }


    //心跳机制 进行服务续约 (这里没有加锁)
    public boolean renew(String appName, String id, boolean isReplication) {
    ....
    Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);//获取Lease
    .... 开始续约操作
    }
      1. 上述代码中我们可以看到. Eurka 使用了读/写锁的添加方式就是为了解决两个共享集合 recentlyChangedQueue 与注册表 registry 的“集合迭代稳定性”问题.

        但是, 为何 在写操作的时候 用的是读锁 读操作居然使用了写锁.


      2. 还有重要的一点, 在 DiscoveryClient.HeartbeatThread 中, 各个服务在初始化后, 都会开启一个线程自动的调用这个刷新注册列表的机制, 也就是我们常说的心跳机制.

        他将会每个一定时间向Eurka进行续约请求, 将会调用到 AbstractInstanceRegistry#renew ,我们可以看到, 这里也会对registry.get() 进行读操作, 但是却不加锁了! 重点就在这里了!

    二.  一切都是为了与你心连心--心跳机制不加锁

    注册表 registry 集合, 是一个迭代非常频繁的家伙 , 所以Eurka使用读写锁进行读写分离 这点咱们都知道了 .

    • 那为什么不增删改加写锁, 读取加读锁呢?

      原因就是 由于续约请求 AbstractInstanceRegistry#renew 是一个非常频繁被调用的方法 . 你可以想象一下, 咱们Eurka集群也就一个集群对吧, 客户端可是有多台的呀!客户端集群相对Eurka来说, 一个集群 对 多台状态不同的机器, 每台都会定时(一般默认是30s) 向Eurka说一声 嘿~ 俺还活着呢!  是不是像极了电影里旧时代换粮票的场景.

      所以这里的 renew方法虽说是写操作, 但也不能加写锁.

    • 加读锁行不行呢?

      也是不行的, 由于renew方法实在是太频繁, 加了读锁会让其他的写操作都阻塞, 这是非常低效率的.

    • 干脆不加锁, 会不会有问题呢?

      这个你放心, 咱们的 registry 和 recentlyChangedQueue 使用的都是JUC集合 带 Concurrent字号的,  如果两个写操作, 写操作都是读锁, 所以允许同时对集合进行修改, 是线程安全的。而且还能保证 在全量下载.

    • 增量查询加了写锁 全量下载却没加锁, 这又是为何呢?

      因为 续约只对registry进行操作 , 且增量下载中没有对注册表 registry 的操作,而全量下载读取了 registry。若为全量下载添加写锁,就会 导致其在全量下载的时候出现续约请求处理被阻塞的情况。这也是尽最大可能的让心跳机制不被阻塞.

    三.总结

            由于Eurka是一个AP的框架, 在保证高可用的情况下, 放弃了强一致性, 而是一个最终一致性的策略. 相比Zookeeper在注册的时候 , 所有读取操作都会阻塞, Eurka在性能方面还是更胜一筹的.

            同时我们可以看出, 对于加锁的策略, 并不是非要读操作就加读锁, 写操作就加写锁的, 而是根据实际的业务 + 测试的情况 进行最优解决方案!


    小编这次排版可以了吧各位看官




    喜欢的话点个赞鼓励鼓励按呗



    往期文章


    Collection 太快受不了 - 集合迭代稳定性


    文章转载自阿哲是哲学的哲,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

    评论