Статьи

NuoDB и хранимые процедуры: как максимизировать производительность Hibernate

В этой статье рассказывается о нескольких случаях использования Java Hibernate, которые выполняют многоэтапные обновления запросов. Я укажу на недостатки производительности при выполнении примера кода Hibernate на стороне клиента, а не на его выполнение на основе хранимой процедуры NuoDB на стороне сервера.

Но прежде чем мы углубимся в код, лучше понять некоторые основы NuoDB.

Что отличает NuoDB от других предложений СУБД?

Давайте рассмотрим гипотетический сценарий: Джон создал замечательное приложение, которое очень взволновало пользователей, и каждый час в нем скачут сотни пользователей. Теперь Джон должен масштабировать свое приложение, как только он может!

У него осталось два варианта:

  • Он может использовать дорогие машины и / или страшные обходные пути для масштабирования своей базы данных.
  • Он может переписать все приложение и использовать облегченную, масштабируемую СУБД NoSQL.

Но при этом он должен следить за тем, чтобы не идти на компромиссы между производительностью и надежностью, что невозможно в обоих вышеописанных сценариях.

Что если у него был третий вариант?

Скажем, что там была RDBMS, которая не имела сложных обходных путей. Скажем, это СУБД, разработанная с нуля, чтобы обеспечить гибкую производительность горизонтального масштабирования без отказа от гарантий SQL / ACID, которые необходимы для многих приложений работающих баз данных. Ну, вот где NuoDB приходит на помощь Джону!

NuoDB — это распределенная база данных веб-масштаба, предназначенная для упругого масштабирования в облачных, локальных и гибридных средах центров обработки данных. 

Начало работы с NuoDB

NuoDB поддерживает MacOS, Windows и различные платформы ОС Linux — см. Загрузку NuoDB , но в этой статье предполагается, что основной операционной системой является GNU / Linux. (Debian)

В оболочке Linux выполните приведенные ниже шаги по настройке, чтобы начать работу с NuoDB:

# In case you haven’t installed maven yet.
$ sudo apt-get install maven2

# Download the latest NuoDB Release.

$ wget http://download.nuohub.org/nuodb-2.0.4.linux.x64.deb

# Install the NuoDB software.

$ sudo dpkg -i nuodb-2.0.4.linux.x64.deb
(Reading database ... 67407 files and directories currently installed.)
Preparing to unpack nuodb-2.0.4.linux.x64.deb ...
 * NuoDB web console already stopped
 * NuoDB auto console already stopped
 * Stopping NuoDB agent
Unpacking nuodb (2.0.4.5) over (2.0.4.5) ...
Setting up nuodb (2.0.4.5) ...
 * Starting NuoDB agent
Processing triggers for libc-bin (2.19-0ubuntu6) ...


# Switch dir to the location where NuoDB is installed
$ cd /opt/nuodb

# Set up a sample database.
$ ./run-quickstart
Setting PATH to /opt/nuodb/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/home/ubuntu/AWS-ElasticBeanstalk-CLI-2.6.1/eb/linux/python2.7
This script will set up a sample NuoDB database.

Starting a database named: test

 Starting a NuoDB Storage Manager
....
....

# After all the DB setup is done, you will end up in a SQL prompt, quit from that.
SQL> exit
-----
NuoSQL has finished.  We've left the 'test@localhost' database running for you.

Learn more about the system:
   Read the documentation online: http://doc.nuodb.com

Experiment with the system:
   Agent CLI: nuodbmgr --user 'quickstart' --password 'quickstart' --broker localhost
   Start Web Console (as root): service nuowebconsole start
   Access Web Console: http://localhost:8080

Run nuosql:
   export PATH=/opt/nuodb/bin:$PATH
   nuosql 'test@localhost' --user 'dba' --password 'goalie' --schema Hockey
-----

Прохладно! Теперь, когда вы установили NuoDB и имеете образец БД, самое время взглянуть на пример кода:

# change dir to the location where nuodb is installed

$ cd /opt/nuodb

# Install the NuoDB jdbc and hibernate jar files in the maven repository.

$ mvn install:install-file -DgroupId=com.nuodb -DartifactId=nuodb-jdbc \

-Dversion=2.0 \

-Dpackaging=jar -Dfile=/opt/nuodb/jar/nuodbjdbc.jar

$ mvn install:install-file -DgroupId=com.nuodb -DartifactId=nuodb-hibernate \

-Dversion=2.0 \

-Dpackaging=jar -Dfile=/opt/nuodb/jar/nuodb-hibernate.jar

# change to hibernate sample dir.

$ cd samples/hibernate

# compile the code

$ sudo mvn compile

# execute the code

$ sudo mvn exec:java

Эврика! Вы запустили свое первое приложение Hibernate с NuoDB.

Анализ производительности

Давайте сделаем анализ производительности MySQL + Hibernate против NuoDB + Hibernate. Прежде чем мы продолжим, мы должны понять хранимые процедуры NuoDB Java.

В NuoDB 2.0 появилась поддержка хранимых процедур на языке Java. Хранимые процедуры Java поддерживают более богатый язык программирования, чем хранимые процедуры SQL, а также обеспечивают легкий путь перехода от кода на стороне клиента к расширению на стороне сервера.

Он включает в себя следующие шаги:

  • Создайте Java-класс, который в основном создает оператор из соединения, выполняет запрос и получает результат.
Statement stmt = conn.createStatement();

  stmt.execute("select * from system.tables");

  ResultSet rs = stmt.getResultSet();
  • Скомпилируйте ваш класс Java и создайте файл JAR. Важно отметить, что файл JAR должен содержать все сторонние классы и / или ресурсы, которые будут использоваться хранимой процедурой.

    В нашем примере это означает классы Hibernate и их зависимости и, поскольку мы используем файлы конфигурации вместо аннотаций, файлы ресурсов Hibernate (* .cf.xml, * .hbm.xml и т. Д.). Самый простой способ сделать это — создать исполняемый файл JAR (например, с помощью eclipse через экспорт, с IntelliJ через артефакт сборки, с Maven через тень).

  • Создать javaclass в NuoSQL:
    `CREATE JAVACLASS classid FROM 'path/to/myfirstjavaproc.jar';`
  • Создайте хранимую процедуру NuoSQL:
    `CREATE PROCEDURE xyz(INOUT s string, INOUT n integer, INOUT d double, INOUT dec decimal(10,2)) LANGUAGE JAVA EXTERNAL 'classid:com.mycompany.MyFirstJavaProc.stored_proc_inout_args_db';`
  • Вызвать хранимую процедуру Java: `EXECUTE xyz (?,?,?,?);`
  • Выгрузите класс Java: `DROP JAVACLASS classid IF EXISTS;`

Сначала без NuoDB:

package com.nuodb.sample;

import java.util.List;

import org.apache.log4j.Logger;

import org.hibernate.Query;

import org.hibernate.SessionFactory;

import org.hibernate.Transaction;

import org.hibernate.cfg.Configuration;

import org.hibernate.Session;

import com.nuodb.sample.model.Address;

import com.nuodb.sample.model.AddressEntity;

import com.nuodb.sample.model.User;

/**

* A sample application that performs a few operations on a hibernate data

* model. The data model is a subset of the "Caveat Emptor" example used in _Java

* Persistence with Hibernate_ (http://caveatemptor.hibernate.org/). This subset

* only contains the User and AddressEntity entity objects and two styles of

* component objects.

*

* This application performs these steps:

*

* 1. Drop and create the necessary tables. To change this behavior, change the

* hbm2ddl.auto value in hibernate.cfg.xml. Allowed values are: "validate",

* "update", "create", or "create-drop".

*

* 2. Create two User records with associated ShippingAddresses.

*

* 3. Print the user records. Note that each record is printed to the console and

* the output shows the identity of the record along with its version number.

* For example, "User (3/0)" is identity 3, version 0. The identity is a

* surrogate key that is assigned when the record is created and does not

* change. The version changes each time the record is modified.

*

* 4. Modify the User's name and billing address by converting fields to

* upper-case. The home address is not changed.

*

* 5. Print the user records a second time to show the modifications. In

* addition to the field changes, note that the identity has not changed but the

* version is incremented (eg: "User (3/0)" becomes "User (3/1)".

*

* Each one of these steps is performed in a separate transaction.

*

* By default, the logging level prints warnings. To override, modify

* log4j.properties or add "-DCONSOLE_LEVEL=DEBUG" to the command line.

*/

public class SampleMain

{

  private static Logger LOG = Logger.getLogger(SampleMain.class);

  public static void main(String[] args)

  {

    runit(); 

  }

    public static int runit()

  {

  Configuration configuration = new Configuration();

  configuration.configure();

  SessionFactory factory = configuration.buildSessionFactory();

  Session session = factory.openSession();

  int users = 0;

  try {

    createUsers(session);

    printUsers(session);

    modifyUsers(session);

    users = printUsers(session);

  } finally {

    session.close();

    factory.close();

  }

    return users;

  }

  /**

  * Create new user records using the given hibernate session.

  */

  private static void createUsers(Session session)

  {

    Transaction txn = session.beginTransaction();

    try {

      // @formatter:off

      createUser(session,

      "Fred", "Flintstone",

      "fredf@example.com", "fred", true,

      "301 Cobblestone Way", "Bedrock", "00001",

      "1 Slate Drive", "Granitetown", "00002");

      createUser(session,

      "Barney", "Rubble",

      "barney@example.com", "barney", false,

      "303 Cobblestone Way", "Bedrock", "00001",

      "1 Slate Drive", "Granitetown", "00002");

      // @formatter:on

      txn.commit();

    } finally {

    if (txn.isActive())

    txn.rollback();

    }

  }

  /**

  * Instantiate a new User instance and save it to the given session.

  */

  // @formatter:off

  private static User createUser(Session session,

  String firstName, String lastName,

  String email, String userName, boolean admin,

  String homeStreet, String homeCity, String homeZip,

  String shipStreet, String shipCity, String shipZip)

  // @formatter:on

  {

    User user = new User(firstName, lastName, userName, "", email);

    user.setHomeAddress(new Address(homeStreet, homeZip, homeCity));

    user.setAdmin(admin);

    // always use home address for billing

    user.setBillingAddress(new Address(homeStreet, homeZip, homeCity));

    user.setShippingAddress(new AddressEntity(shipStreet, shipZip, shipCity));

    user.getShippingAddress().setUser(user);
  
    session.save(user);
  
    LOG.info("Created user " + user);

    return user;

  }

  /**

  * Modify all Users by converting the name and billing address fields to upper

  * case. Home and shipping address are not changed.

  */

  private static void modifyUsers(Session session)

  {

    Transaction txn = session.beginTransaction();

    try {

      Query query = session.createQuery("from " + User.class.getName());

      @SuppressWarnings("unchecked")

      List<User> users = query.list();

      System.out.println("Found " + users.size() + " user records:");

      for (User user : users) {

      LOG.info("Updating " + user);

      user.setFirstname(user.getFirstname().toUpperCase());

      user.setLastname(user.getLastname().toUpperCase());

      user.setAdmin(false);

      Address bill = user.getBillingAddress();

      bill.setStreet(bill.getStreet().toUpperCase());

      bill.setCity(bill.getCity().toUpperCase());

      bill.setZipcode(bill.getZipcode().toUpperCase());

    }

    txn.commit();

    } finally {

      if (txn.isActive())

      txn.rollback();

    }

  }

  /**

  * Iterate over all User records and print them.

  */

  private static int printUsers(Session session)

  {

    int count = 0;

    Transaction txn = session.beginTransaction();

    try {

      Query query = session.createQuery("from " + User.class.getName());

      @SuppressWarnings("unchecked")

      List<User> users = query.list();

      System.out.println("Found " + users.size() + " user records:");

      for (User user : users) {

      System.out.println(user);

      System.out.println("  home: " + user.getHomeAddress());

      System.out.println("  bill: " + user.getBillingAddress());

      System.out.println("  ship: " + user.getShippingAddress());

      count++;

    }

    txn.commit();

    } finally {

      if (txn.isActive())

      txn.rollback();

    }

  return count;

  }

}

Тот же код с __nuoDB__ представлен как хранимая процедура:

package com.nuodb.sample;

import com.nuodb.sample.model.Address;

import com.nuodb.sample.model.AddressEntity;

import com.nuodb.sample.model.User;

import org.hibernate.Query;

import org.hibernate.Session;

import org.hibernate.SessionFactory;

import org.hibernate.cfg.Configuration;

import java.sql.Connection;

import java.util.List;

public class StoredProcMain {

  public static void stored_proc_hibernate(Connection conn, String firstname,String lastname,String username)

  throws java.sql.SQLException {

    System.out.println(String.format("stored_proc_hibernate = " +username));

    Configuration configuration = new Configuration();

    configuration.configure();

    String chorus = System.getProperty("nuodb.url");

    if (chorus != null)

    {

      System.out.println("Using JDBC URL: " + chorus);

      configuration.setProperty("hibernate.connection.url", chorus);

    }

    long start=System.currentTimeMillis();

    SessionFactory factory = configuration.buildSessionFactory();

    Session session = factory.openSession(conn);

    for(int i=0;i<1000;i++)

    {

      User user = new User(firstname+i,lastname+i ,username+i, "", username+i+"@gmail.com");

      user.setHomeAddress(new Address("street", "address", "city"));

      user.setAdmin(false);

      user.setBillingAddress(new Address("street", "address", "city1"));

      user.setShippingAddress(new AddressEntity("street1", "street2", "city3"));

      user.getShippingAddress().setUser(user);

      session.save(user);

    }

    printUsers(session);

    modifyUsers(session);

    printUsers(session);

    System.out.println("total time taken"+(System.currentTimeMillis()-start));

  }

  private static void modifyUsers(Session session)

  {

    Query query = session.createQuery("from " + User.class.getName());

    @SuppressWarnings("unchecked")

    List<User> users = query.list();

    System.out.println("Found " + users.size() + " user records:");

    for (User user : users) 

 { user.setFirstname(user.getFirstname().toUpperCase()); user.setLastname(user.getLastname().toUpperCase()); user.setAdmin(false); Address bill = user.getBillingAddress(); bill.setStreet(bill.getStreet().toUpperCase()); bill.setCity(bill.getCity().toUpperCase()); bill.setZipcode(bill.getZipcode().toUpperCase()); } } /** * Iterate over all User records and print them. */ private static int printUsers(Session session) { int count = 0; Query query = session.createQuery("from " + User.class.getName()); @SuppressWarnings("unchecked") List<User> users = query.list(); System.out.println("Found " + users.size() + " user records:"); for (User user : users) { System.out.println(user); System.out.println(" home: " + user.getHomeAddress()); System.out.println(" bill: " + user.getBillingAddress()); System.out.println(" ship: " + user.getShippingAddress()); count++; } return count; } }

Install the stored procedure and execute it:

nuosql test@localhost --user 'dba' --password 'goalie' --schema test
SQL> use sample;

SQL> CREATE JAVACLASS hsp1 FROM 'nuodb-hibernate-sample.jar'; 

SQL> CREATE PROCEDURE newsp1(IN firstname String,IN lastname String,IN username String) LANGUAGE JAVA EXTERNAL 'hsp1:com.nuodb.sample.StoredProcMain.stored_proc_hibernate' ;

SQL> execute newsp1(?,?,?);

Enter value: "Hemanth"

Enter value: "HM"

Enter value: "GNUMANTH"

Basically, we are doing CRUD operations and printing the users. Here are some performance results:

Created 2000 users in 1000 transactions.  Two users are created in each transaction:

  • With MySQL: __25.032 seconds__
  • With NuoDB (equivalent to MySQL code): __14.038secs__
  • With NuoDB (and Java Stored Procedures): __10seconds__

In applications where every millisecond matters, __10seconds__ instead of __25.032__ is undoubtedly a major performance improvement!

By the way, you can view the results of these queries in `/var/log/nuodb/agent.log`

Now that you have NuoDB running a Hibernate stored procedure, you can simply scale things out by starting one or more additional Transaction Engines. NuoDB takes care of automatically ensuring that the Java code is deployed in any new Transaction Engine that is part of a database.  The simple management command for starting a Transaction engine is the following:

$ nuodbmgr --broker localhost --user domain --password bird

nuodb [domain] > start process te host scaleouthostname  database test

In this example, scaleouthostname is a new machine that is already provisioned for running NuoDB.

When running the client program, any new connection to the database will take advantage of the scaleouthostname.

So what are you waiting for? Go ahead and use the power of NuoDB!