Настройка и использование Apache Ignite в качестве MyBatis кэш второго уровня (L2 cache)

    В этой статье я расскажу о том, как настроить apache ignite в качестве 2-го уровня кэша для MyBatis и посмотреть запись кэша в Apache Ignite.

    image

    Что такое Apache Ignite? Это распределенная, высокопроизводительная платформа для вычислений в оперативной памяти (In-memory) с основными характеристиками:

    • с распределенным хранилищем объектов In-memory data grid, имплементацией JSR 107 (Jcache)
    • с распределенными вычислениями в оперативной памяти
    • с распределенной системой обмена сообщениями и событиями
    • с ускорителем (In-memory accelerator) для Hadoop и Spark.

    Почему именно Apache Ignite? Мы долго использовали EhCache и Oracle Coherence, после чего перешли на HazelCast из-за простоты его использования. В последних версиях HazelCast снизил производительность в open source версии, а также нам было интересно использовать его как единую платформу для spark и Hadoop.

    Почему MyBatis? Выбор между Hibernate и MyBatis – это как выбор между брендами BMW и Mercedes. Для нас очень важна поддержка Native SQL и расположение SQL-скриптов в одном месте (не разбросанных по всем исходным кодам), чтобы было удобно оптимизировать SQL-запросы.

    Недавно Apache Ignite анонсировал поддержку MyBatis в качестве 2-ого уровня кэша, и мы решили протестировать его функциональность и производительность. Любые операции с базами данными стоят дорого, поэтому одна из основных задач для увеличения производительности систем – уменьшить число обращений к БД: т.е. использовать кэш.
    Время отклика на запрос можно рассчитать по простой формуле:

    T = tacq + treq + texec + tres

    где:

    tacq – время приобретения соединения
    treq – время отправки запроса к БД
    texec – время выполнения запроса в БД
    tres – время получения ответа от БД


    Для хорошо оптимизированного запроса минимальное время отклика составляет от 20 до 150 мс.

    Технический MyBatis поддерживает 2 уровня кэша по умолчанию:

    • кэширование в локальной сессии Local cache (включено по умолчанию)
    • кэш второго уровня 2nd level


    По умолчанию MyBatis использует только кэширование первого уровня (L1 Cache), то есть кэшированные в одной сессии объекты не доступны для другой. Однако глобальный 2-ой уровень тоже можно использовать: в нем кэшированные объекты будут доступны для всех сессий. Обычно это улучшает производительность, потому что каждая новая сессия использует данные из L2 кэш-памяти.

    MyBatis 2nd level кэш хранит данные или информации о объектах (entitiy data), а не собственный объект как в hibernate. Данные в кэше хранятся в формате ‘Serialized’ – хэш-таблице, где ключ – это идентификатор сущности, а значения – список значения параметров.

    В примере ниже вы увидите кэш записей в apache ignite для MyBatis 2nd level cache.



    Где:

    cache key: [idHash=1499858, hash=2019660929, checksum=800710994, count=6, multiplier=37,hashcode=2019660929, updateList=[com.blu.ignite.mapper.UserMapper.getUserObject, 0, 2147483647, SELECT * FROM all_objects t where t.OBJECT_TYPE='TABLE' and t.object_name=?, USERS, SqlSessionFactoryBean]]
    Value class: java.util.ArrayList
    Cache value: [UserObject [idHash=243119413, hash=1658511469, owner=C##DONOTDELETE, object_type=TABLE, object_id=94087, created=Mon Feb 15 13:59:41 MSK 2016, object_name=USERS]]


    В нашем случае ключ – это SQL запрос "SELECT * FROM all_objects t where t.OBJECT_TYPE='TABLE' and t.object_name=?"

    Как пример я взял системную таблицу ‘all_objects’ из СУБД Oracle и следующие запросы:

    QUERY_1: SELECT * FROM all_objects t where t.OBJECT_TYPE='TABLE' and t.object_name='EMP';
    QUERY_2: SELECT * FROM all_objects t where t.OBJECT_TYPE='TABLE';
    QUERY_3: SELECT count(*) FROM all_objects;

    Характеристики SandBox:
    Кластер Apache ignite
    2 виртуальных машины (VM Ware)
    CPU: 2
    RAM: 4 ГВ
    Java HEAP: 2 ГВ
    OS: Red Hat Santigo
    JVM: Oracle JVM 1.7_45
    Oracle 12c
    виртуальная машина (VM Ware)
    CPU: 4
    RAM: 8 ГВ
    OS: Red Hat Santigo
    Standalone java app + SoapUI
    MacBook Pro
    CPU: 4
    RAM: 16 ГВ
    JVM: Oracle JVM 1.7_45

    Если выполнять выше указанные SQL-запросы (QUERY1-3) через SQL Developer, получим следующее время отклика:

    Наименование запроса
    Время отклика (mc)
    1
    QUERY_1: SELECT * FROM all_objects t where t.OBJECT_TYPE='TABLE' and t.object_name='EMP';
    ~660
    2
    QUERY_2: SELECT * FROM all_objects t where t.OBJECT_TYPE='TABLE';
    ~378
    3
    QUERY_3: SELECT count(*) FROM all_objects;
    ~700

    Теперь добавим apache ignite в качестве кэша 2-го уровня и посмотрим на результат. Инструкцию по установке apache ignite вы можете найти в моем блоге, а все исходные коды – на github.

    Добавляем mybatis-ignite библиотеку в проекте Мавена:

    <dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ignite</artifactId>
    <version>1.0.0-beta1</version>
    </dependency>

    Добавляем MyBatis sql mapper

    <mapper namespace="com.blu.ignite.mapper.UserMapper">
    <cache type="org.mybatis.caches.ignite.IgniteCacheAdapter" />
    <select id="getUserObject" parameterType="String" resultType="com.blu.ignite.dto.UserObject" useCache="true">
          SELECT * FROM all_objects t where t.OBJECT_TYPE='TABLE' and t.object_name=#{objectName}
    </select>
    <select id="getAllObjectsTypeByGroup" parameterType="String" resultType="com.blu.ignite.dto.UobjectGroupBy" useCache="true">
          SELECT t.object_type, count(*) as cnt FROM all_objects t group by t.OBJECT_TYPE
    </select> 
    <select id="allObjectCount" parameterType="String" resultType="String" useCache="true">
          SELECT count(*) FROM all_objects
    </select>
    </mapper>

    Здесь мы:
    • указываем кэш-адаптер на IgniteCacheAdapter
    • для каждого SQL-запроса указываем useCache=«true», то есть включаем режим кэширования.

    Добавляем ignite spring конфигурации

    <bean id="ignite.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
    <property name="gridName" value="TestGrid"/>
     <property name="clientMode" value="false"/>
     <property name="cacheConfiguration">
            <list>
                     <bean class="org.apache.ignite.configuration.CacheConfiguration">
                          <property name="name" value="myBatisCache"/>
                    <property name="cacheMode" value="PARTITIONED"/>
                          <property name="backups" value="1"/>
                   <property name="statisticsEnabled" value="true" />
                   <property name="writeSynchronizationMode" value="FULL_SYNC"/>
                </bean>
            </list>
        </property>
        <property name="discoverySpi">
            <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
                <property name="ipFinder">
    <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder">
                        <property name="addresses">
                            <list>
                                <value>IP_ADDRESS_IGNITE_NODE</value>
                                <value>IP_ADDRESS_IGNITE_NODE</value>
                            </list>
                        </property>
                    </bean>
                </property>
            </bean>
        </property>
    </bean>
    

    Обратите особое внимание на настройку ‘clientMode’ с значением false. Она позволяет подключить cacheMode = Partitioned, где мы используем секционированный (Partitioned) кэш для разделения данных между узлами кэширования. Другой возможный вариант – это включение реплицирующего (Replicated) режима, с помощью данные реплицируются между всеми узлами кэширования.

    statisticsEnabled = true, позволяет получить статистику использования кэш: hit count и.т.д

    writeSynchronizationMode= FULL_SYNC, позволяет полностью синхронизовать кэшированные данные с резервными узлами.

    Добавляем соответствующий Java-интерфейс:

    public interface UserMapper {
           User getUser( String id);
    List<string> getUniqueJob();
    UserObject getUserObject(String objectName);
    String allObjectCount();
    List<uobjectgroupby> getAllObjectsTypeByGroup();
    }

    А также простой soap-сервис

    @WebService(name = "IgniteTestServices",
            serviceName=" IgniteTestServices ",
            targetNamespace = "http://com.blu.rules/services")
    public class WebServices {
        private UserServices userServices;
     
        @WebMethod(operationName = "getUserName")
        public String getUserName(String userId){
            User user = userServices.getUser(userId);
            return user.getuName();
        }
        @WebMethod(operationName = "getUserObject")
        public UserObject getUserObject(String objectName){
            return userServices.getUserObject(objectName);
        }
        @WebMethod(operationName = "getUniqueJobs")
        public List<string> getUniqueJobs(){
            return userServices.getUniqueJobs();
        }
        @WebMethod(exclude = true)
        public void setDao(UserServices userServices){
            this.userServices = userServices;
        }
        @WebMethod(operationName = "allObjectCount")
        public String allObjectCount(){
            return userServices.allObjectCount();
        }
        @WebMethod(operationName = "getAllObjectsTypeCntByGroup")
        public List<uobjectgroupby> getAllObjectsTypeCntByGroup(){
            return userServices.getAllObjectCntbyGroup();
        } 
    }
    

    После успешной компиляции проекта если будем вызвать веб-метод ‘getAllObjectsTypeCntByGroup’, то через SoapUi время отклика увеличится до ~1600 мс в моем случае.



    Со второго раза время отклика должно значительно снизиться, потому что результат возвращается из кэш apache ignite, и запрос к БД не поступает.



    Теперь после первого раза вызова веб-метода, время отклика займет от 5-6 мс.

    В apache ignite запись кэш будет выглядеть следующим образом:



    cache key: [idHash=46158416, hash=1558187086, checksum=2921583030, count=5, multiplier=37, hashcode=1558187086, updateList=[com.blu.ignite.mapper.UserMapper.getAllObjectsTypeByGroup, 0, 2147483647, SELECT t.object_type, count(*) as cnt FROM all_objects t group by t.OBJECT_TYPE, SqlSessionFactoryBean]]
    Value class: java.util.ArrayList
    Cache value: [UobjectGroupBy [idHash=2103707742, hash=1378996400, cnt=1, object_type=EDITION], UobjectGroupBy [idHash=333378159, hash=872886462, cnt=444, object_type=INDEX PARTITION], UobjectGroupBy [idHash=756814918, hash=1462794064, cnt=32, object_type=TABLE SUBPARTITION], UobjectGroupBy [idHash=931078572, hash=953621437, cnt=2, object_type=CONSUMER GROUP], UobjectGroupBy [idHash=1778706917, hash=1681913927, cnt=256, object_type=SEQUENCE], UobjectGroupBy [idHash=246231872, hash=1764800190, cnt=519, object_type=TABLE PARTITION], UobjectGroupBy [idHash=1138665719, hash=1030673983, cnt=4, object_type=SCHEDULE], UobjectGroupBy [idHash=232948577, hash=1038362844, cnt=1, object_type=RULE], UobjectGroupBy [idHash=1080301817, hash=646054631, cnt=310, object_type=JAVA DATA], UobjectGroupBy [idHash=657724550, hash=1248576975, cnt=201, object_type=PROCEDURE], UobjectGroupBy [idHash=295410055, hash=33504659, cnt=54, object_type=OPERATOR], UobjectGroupBy [idHash=150727006, hash=499210168, cnt=2, object_type=DESTINATION], UobjectGroupBy [idHash=1865360077, hash=727903197, cnt=9, object_type=WINDOW], UobjectGroupBy [idHash=582342926, hash=1060308675, cnt=4, object_type=SCHEDULER GROUP], UobjectGroupBy [idHash=1968399647, hash=1205380883, cnt=1306, object_type=PACKAGE], UobjectGroupBy [idHash=1495061270, hash=1345537223, cnt=1245, object_type=PACKAGE BODY], UobjectGroupBy [idHash=1328790450, hash=1823695135, cnt=228, object_type=LIBRARY], UobjectGroupBy [idHash=1128429299, hash=1267824468, cnt=10, object_type=PROGRAM], UobjectGroupBy [idHash=760711193, hash=1240703242, cnt=17, object_type=RULE SET], UobjectGroupBy [idHash=317487814, hash=61657487, cnt=10, object_type=CONTEXT], UobjectGroupBy [idHash=1079028994, hash=1960895356, cnt=229, object_type=TYPE BODY], UobjectGroupBy [idHash=276147733, hash=873140579, cnt=44, object_type=XML SCHEMA], UobjectGroupBy [idHash=24378178, hash=1621363993, cnt=1014, object_type=JAVA RESOURCE], UobjectGroupBy [idHash=1891142624, hash=90282027, cnt=10, object_type=DIRECTORY], UobjectGroupBy [idHash=902107208, hash=1995006200, cnt=593, object_type=TRIGGER], UobjectGroupBy [idHash=142411235, hash=444983119, cnt=14, object_type=JOB CLASS], UobjectGroupBy [idHash=373966405, hash=1518992835, cnt=3494, object_type=INDEX], UobjectGroupBy [idHash=580466919, hash=1394644601, cnt=2422, object_type=TABLE], UobjectGroupBy [idHash=1061370796, hash=1861472837, cnt=37082, object_type=SYNONYM], UobjectGroupBy [idHash=1609659322, hash=1543110475, cnt=6487, object_type=VIEW], UobjectGroupBy [idHash=458063471, hash=1317758482, cnt=346, object_type=FUNCTION], UobjectGroupBy [idHash=1886921697, hash=424653540, cnt=7, object_type=INDEXTYPE], UobjectGroupBy [idHash=1455482905, hash=1776171634, cnt=30816, object_type=JAVA CLASS], UobjectGroupBy [idHash=49819096, hash=2110362533, cnt=2, object_type=JAVA SOURCE], UobjectGroupBy [idHash=1916179950, hash=1760023032, cnt=10, object_type=CLUSTER], UobjectGroupBy [idHash=1138808674, hash=215713426, cnt=2536, object_type=TYPE], UobjectGroupBy [idHash=305229607, hash=340664529, cnt=23, object_type=JOB], UobjectGroupBy [idHash=1365509716, hash=623631686, cnt=12, object_type=EVALUATION CONTEXT]]

    Оценка производительности:


    Хотя наши тесты не образец правильного вычисления производительности (мы не использовали connection pool, а также не оптимизировали SQL-запросы), они все же помогают вычислить прирост производительности по обычной формуле:

    Прирост производительности = Время отклика без кэширования/Время отклика с кэшированием = 1589/6 что приблизительно в 265 раз быстрее или прирост производительности = ((Время отклика без кэширования- Время отклика с кэшированием)/ Время отклика с кэшированием * 100) приблизительно на 26 383% быстрее.

    Таким образом, кэш 2-го уровня позволяет увеличить производительность систем в сотню по сравнению с подходом без использования кэша.
    AT Consulting 51,91
    Компания
    Поделиться публикацией
    Комментарии 16
    • +1
      Не понял аналогию Hibernate vs. MyBatis -> BMW vs. Mercedes. Что вы имеете ввиду?
      • 0
        у Оба framework Hibernate и MyBatis есть свой достойнства и недостатки. Выбор очень сильно зависит от системного требования проекта. Очень хотел избежать от дебаты типа "Hibernate лучшее чем MyBatis" или "MyBatis лучшее Hibernate".
      • +2
        получим следующее время отклика:

        Эка вы как классно профилируете запросы! Я так тоже умею — можно несколько раз позапускать запрос в SQL Developer и он закешируется в самой оракл и никакой MyBatis 2 Level Cache не нужен :)
        Если серьезно, MyBatis кеш конечно помогает и выручает, но профит посчитан неверно.
        • 0
          Спасибо за замечания, следующий раз будем постараться улучшить статью
          • 0
            В принципе можно было бы поставить логирование времени на входе выполнения в MyBatis и на выходе с учетом кеша.
            Вход понятен где — это вызов метода маппера.
            В методе по выборке из кеша это второе время.
            После вызова маппера это третье время.
            Работу самого MyBatis'a в данном случае можно пренебречь. Уже тремя этими параметрами можно как то манипулировать и делать выводы.
            Кстати, у Вас все виртуальные машины расположены локально? Если да, то имеет смысл их разнести на разные машины в рамках локальной сети, это будет еще ближе к "боевым" измерениям (потери на сетевом транспорте).
            • 0
              Да, все виртуальные машины расположены в локальной сети.
        • 0
          а также нам было интересно использовать его как единую платформу для spark и Hadoop.

          Вопрос немного не по теме. Вы alluxio не пробовали для этих целей?
          • 0
            нет, мы не попировали alluxio
            • 0
              'попировали' опечатка )). Мы не попровали alluxio
          • +1
            Статья гуд!)
            • +1
              А как происходит инвалидация кэша? что если по ключу "SELECT * FROM all_objects t where t.OBJECT_TYPE='TABLE' and t.object_name=?" в базе есть новые данные?
              • 0
                Существуют несколько способ:
                • В настройках кэш apache ignite: политика Expire — по подробнее можно читать здесь
                • На уровне MyBatis mapper: В операциях CRUD в XML можно указать flushCache=«true»
                • А если в таблице изменился данные мимо DAO, то есть кто не будут или 3ая система изменил данные в БД прямую: В этом случае необходимо сбросит или обновить кэш ignite, для реализации таких случае у Oracle есть возможности так называемое «Oracle database change notification», более подробнее можно узнать здесь
                • 0
                  или 3ая система изменил данные в БД прямую

                  Кто такая эта "Зая"? Я бы не советовал пускать Зай и Мась в продакшн :)
                  • 0
                    "Зая" — 3-ая или любой legecy систем )))
                • 0
                  Прирост производительности = Время отклика без кэширования/Время отклика с кэшированием = 1589/6 что приблизительно в 265 раз быстрее или прирост производительности = ((Время отклика без кэширования- Время отклика с кэшированием)/ Время отклика с кэшированием * 100) приблизительно на 26 383% быстрее.

                  Выглядит круто, но статья про L2 кэш, а вычисления для случая, когда L1 также пуст. В реальности же увеличение производительности не будет больше, чем число нод. А если еще и sticky-session использовать, то того меньше.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое