Java并发&分布式场景

时间:2011-05-23 23:29:15

标签: java concurrency distributed

我正在尝试了解我的应用程序的并发要求。现在我在内存中存储一​​个Map,其中包含每个用户的最新更新。结构如下:

Map<String, RecentUpdates> cacheMap; //Key is userId

class RecentUpdates {
    public String userId; //the user   
    public List<EntityUpdate> recentUpdates;
}

class EntityUpdate { 
    public String timestamp; //The timestamp when the entity was updated
    public String id; //The unique entity id    
}

以下线程在地图中读/写:

单线程A:

从数据库队列中读取操作(MongoDB操作日志)。对于每个插入/更新/删除操作:

  1. 如果用户没有RecentUpdates 缓存映射中的对象,创建它并放入 它进入缓存地图。
  2. 创建一个新的EntityUpdate并添加它 到用户的RecentUpdates列表。
  3. 单线程B:

    迭代缓存映射。如果条目在过去一小时内没有最近更新,则删除条目

    多个线程C到Z:

    如果缓存映射包含给定用户的最新更新,请迭代最近的更新并检索发生在给定时间戳之后的更新。

    问题是:

    1。缓存映射并发

    哪个是缓存映射最合适的数据结构? ConcurrentHashMap的?也许与番石榴不同?

    2。最近的更新列出了并发性

    我是否需要同步最近的更新列表,假设只有一个线程向其添加项目,或者我可以安全地使用ArrayList吗?

    如果我是对的,如果线程A添加新元素,而线程C-Z使用Iterator迭代列表,则会抛出ConcurrentModificationException。但是如果我使用for循环迭代列表是否安全?

    for(int i = 0; i < recentUpdates.size(); i++) {}
    

    第3。分布式方案

    在分布式场景(不同Web服务器中的线程C-Z)中,您能否根据我的需求向我推荐分布式缓存解决方案(Hazelcast,Terracota ......)?

    非常感谢

3 个答案:

答案 0 :(得分:3)

1 ConcurrentHashMap可以使用,但您需要使用putIfAbsent,这意味着您必须构建可能不必要的RecentUpdate个对象(如果您不小心,不必要的List对象)。或者,使用synchronized get / put:

synchronized RecentUpdates getOrCreateRecentUpdates(String key) {
  RecentUpdates recentUpdates = map.get(key);
  if (recentUpdates == null) {
    recentUpdates = new RecentUpdates();
    map.put(key, recentUpdates);
  }
  return recentUpdates;
}

2多个线程可以访问列表吗?如果是,那么您需要一些同步。默认情况下,ArrayList不会同步。使用.size()是不安全的。没有内存屏障(同步,易失性等),您无法保证另一个线程会看到列表已更新。

我对#3没有经验。

答案 1 :(得分:3)

1&amp; 2您可以使用Guava's MapMaker构建缓存:

Map<String, RecentUpdates> cacheMap = new MapMaker().expireAfterAccess(1, TimeUnit.HOURS).makeComputingMap(new Function<String, RecentUpdates>() {
  public RecentUpdates apply(String user) {
    return create(user); //whatever your impl is
  }
}

这个映射保证一次调用init函数,每次获取一个唯一键只调用一次(如果有相同键的并发获取,则其他键阻塞并等待)。

只要对RecentUpdates的所有访问都是通过那里进行的,并且你不保留对它们的引用,这将很有效。

但是,您似乎想要一个短暂的(可变的)RecentUpdates,它可能会遇到在此结构之外更新的问题。然后,您可以更新RecentUpdates结构,但不会重置缓存中的过期时间。

解决上述问题的方法是在一个ConcurrentMap缓存中替换一个不可变的RecentUpdates

while (true) {
  RecentUpdates old = map.get(key);
  RecentUpdates updated = update(old); // copy
  if((old == null) 
      ? map.putIfAbsent(key, value) == null 
      : map.replace(key, old, value)) {
    return updated;
  }
}

这意味着地图在到期时不会有任何竞争条件。

就此而言,有许多因素需要考虑这样的决定。谁还需要这个缓存?它仅适用于当前用户,因此您可能首先考虑其他类型的缓存。

答案 2 :(得分:2)

我尝试了一个解决方案。这比使用锁的解决方案使用更多的内存,但提供更好的并发性。请看看这是否符合您的需求。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;

public class CacheMap {
    public static class EntityUpdate {
        public final String entityId;
        public final long timestamp;
        public final EntityUpdate previous;

        public EntityUpdate(String entityId, 
                            long timestamp, EntityUpdate previous) {
            this.entityId = entityId;
            this.timestamp = timestamp;
            this.previous = previous;
        }

        public EntityUpdate cloneChangingPrevious(EntityUpdate newPrevious) {
            return new EntityUpdate(entityId, timestamp, newPrevious);
        }
    }

    public static class RecentUpdates {
        public final String userId;
        public final EntityUpdate lastUpdate;

        public RecentUpdates(String userId, EntityUpdate lastUpdate) {
            this.userId = userId;
            this.lastUpdate = lastUpdate;
        }

        public RecentUpdates recordUpdate(String entityId, long timestamp) {
            EntityUpdate update = new EntityUpdate(entityId, 
                timestamp, lastUpdate);
            return new RecentUpdates(userId, update);
        }

        public RecentUpdates removeUpdatesOlderThan(long timestamp) {
            Stack<EntityUpdate> recent = new Stack<EntityUpdate>();
            EntityUpdate update = lastUpdate;
            while (update != null) {
                if (update.timestamp >= timestamp) {
                    recent.push(update);
                } else {
                    break;
                }
                update = update.previous;
            }

            EntityUpdate last = null;
            while (!recent.isEmpty()) {
                last = recent.pop().cloneChangingPrevious(last);
            }

            return new RecentUpdates(userId, last);
        }

        public boolean isEmpty() {
            return lastUpdate == null;
        }

        public List<EntityUpdate> getUpdatesSince(long timestamp) {
            List<EntityUpdate> list = new ArrayList<EntityUpdate>();
            EntityUpdate update = lastUpdate;
            while (update != null) {
                if (update.timestamp >= timestamp) {
                    list.add(update);
                } else {
                    break;
                }
                update = update.previous;
            }
            return Collections.unmodifiableList(list);
        }
    }

    private final ConcurrentHashMap<String, RecentUpdates> map = 
        new ConcurrentHashMap<String, RecentUpdates>();

    // called by thread A
    public void recordUpdate(String userId, String entityId) {
        boolean done = false;
        while (!done) {
            RecentUpdates updates = map.get(userId);
            if (updates == null) {
                // looks like there is no mapping for this userId,
                // make an effort to insert a new one
                map.putIfAbsent(userId, new RecentUpdates(userId, null));
            }
            // query the map again
            updates = map.get(userId);
            // updates could still be null, because the entry might have
            // been removed from the map by now; if so, retry
            if (updates != null) {
                long newTimestamp = System.currentTimeMillis();
                RecentUpdates newVal = 
                    updates.recordUpdate(entityId, newTimestamp);
                done = map.replace(userId, updates, newVal);
            }
        }
    }

    // called by thread B
    public void removeUpdatesOlderThan(long timestamp) {
        for (String userId : map.keySet()) {
            boolean done = false;
            while (!done) {
                // updates will always be non-null, 
                // because only this thread can
                // remove an entry from the map
                RecentUpdates updates = map.get(userId);
                RecentUpdates purgedUpdates = 
                    updates.removeUpdatesOlderThan(timestamp);
                if (purgedUpdates.isEmpty()) {
                    // remove from the map, if now new insert has
                    // happened in the interim
                    done = map.remove(userId, updates);
                } else {
                    // replace with the purged value, if no new
                    // insert has happened in the interim
                    done = map.replace(userId, updates, purgedUpdates);
                }
            }
        }
    }

    // called by threads C-Z
    public List<EntityUpdate> getUpdatesSince(String userId, long timestamp) {
        RecentUpdates updates = map.get(userId);
        if (updates == null) {
            return Collections.EMPTY_LIST;
        } else {
            return updates.getUpdatesSince(timestamp);
        }

    }
}