我正在寻找一种java并发习惯用法来匹配具有最高吞吐量的大量元素。
考虑我有多个线程进来的“人”。每个“人”都在寻找一场比赛。当它找到另一个等待的“人”时,它们会相互分配并被移除以进行处理。
我不想锁定一个大结构来改变状态。考虑Person有getMatch和setMatch。在提交之前,每个人的#getMatch都是null。但是当他们解锁(或被捕获)时,他们要么已经过期了,因为他们等待渴望比赛,或者#getMatch是非空的。
保持高通过率的一些问题是,如果PersonA与PersonB同时提交。它们相互匹配,但PersonB也匹配已经在等待的PersonC。 PersonB的状态在提交时变为“可用”。但是当PersonB与PersonC匹配时,PersonA不需要偶然获得PersonB。合理?另外,我想以异步方式执行此操作。换句话说,我不希望每个提交者都必须在具有waitForMatch类型的线程上持有Person。
同样,我不希望请求必须在不同的线程上运行,但是如果有一个额外的匹配器线程则可以。
似乎应该有一些成语,因为它似乎是一个非常常见的事情。但我的谷歌搜索已经枯竭(我可能使用了错误的条款)。
更新
有一些事情让我很难解决这个问题。一个是我不想在内存中有对象,我想让所有等待的候选人都使用redis或memcache或类似的东西。另一个是任何人可能有几个可能的比赛。考虑如下界面:
person.getId(); // lets call this an Integer
person.getFriendIds(); // a collection of other person ids
然后我有一个看起来像这样的服务器:
MatchServer:
submit( personId, expiration ) -> void // non-blocking returns immediately
isDone( personId ) -> boolean // either expired or found a match
getMatch( personId ) -> matchId // also non-blocking
这是一个休息界面,它会使用重定向,直到你得到结果。我的第一个想法是在MatchServer中有一个Cache,它由redis之类的东西支持,并且对于当前被锁定和被操作的对象具有并发的弱值哈希映射。每个personId将由持久状态对象包装,状态为已提交,匹配和过期。
到目前为止?非常简单,提交代码完成了初始工作,它是这样的:
public void submit( Person p, long expiration ) {
MatchStatus incoming = new MatchStatus( p.getId(), expiration );
if ( !tryMatch( incoming, p.getFriendIds() ) )
cache.put( p.getId(), incoming );
}
public boolean isDone( Integer personId ) {
MatchStatus status = cache.get( personId );
status.lock();
try {
return status.isMatched() || status.isExpired();
} finally {
status.unlock();
}
}
public boolean tryMatch( MatchStatus incoming, Iterable<Integer> friends ) {
for ( Integer friend : friends ) {
if ( match( incoming, friend ) )
return true;
}
return false;
}
private boolean match( MatchStatus incoming, Integer waitingId ) {
CallStatus waiting = cache.get( waitingId );
if ( waiting == null )
return false;
waiting.lock();
try {
if ( waiting.isMatched() )
return false;
waiting.setMatch( incoming.getId() );
incoming.setMatch( waiting.getId() );
return true
} finally {
waiting.unlock();
}
}
所以这里的问题是,如果两个人同时进来并且他们是他们唯一的比赛,他们就不会找到对方。竞争条件对吗?我能看到解决它的唯一方法是同步“tryMatch()”。但这会影响我的吞吐量。我不能无限期地循环tryMatch,因为我需要这些非常短的调用。
那么有什么更好的方法来解决这个问题呢?我提出的每一个解决方案都会一次一个地强迫人们使用吞吐量。例如,创建后台线程并使用阻塞队列一次放入和接收传入线程。
非常感谢任何指导。
答案 0 :(得分:2)
您可以使用ConcurrentHashMap
。我假设您的对象具有可以匹配的密钥,例如PersonA和PersonB将拥有一个&#34; Person&#34;键。
ConcurrentHashMap<String, Match> map = new ConcurrentHashMap<>();
void addMatch(Match match) {
boolean success = false;
while(!success) {
Match oldMatch = map.remove(match.key);
if(oldMatch != null) {
match.setMatch(oldMatch);
success = true;
} else if(map.putIfAbsent(match.key, match) == null) {
success = true;
}
}
}
在您将匹配项添加到地图之前,或者直到您删除了现有匹配项并将其配对为止,您才会继续循环播放。 remove
和putIfAbsent
都是原子的。
修改:因为您要将数据卸载到磁盘,所以您可以使用例如MongoDB为此,使用findAndModify方法。如果具有密钥的对象已经存在,则该命令将删除并返回它,以便您可以将旧对象与新对象配对,并且可能存储与新密钥关联的对;如果具有该键的对象不存在,则该命令将该对象与该键一起存储。这相当于ConcurrentHashMap
的行为,除了数据存储在磁盘而不是内存中;你不必担心同时写两个对象,因为findAndModify
逻辑可以防止它们无意中占用同一个密钥。
如果您需要将对象序列化为JSON,请使用Jackson。
有Mongo的替代品,例如DynamoDB,虽然Dynamo只能免费获取少量数据。
编辑:鉴于朋友列表不是自反性的,我认为您可以使用MongoDB(或其他键值数据库与原子更新)和ConcurrentHashMap
的组合来解决这个问题。
ConcurrentHashMap<key, boolean>
,可能是全局ConcurrentHashMap<key, ConcurrentHashMap<key, boolean>>
。findAndModify
以原子方式将其设置为&#34;匹配,&#34;然后将新人写入MongoDB,状态为&#34;匹配,&#34;最后将这一对添加到&#34; Pairs&#34; MongoDB中可以由最终用户查询的集合。从全球地图中删除此人ConcurrentHashMap
。ConcurrentHashMap
。它有,然后什么都不做;如果没有,则检查该朋友是否与ConcurrentHashMap
相关联;如果是,则将与当前人的关键字相关联的值设置为&#34;为真。&#34; (请注意,由于当前的人无法检查自己的地图并使用一个原子操作修改朋友的地图,因此两位朋友仍然可以写对方&#39;哈希地图,但自哈希映射检查减少了这种可能性。)ConcurrentHashMap
,并创建一个延迟任务,该任务将迭代写入该人ConcurrentHashMap
的所有朋友的ID(即使用{{1} }})。此任务的延迟应该是随机的(例如ConcurrentHashMap#keySet()
),以便两个朋友不会同时尝试匹配。如果当前的人没有任何需要重新检查的朋友,那么就不要为此创建延迟任务。在一般情况下,一个人要么与朋友匹配,要么在没有朋友加入系统的同时迭代朋友列表(即人Thread.sleep(500 * rand.nextInt(30))
将是空的)。如果同时写朋友:
同时添加了Friend1和Friend2。
ConcurrentHashMap
,表示他们错过了对方。ConcurrentHashMap
表示相同(只有当Friend2要检查Friend1是否在Friend1写信的同时写入其地图时才会发生这种情况) Friend2会检测到Friend1已写入其地图,因此不会写入Friend1的地图)。一些小问题:
ConcurrentHashMap
与他们相关联,例如如果Friend2在Friend1检查以查看地图是否在内存中时仍在初始化其哈希映射。这很好,因为Friend2会写入Friend1的哈希映射,因此我们保证最终会尝试匹配 - 至少其中一个将具有哈希映射而另一个迭代,因为哈希地图创建先于迭代。ConcurrentHashMaps
的朋友列表结合起来,然后下一次迭代应该将其用作新朋友列表。最终,该人将被匹配,否则该人将重新检查&#34;朋友名单将被清空。ConcurrentHashMap
,第二次迭代时为Thread.sleep(500 * rand.nextInt(30))
,第三次迭代时为Thread.sleep(500 * rand.nextInt(60))
等。)Thread.sleep(500 * rand.nextInt(90))
,否则您将进行数据竞争。同样,您必须在迭代其潜在匹配项时从MongoDB中删除某个人,否则您可能会无意中将其匹配两次。修改:部分代码:
方法 ConcurrentHashMap
会写一个&#34;无法匹配的&#34; person1到MongoDB
addUnmatchedToMongo(person1)
使用setToMatched(friend1)
将findAndModify
原子设置为&#34;匹配&#34 ;;如果friend1
已匹配或不存在,则该方法将返回false;如果更新成功,则返回true
friend1
如果isMatched(friend1)
存在并且匹配则返回true,如果它不存在或存在则返回false并且&#34;不匹配&#34 ;
friend1
答案 1 :(得分:1)
我仍然不清楚你的匹配系统的细节,但我可以给你一些一般指导。
从根本上说,如果没有原子读 - 修改 - 写功能,则无法同步进程。我不会解释如何从数据库中获取它,因为它从简单(具有事务隔离的SQL数据库)到不可能(某些NoSQL数据库)不等。如果您无法从数据库中获取它,那么您别无选择,只能在内存中进行同步。
其次,您需要能够同时从可用性池中自动删除两个匹配的人。但作为同一原子操作的一部分,您还需要在将它们彼此分配之前验证两者是否仍然可用。
第三,为了最大化吞吐量,您可以将系统设计为检测竞争条件,而不是阻止它们,并在检测到竞争时实施恢复过程。
上述所有内容在内存中比在数据库中更容易(和更高的性能)。如果可能的话,我会在记忆中做到这一点。
在此系统中,唯一的阻塞同步操作正在插入匹配池并从匹配池中删除,这些是单独的锁。 (请求线程在从匹配池中删除其请求之前,必须获取一个锁,看它是否仍然在匹配池中,如果不是,则分支到种族恢复程序。)我认为这是理论上的你可以同步的极限。 (好吧,我猜你还必须在池已满时阻止插入池中,但你还能做什么?如果你可以创建一个新池,那么你可以扩展现有池。)
请注意,通过对请求队列进行排序并按顺序搜索,您可以保证请求线程可以执行完整搜索。如果它找不到搜索,那么唯一的希望是以后的请求将匹配,并且后一个请求线程将找到该匹配。
答案 2 :(得分:0)
直到可以提出更好更简单的东西,我已经采用了超级简单的方法。处理BlockingQueue的单个后台线程。它没有很好的吞吐量,但提交者不必阻止。它还具有不需要在服务员的持久缓存上进行同步的优点。我可以很容易地将BlockingQueue更改为持久性支持的BlockingQueue。如果队列已满,提交者只需等待。
唯一的问题是,如果有很多提交者和轮询者,处理队列绝对落后于提交者。这是泵的简化实现。 match方法只是通过#getFriendIds进行迭代,并进行键控查找以查看该id的人是否存在于redis(或其他)缓存中。如果它们在缓存中,则它们是匹配的。我交换彼此的ID以匹配它们。
class HoldPump extends Thread {
private final BlockingQueue<Incoming> queue = new ArrayBlockingQueue<>( CAPACITY );
HoldPump() {
super( "MatchingPump" );
}
public void submit( Person p ) {
Incoming incoming = new Incoming( p.getId(), p.getFriendIds() ) );
queueProcessing( incoming );
}
public void queueProcessing( Incoming incoming ) ... {
queue.put( incoming );
}
@Override
public void run() {
try {
while ( true ) {
Incoming incoming = queue.take();
tryMatch( incoming );
}
} catch ( InterruptedException e ) {
Thread.interrupted();
}
}
}
protected void trytMatch( Incoming incoming ) {
MatchStatus status = incoming.status;
status.answer( incoming.holdDuration );
for ( Integer candidate : incoming.candidates ) {
MatchStatus waiting = waitingForMatchByPersonId.get( candidate );
if ( waiting != null ) {
waiting.setMatch( incoming.status.getPersonId() );
status.setMatch( waiting.getPersonId() )
}
}
}
#setMatch方法实质上表示一个完成条件,它是MatchStatus中可重入锁定的一部分。
答案 3 :(得分:0)
我希望以异步方式执行此操作
异步处理=“人物提交者”和“人物匹配者”之间的逻辑队列对:
有一些事情让我很难解决这个问题。一个是我不想在内存中有对象,我想让所有等待的候选人都使用redis或memcache或类似的东西。
我的第一个想法是在MatchServer中有一个Cache,它由redis之类的东西支持,并且对于当前被锁定和被操作的对象具有并发的弱值哈希映射。
<强> 1。解决方案1:单线程中的序列化匹配处理
<强> 2。解决方案2:匹配处理的多个线程,悲观锁定具有最小关键区域
第3。解决方案3:使用事务持久存储和* 乐观锁定进行匹配处理的多个线程 : *