Erzwungenes Caching: Befestigen Sie den L2-Cache von Apache Ignite an Activiti

Es kommt oft vor, dass es eine gute Bibliothek gibt, in der jedoch etwas fehlt, eine Art Perlmuttknopf. Also ich mit Activiti , einer ziemlich beliebten Geschäftsprozess-Engine mit Unterstützung für BPMN 2.0, die für ihre Java-Ursprünglichkeit wertvoll ist. Ohne auf die Details der internen Struktur dieses Open-Source-Produkts einzugehen, ist es offensichtlich, dass es in seiner Arbeit eine Vielzahl von Daten verwendet: Metadaten für Geschäftsprozessdefinitionen, Instanzdaten und historische Daten. Activiti verwendet ein DBMS, um sie zu speichern. Sie können zwischen DB2, H2, Oracle, MySQL, MS SQL und PostgreSQL wählen. Dieser Motor ist sehr gut und wird nicht nur für kleine Fahrzeuge verwendet. Vielleicht ist die Frage nach der Unterstützung der Zwischenspeicherung von Datenbankaufrufen in diesem Produkt nicht nur für mich aufgetaucht. Zumindest einmal erEs wurde an Entwickler gerichtet , die es in dem Sinne beantworteten, dass Metadaten zwischengespeichert werden. Für den Rest der Daten ist dies jedoch nicht sinnvoll und nicht einfach. Im Prinzip kann man sich über das Fehlen von viel Sinn einigen - die Daten einer bestimmten Instanz oder ihre historischen Daten können mit einer geringen Wahrscheinlichkeit wiederverwendet werden. Aber das Szenario, in dem dies immer noch passiert, ist auch möglich. Zum Beispiel, wenn wir einen Cluster von Activiti-Servern mit einer gemeinsamen Basis haben. Im Allgemeinen kann eine Person mit einem fragenden Verstand sehr wohl einen anständigen Cache in Activiti haben wollen. Verwenden Sie als Beispiel Apache Ignite .

Unter kat, einem Beispiel für die Lösung dieses Problems, wird der Code auf GitHub veröffentlicht .

Aufgabendenken


Was haben wir dafür? Vor allem von den Entwicklern der Cache - Prozessdefinitionen garantiert, gespeichert in java.util.HashMap, die nicht Enterprise - Lösung bezeichnet werden kann. Activiti verwendet für den Zugriff auf die Datenbank die Mybatis-Bibliothek, die natürlich das Caching unterstützt. Mybatis verwendet XML-Konfigurationen, und Activiti verfügt über viele dieser XML-Dateien. Sie enthalten Abfragedefinitionen von ungefähr folgendem Typ:


Die folgenden Links zeigen Ihnen, wie Sie Apache Ignite mit Mybatis kreuzen können. Von ihm klar wird , dass , wenn der Tag in der Auswahl hat set gewesen useCache = «true» und war Art von Cache angegeben ...


... dann wäre das fast genug. Dort ist auch die Mikrobibliothek org.mybatis.caches angegeben: mybatis-ignite, in der es genau 2 Klassen und keine spezifischen Mybatis gibt. Das ist eine ganz allgemeine Lösung.

Obwohl Activiti auf GitHub läuft und nicht selektiv gespalten werden kann, nehmen Sie Änderungen an den Mybatis-Konfigurationen vor und genießen Sie das Zwischenspeichern. Ich empfehle, dass Sie diesen Weg nicht einschlagen. Dies zwingt uns dazu, unsere eigene Version eines ziemlich großen Projekts beizubehalten, das entwickelt wurde, um unsinnige Änderungen vorzunehmen. Activiti unterstützt jedoch Spring Boot und dies eröffnet neue Perspektiven. Für das Experiment war das letzte zum Zeitpunkt des Schreibens die 4. Beta von Activiti Version 6.0.

Lösung


SQL-Abfragen in Mybatis werden von der Klasse org.apache.ibatis.mapping.MappedStatement beschrieben, die, wie Sie sich vorstellen können , eine isUseCache- Methode hat . MappedStatement-Objekte geben eine org.apache.ibatis.session.Configuration-Klasse mit einer getMappedStatement- Methode zurück. Die Konfiguration wird in der Klasse org.activiti.spring.SpringProcessEngineConfiguration erstellt, die während der Autokonfiguration von Spring Boot eingefügt wird. Daher müssen Sie das von der MappedStatement-Klasse zurückgegebene Ergebnis irgendwie beeinflussen. Leider gibt es keine einfachen Wege, und ich habe nichts Besseres gefunden, als alles mit der cglib-Bibliothek zu lernen, die uns zusammen mit dem Frühling erreicht. Der Algorithmus sieht kurz so aus: Wir definieren die Spring Boot-Autokonfiguration für das SpringProcessEngineConfiguration-Objekt neu, das die Aktivierung von Activiti steuert und das Objekt durch seine instrumentierte Version ersetzt, das das instrumentierte Configuration-Objekt zurückgibt, das neue MappedStatement-Objekte zurückgibt (dies ist leider die letzte Klasse, die mit cglib nicht angewiesen werden kann). wer denkt dass sie den Cache benutzen sollten. Und ja, das neue Configuration-Objekt kennt die Existenz von Apache Ignite. Es mag kompliziert klingen, aber tatsächlich ist alles transparent (nur für den Fall, dass der Link zur Cglib-Anleitung beigefügt ist).

Der endgültige Code wird so aussehen
@Configuration
@ConditionalOnClass(name = "javax.persistence.EntityManagerFactory")
@EnableConfigurationProperties(ActivitiProperties.class)
public class CachedJpaConfiguration extends JpaProcessEngineAutoConfiguration.JpaConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public SpringProcessEngineConfiguration springProcessEngineConfiguration(
            DataSource dataSource, EntityManagerFactory entityManagerFactory,
            PlatformTransactionManager transactionManager, SpringAsyncExecutor springAsyncExecutor)
                                throws IOException {
        return
                getCachedConfig(super.springProcessEngineConfiguration
                              (dataSource, entityManagerFactory, transactionManager, springAsyncExecutor));
    }
    private SpringProcessEngineConfiguration getCachedConfig(final SpringProcessEngineConfiguration parentConfig) {
        Enhancer enhancer = new Enhancer();
        CallbackHelper callbackHelper = new CallbackHelper(SpringProcessEngineConfiguration.class, new Class[0]) {
            @Override
            protected Object getCallback(Method method) {
                if (method.getName().equals("initMybatisConfiguration")) {
                    return (MethodInterceptor) (obj, method1, args, proxy) ->
                            getCachedConfiguration(
                               (org.apache.ibatis.session.Configuration) proxy.invokeSuper(obj, args));
                } else {
                    return NoOp.INSTANCE;
                }
            }
        };
        enhancer.setSuperclass(SpringProcessEngineConfiguration.class);
        enhancer.setCallbackFilter(callbackHelper);
        enhancer.setCallbacks(callbackHelper.getCallbacks());
        SpringProcessEngineConfiguration result = (SpringProcessEngineConfiguration) enhancer.create();
        result.setDataSource(parentConfig.getDataSource());
        result.setTransactionManager(parentConfig.getTransactionManager());
        result.setDatabaseSchemaUpdate("create-drop");
        return result;
    }
    private org.apache.ibatis.session.Configuration 
                         getCachedConfiguration(org.apache.ibatis.session.Configuration configuration) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(org.apache.ibatis.session.Configuration.class);
        enhancer.setCallback(new CachedConfigurationHandler(configuration));
        return (org.apache.ibatis.session.Configuration) enhancer.create();
    }
    private class CachedConfigurationHandler implements InvocationHandler {
        private org.apache.ibatis.session.Configuration configuration;
        CachedConfigurationHandler(org.apache.ibatis.session.Configuration configuration) {
            this.configuration = configuration;
            this.configuration.addCache(IgniteCacheAdapter.INSTANCE);
        }
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object originalResult = method.invoke(configuration, args);
            if (method.getName().equals("getMappedStatement")) {
                return getCachedMappedStatement((MappedStatement) originalResult);
            }
            return originalResult;
        }
    }
    private MappedStatement getCachedMappedStatement(MappedStatement mappedStatement) {
        return new MappedStatement
                .Builder(mappedStatement.getConfiguration(), mappedStatement.getId(),
                     mappedStatement.getSqlSource(), mappedStatement.getSqlCommandType())
                .databaseId(mappedStatement.getDatabaseId())
                .resource(mappedStatement.getResource())
                .fetchSize(mappedStatement.getFetchSize())
                .timeout(mappedStatement.getTimeout())
                .statementType(mappedStatement.getStatementType())
                .resultSetType(mappedStatement.getResultSetType())
                .parameterMap(mappedStatement.getParameterMap())
                .resultMaps(mappedStatement.getResultMaps())
                .cache(IgniteCacheAdapter.INSTANCE)
                .useCache(true)
                .build();
    }
}


Achten Sie auf die Zeile:

result.setDatabaseSchemaUpdate("create-drop");

Hier haben wir die automatische Erstellung von Activiti-Tabellen bereitgestellt. Tun Sie dies nicht in der Produktion.

Jetzt müssen Sie Ignite verbinden. Ich werde seine Installation und Konfiguration hier nicht beschreiben, die Version 1.7.0 wurde verwendet. In der einfachsten Version, die ich verwendet habe, ist es ziemlich einfach, sie herunterzuladen und zu entpacken. Es gibt zwei Möglichkeiten, es in der Anwendung zu konfigurieren: über XML, da Ignite eine Spring-Anwendung ist, oder über Java-Code. Ich habe die zweite Option gewählt:

Die einfachste Konfiguration für Ignite in Java
        IgniteConfiguration igniteCfg = new IgniteConfiguration();
        igniteCfg.setGridName("testGrid");
        igniteCfg.setClientMode(true);
        igniteCfg.setIgniteHome("");
        CacheConfiguration config = new CacheConfiguration();
        config.setName("myBatisCache");
        config.setCacheMode(CacheMode.LOCAL);
        config.setStatisticsEnabled(true);
        config.setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_SYNC);
        igniteCfg.setCacheConfiguration(config);
        TcpDiscoverySpi tcpDiscoverySpi = new TcpDiscoverySpi();
        TcpDiscoveryJdbcIpFinder jdbcIpFinder = new TcpDiscoveryJdbcIpFinder();
        jdbcIpFinder.setDataSource(dataSource);
        tcpDiscoverySpi.setIpFinder(jdbcIpFinder);
        tcpDiscoverySpi.setLocalAddress("localhost");
        igniteCfg.setDiscoverySpi(tcpDiscoverySpi);
        TcpCommunicationSpi tcpCommunicationSpi = new TcpCommunicationSpi();
        tcpCommunicationSpi.setLocalAddress("localhost");
        igniteCfg.setCommunicationSpi(tcpCommunicationSpi);


Die IgniteCacheAdapter-Klasse, in der diese Konfiguration liegt, basiert auf der maximal vereinfachten Version der Klasse aus der org.mybatis.caches-Bibliothek: mybatis-ignite. Das ist alles, unsere Anfragen werden zwischengespeichert. Achten Sie auf den angegebenen Pfad zur Ignite-Laufzeit, hier müssen Sie Ihren eigenen ersetzen.

Ergebnisse


Sie können die Anwendung mit den im Handbuch [2] beschriebenen REST-Dienstaufrufen testen. Es gibt einen einfachen Geschäftsprozess zum Überprüfen eines Lebenslaufs. Wenn Sie es mehrmals ausführen, können Sie die Statistiken anzeigen, deren Auflistung mit dem Befehl config.setStatisticsEnabled (true) aktiviert wurde:

Ignition.ignite("testGrid").getOrCreateCache("myBatisCache").metrics();

Im Debug sehen Sie diese Metriken, insbesondere die Anzahl der Lesevorgänge aus dem Cache und die Anzahl der Fehlschläge. Nach 2 Starts des Prozesses, 16 Messwerte und 16 Fehlschläge. Das heißt, sie haben nie den Cache erreicht.

Schlussfolgerungen


Wie sich herausstellte, wird im betrachteten Beispiel der L2-Cache nicht benötigt. Aber es war ein sehr einfaches und nicht bezeichnendes Beispiel. In einer komplexeren Topologie und mit einer anderen Art der Last ist das Bild bei mehreren Benutzern möglicherweise unterschiedlich. Wie sie sagen, werden wir suchen ...

Außerdem zeigte der Artikel die Möglichkeit einer nicht sehr unhöflichen Störung in einer großen Bibliothek für eine signifikante Änderung ihres Verhaltens.

Referenzen



Jetzt auch beliebt: