在并发交换期间隔离有状态bean的最正确方法

时间:2014-09-24 09:55:55

标签: java spring concurrency apache-camel

让我们说有一个调用statefull bean的路由:

<camel:route id="Concurrently-called-route">
    <camel:from uri="direct:concurrentlyCalledRoute"/>
    <camel:bean ref="statefullBean" method="setSomeState"/>
    <camel:bean ref="statefullBean" method="getSomeDataDependingOnState"/>
</camel:route>

可以同时沿着此路由发送消息,即从并发线程调用requestBody ProducerTemplate方法。因此,如果两个excahnges正在进行,并且在另一次交换期间执行的setSomeStatesetSomeState之间的一次交换中调用getSomeDataDependingOnState,则会出现问题。我看到两种解决这个问题的方法,每种方法都有一个缺点。

使用SEDA

<camel:route id="Councurrently-called-route">
    <camel:from uri="direct:concurrentlyCalledRoute"/>
    <camel:to uri="seda:sedaRoute"/>
</camel:route>

<camel:route id="SEDA-route">
    <camel:from uri="seda:sedaRoute"/>
    <camel:bean ref="statefullBean" method="setSomeState"/>
    <camel:bean ref="statefullBean" method="getSomeDataDependingOnState"/>
</camel:route>

在这种情况下,从不同线程发送的消息会聚集在SEDA端点的队列中。来自此队列的消息在SEDA-route的同时在一个线程中处理。因此,处理消息不会干扰另一个消息的处理。但是,如果有许多线程向concurrentlyCalledRoute SEDA-route发送消息将成为瓶颈。如果使用多个线程来处理seda队列中的项目,则会再次出现对statefull bean的并发调用的问题。

另一种方式 - 使用自定义范围。

自定义范围

Spring Framework允许实现自定义范围。因此,我们能够实现一个范围,该范围将为每个excahange存储一个单独的bean实例。

public class ExchangeScope implements Scope {

    private Map<String, Map<String,Object>> instances = new ConcurrentHashMap<>();

    private Map<String,Runnable> destructionCallbacks = new ConcurrentHashMap<>();

    private final ThreadLocal<String> currentExchangeId = new ThreadLocal<>();

    public void activate(String exchangeId) {
        if (!this.instances.containsKey(exchangeId)) {
            Map<String, Object> instancesInCurrentExchangeScope = new ConcurrentHashMap<>();
            this.instances.put(exchangeId, instancesInCurrentExchangeScope);
        }
        this.currentExchangeId.set(exchangeId);
    }

    public void destroy() {
        String currentExchangeId = this.currentExchangeId.get();
        Map<String,Object> instancesInCurrentExchangeScope = instances.get(currentExchangeId);
        if (instancesInCurrentExchangeScope == null)
            throw new RuntimeException("ExchangeScope with id = " + currentExchangeId + " doesn't exist");
        for (String name : instancesInCurrentExchangeScope.keySet()) {
            this.remove(name);
        }
        instances.remove(currentExchangeId);
        this.currentExchangeId.set(null);
    }

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
    // selects by name a bean instance from a map storing instances for current exchange
    // creates a new bean instance if necessary
    }

    @Override
    public Object remove(String name) {
    // removes a bean instance
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        this.destructionCallbacks.put(name, callback);
    }

    @Override
    public Object resolveContextualObject(String name) {
        String currentExchangeId = this.currentExchangeId.get();
        if (currentExchangeId == null)
            return null;

        Map<String,Object> instancesInCurrentExchangeScope = this.instances.get(currentExchangeId);
        if (instancesInCurrentExchangeScope == null)
            return null;

        return instancesInCurrentExchangeScope.get(name);
    }

    @Override
    public String getConversationId() {
        return this.currentExchangeId.get();
    }
}

现在我们可以注册这个自定义范围并声明statefullBean作为交换范围:

<bean id="exchangeScope" class="org.my.ExchangeScope"/>

<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
    <property name="scopes">
        <map>
            <entry key="ExchangeScope" value-ref="exchangeScope"/>
        </map>
    </property>
</bean>

<bean id="statefullBean" class="org.my.StatefullBean" scope="ExchangeScope"/>

要使用交换范围,我们应在发送消息之前调用activate ExchangeScope方法,然后再调用destroy

this.exchangeScope.activate(exchangeId);
this.producerTemplate.requestBody(request);
this.exchangeScope.destroy(exchangeId);

通过此实现,交换范围实际上是一个线程范围。这是一个缺点。例如,如果在路由中使用多线程拆分器,它将无法从拆分器创建的线程调用交换范围bean,因为对bean的调用将在与启动交换的线程不同的线程中执行。

任何想法如何解决这些缺点?是否有完全不同的方法在并发交换期间隔离状态bean?

2 个答案:

答案 0 :(得分:2)

另一个需要考虑的选择是不要使你的bean有状态。您可以将状态数据存储在消息本身而不是bean中,因此您的方法应该类似于:

public class StatefulBean {
    public StateInfo setSomeState(Message msg) {...}

    public void getSomeDataDependingOnState(StateInfo stateinfo) {...}
}

答案 1 :(得分:0)

使用seda队列,它专为此类问题而设计。

鉴于您可以比进入邮件更快地处理邮件,这应该是理想的。 seda队列大小限制的一般大概是大约10,000 - 显然你可以根据你的需要调整它。

我在项目中面临类似的情况,我收到大约2000条消息的初始块,然后是每秒1条消息。它们需要按指定的顺序处理,因此我将消息放入seda队列进行顺序处理,并且可能需要3-5秒才能清除它们。

否则你可以找到一种方法来使用bean的差异实例,每次交换......