Atenção: esse artigo tem uma palestra complementar. Clique e assista!

De que se trata o artigo:

Examinamos técnicas de programação geralmente classificadas no domínio de ETL ou Data Warehousing, que permitem manipular volumes muito grandes de dados.


Para que serve:

Persistência e distribuição de informações são requisitos comuns na maioria das aplicações, mas as mesmas técnicas que nos permitem tratar cem mil registros por dia talvez não sirvam para cem milhões de registros por dia. Veremos alguns truques, especialmente de persistência, para encarar esse trabalho pesado de forma robusta e eficiente.


Em que situação o tema é útil:

O principal assunto tratado no artigo é a persistência de dados em grande volume; portanto, será de interesse para programadores que lidam com bases de dados de médio a grande porte (tabelas com milhões de registros ou mais). Falaremos de JDBC, MySQL (especificamente o recurso de particionamento do MySQL 5.1), e apresentaremos dicas gerais para ler, gravar e transmitir grandes fluxos de dados.

Neste artigo, vamos abordar a manipulação de dados em grande volume. Este problema não é intrínseco à plataforma Java; é um desafio enfrentado por desenvolvedores que trabalham com sistemas de back-end de todos os tipos, mas o Java – e especialmente o Java EE – é hoje uma das principais plataformas utilizadas para construir tais sistemas.

Os problemas que vamos discutir exigem, em geral, um tratamento holístico, não podendo ser solucionados apenas por alguns passes de mágica de programação – uma API especial aqui, um algoritmo avançado ali. Pode ser também necessário otimizar seu código SQL ou schemas físicos de SGBD; repensar sua arquitetura de comunicação; ajustar estratégias de integração com outros sistemas, e assim por diante. Assim, vamos falar um pouco de Java, mas também um pouco de outras tecnologias e também de arquitetura em geral. Mas para deixar a discussão toda mais concreta, trabalharemos com um exemplo prático de aplicação.

O sistema de Cotações

Nossa aplicação de exemplo trabalha com cotações de ações (stock quotes). Este é um dos exemplos mais “manjados” da área de mensageria / distribuição de dados. Primeiro, os volumes de dados são enormes, frequentemente na ordem de milhares de mensagens por segundo. Para piorar, estes dados frequentemente têm uma logística complexa de distribuição e processamento, e a ação toda deve acontecer em tempo real.

No pior-caso, em aplicações de HFT (high-frequency trading), bancos de investimento usam supercomputadores com softwares que analisam cotações com modelos matemáticos sofisticados, tentando detectar tendências de curtíssimo prazo, e tomam decisões de compra ou venda milissegundos após uma informação decisiva ser disponibilizada. A necessidade de tanta velocidade é a competição – há vários bancos usando a mesma técnica, e não basta tomar a decisão correta, é preciso ser o primeiro a fazê-lo para ficar com o lucro.

Como se parece uma cotação de ações? Um registro completo – que pode ser obtido de um web service público como o popular Yahoo! Finance – inclui informações como as da Tabela 1, entre muita outras que não vamos incluir aqui para não “inchar” o exemplo.

Campo

Valor

Símbolo da ação

PZE

Valor atual

16.25

Data/Hora da última alteração

15/03/15/2010 04:02

Alteração (fechamento – abertura)

-0.19

Valor de abertura

16.44

Valor máximo no dia

16.54

Valor mínimo no dia

16.25

Volume negociado

20590

Tabela 1. Exemplo de registro de cotações (para a Petrobras Energia / NYSE).

Schema de dados

Vamos criar uma tabela relacional, bastante simples, a partir dos nossos campos de cotação. Eu vou usar o MySQL 5.1, mas somente com código SQL ANSI, de forma que a aplicação funcionará em outros SGBDs.

Listagem 1. Tabela para as cotações.


  CREATE TABLE QUOTE
  (
    symbol     CHAR(5) NOT NULL,
    updateTime DATETIME NOT NULL,
    value      DECIMAL(10,2) NOT NULL,
    valOpen    DECIMAL(10,2),
    valMin     DECIMAL(10,2),
    valMax     DECIMAL(10,2),
    volume     DECIMAL(10)
  );
   
  ALTER TABLE QUOTE ADD CONSTRAINT PK_QUOTE PRIMARY KEY(symbol, updateTime); 

A Listagem 1 mostra a DDL da tabela. Já podemos identificar algumas decisões importantes:

• Não temos nenhuma redundância. Uma coluna para a alteração não é necessária, pois basta subtrair o valor de abertura do valor de fechamento. Também não precisamos de uma coluna para o valor de fechamento, pois este valor é nulo em horários de pregão aberto, e quando não é nulo (pregão fechado) é igual ao valor atual;

• Não temos normalização. Especialmente numa versão mais completa da tabela, um design relacional purista (3NF) determinaria o uso de mais de uma tabela. Esqueça isso. Nossa tabela irá acumular centenas ou milhares de registros por segundo – dezenas de milhões por dia – e queremos dispensar os custos de joins, FKs, etc. Mesmo a chave primária (PK) só foi criada por que esta chave acompanha o índice por symbol + updateTime, importante para consultas à tabela.

Gravando dados

A primeira dica aqui é óbvia: esqueça qualquer ferramenta de persistência de alto nível – JPA, iBatis ou qualquer outra coisa. Você precisará trabalhar somente com JDBC e SQL.

O segundo truque também não é nenhuma novidade: vamos inserir registros em batches, usando as facilidades de PreparedStatement. Porém, o problema não para por aí. Nossa aplicação estará recebendo estes dados continuamente, 24h por dia (imagine que somos alimentados por dados de bolsas de toda parte do mundo: sempre tem algum pregão aberto).

Isso quer dizer que não podemos simplesmente abrir uma conexão, criar um statement preparado de INSERT, fazer um loop gravando uma coleção de registros, fazer commit e terminar. A “coleção de registros” é algo que nunca termina, e não existem transações do ponto de vista negocial. Há apenas um fluxo contínuo de dados que devem ser carregados na tabela.

Listagem 2. POJO para a cotação.


  import java.text.SimpleDateFormat;
  import java.util.Date;
   
  public class Quote {
    private final String symbol;
    private final Date updateTime;
    private final float value;
    private final float valOpen;
    private final float valMin;
    private final float valMax;
    private final int volume;
    
    public Quote (final String symbol, final Date updateTime,
      final float value, final float valOpen, final float valMin,
      final float valMax, final int volume) {
      this.symbol = symbol;
      this.updateTime = updateTime;
      this.value = value;
      this.valOpen = valOpen;
      this.valMin = valMin;
      this.valMax = valMax;
      this.volume = volume;
    }
   
    public final String getSymbol () {
      return symbol;
    }
    public final Date getUpdateTime () {
      return updateTime;
    }
    public final float getValue () {
      return value;
    }
    public final float getValOpen () {
      return valOpen;
    }
    public final float getValMin () {
      return valMin;
    }
    public final float getValMax () {
      return valMax;
    }
    public final int getVolume () {
      return volume;
    }
    
    @Override
    public String toString () {
      return symbol +
        "@" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(updateTime) +
        " = " + value;
    }
  } ... 

Quer ler esse conteúdo completo? Tenha acesso completo