我正在尝试了解我的应用程序的并发要求。现在我在内存中存储一个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操作日志)。对于每个插入/更新/删除操作:
单线程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 ......)?
非常感谢
答案 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);
}
}
}