文章开始前, 先跪求一波关注 谢谢各位看官
上片文章俺留了个小坑, 今晚趁着网抑云时刻睡不着, 把坑给填了把 . 上期地址: Collection 太快受不了 - 集合迭代稳定性

一. Eurka 中为何读写锁反向写?
咱们先回顾一下代码
//所有注册实例集合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;}//获取全量实例 (这里没有加锁)@Deprecatedpublic 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.... 开始续约操作}
上述代码中我们可以看到. Eurka 使用了读/写锁的添加方式就是为了解决两个共享集合 recentlyChangedQueue 与注册表 registry 的“集合迭代稳定性”问题.
但是, 为何 在写操作的时候 用的是读锁 读操作居然使用了写锁.
还有重要的一点, 在 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在性能方面还是更胜一筹的.
同时我们可以看出, 对于加锁的策略, 并不是非要读操作就加读锁, 写操作就加写锁的, 而是根据实际的业务 + 测试的情况 进行最优解决方案!
小编这次排版可以了吧各位看官

喜欢的话点个赞鼓励鼓励按呗
往期文章




