Google Analytics

среда, 22 сентября 2010 г.

Практическое знакомство с Cassandra: Часть 2 Типы сортировщиков колонок, TimeUUIDHelper и генератор кода Thrift

Этот пост продолжение практического знакомства с Cassandra начатого в "Часть 1 Модель данных и установка". Мы поговорим о типах сортировщиков колонок поставляемых по умолчанию, напишем вспомогательный класс TimeUUIDHelper для работы с TimeUUID и сгенерируем код API для взаимодействия с Cassandra. Это поможет написать учебное приложение. Повторюсь, вся разработка будет вестись на Java под GNU/Linux Debian Squeeze. И так, на данный момент мы должны иметь: настроенный и запускаемый хотя бы один узел Cassandra 0.7.0 beta1, скомпилированный генератор кода из Thrift framework.


Типы сортировщиков колонок

В Column family можно задать тип сортировщика колонок. Он определит порядок, в котором будут храниться и возвращаться данные. Почему это важно? Ответ на этот вопрос содержится в понимание различий между двумя моделями работы с данными: push-on-change - предварительное размножение данных при изменении и простые запросы на извлечение; pull-on-demand - сохранение данных в нормализованном виде без избыточности и конструирование нужного вида при сложном запросе. Конечно порядок хранения это только частный случай упрощения запроса выборки. Полный перечень различий двух моделей это фундаментальное отличие NoSQL и SQL миров. В NoSQL мы храним данные в таком виде, в котором мы хотим их получать. С Cassandra поставляются следующие типы сортировщиков: AsciiType - ASCII сортировка строк; BytesType - сортировка байтовых массивов; LexicalUUIDType - лексическое побайтовая сортировка UUID; LongType - сортировка длинных целых; TimeUUIDType - сортировка по времени UUID версии 1; UTF8Type - сортировка UTF-8 строк. Из описания ясно, что для одного и того же байтового значения названия колонки разные типы дадут разный порядок. По отсортированным данным мы сможем выбирать интервалы колонок. Если тип сортировщика не задан, то используется BytesType.
Хочу обратить внимания на TimeUUID и его особенность. Может возникнуть вопрос: "Для чего использовать TimeUUID? Если можно взять представление времени в миллисекундах". Ответ очень прост: "Для того, чтобы обеспечить уникальность значений". С Cassandra может работать одновременно множество потоков кода, в результате чего появляется возможность одновременного генерирования одного и того-же значения времени. TimeUUID избавляет от таких коллизий, внося элемент случайности в идентификатор, тем самым гарантируя (до определенной степени) его уникальность.

TimeUUIDHelper

В учебном приложении будет использоваться сортировщик TimeUUID. К сожалению, в стандартной библиотеки Java возможно генерировать только UUID версии 3 и 4, поэтому необходимо реализовать работу с UUID версии 1, т.е. TimeUUID. Описания алгоритма реализации и дополнительной информации находятся в RFC4122. Я буду реализовывать работу с TimeUUID с максимальным использованием библиотечных классов Java. По этой причине я не реализую алгоритм в соответствии с пунктом 4.2 RFC4122, а только добавляю временную метку и версию к сгенерированному java.util.UUID объекту, это дает вполне рабочий результат для нашего учебного примера. Для начала посмотрим на jUnit4 тест вспомогательного класса.
package my.blog;

import java.util.Calendar;
import java.util.TimeZone;
import java.util.UUID;

import static org.junit.Assert.*;

import my.blog.TimeUUIDHelper;

import org.junit.Before;
import org.junit.Test;

public class TimeUUIDHelperTest {

    private TimeUUIDHelper timeUUIDHelper;

    @Before
    public void setUp() {
        timeUUIDHelper = new TimeUUIDHelper();
    }

    @Test
    public void testExtractTimeInMillis() {
        /*
         * UUID b54adc00-67f9-11d9-9669-0800200c9a66 содержит временную метку
         * Sunday, January 16, 2005 8:03:37.24 PM GMT
         */
        UUID uuid = UUID.fromString("b54adc00-67f9-11d9-9669-0800200c9a66");
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeZone(TimeZone.getTimeZone("UTC 0"));
        calendar.set(Calendar.MILLISECOND, 24);
        calendar.set(2005, 0, 16, 8, 3, 37);

        assertEquals(calendar.getTimeInMillis(),
                timeUUIDHelper.extractTimeInMillis(uuid));
    }

    @Test(expected = RuntimeException.class)
    public void testNullUUIDExtractTimeInMillis() {
        timeUUIDHelper.extractTimeInMillis(null);
    }

    @Test(expected = RuntimeException.class)
    public void testIncorrectUUIDVersionExtractTimeInMillis() {
        timeUUIDHelper.extractTimeInMillis(UUID.randomUUID());
    }

    @Test
    public void testGenerateTimeUUID() {
        long timeInMillis = 56342332423l;
        assertEquals(timeInMillis,
                timeUUIDHelper.extractTimeInMillis(timeUUIDHelper
                        .generateTimeUUID(timeInMillis)));
    }

    @Test
    public void testUUIDToByteArray() {
        String uuidString = "b54adc00-67f9-11d9-9669-0800200c9a66";
        byte[] expectedArray = { 
                (byte) 0xb5, (byte) 0x4a, (byte) 0xdc, (byte) 0x00, 
                (byte) 0x67, (byte) 0xf9, (byte) 0x11, (byte) 0xd9, 
                (byte) 0x96, (byte) 0x69, (byte) 0x08, (byte) 0x00, 
                (byte) 0x20, (byte) 0x0c, (byte) 0x9a, (byte) 0x66 
                };
        UUID uuid = UUID.fromString(uuidString);

        assertArrayEquals(expectedArray, timeUUIDHelper.uuidToByteArray(uuid));
    }

    @Test(expected = RuntimeException.class)
    public void testNullUUIDUUIDToByteArray() {
        timeUUIDHelper.uuidToByteArray(null);
    }

    @Test
    public void testByteArrayToUUID() {
        String uuidString = "b54adc00-67f9-11d9-9669-0800200c9a66";
        byte[] byteArray = { 
                (byte) 0xb5, (byte) 0x4a, (byte) 0xdc, (byte) 0x00, 
                (byte) 0x67, (byte) 0xf9, (byte) 0x11, (byte) 0xd9, 
                (byte) 0x96, (byte) 0x69, (byte) 0x08, (byte) 0x00, 
                (byte) 0x20, (byte) 0x0c, (byte) 0x9a, (byte) 0x66 
                };
        UUID expectedUUID = UUID.fromString(uuidString);

        assertEquals(expectedUUID, timeUUIDHelper.byteArrayToUUID(byteArray));
    }

    @Test(expected = RuntimeException.class)
    public void testNullArrayByteArrayToUUID() {
        timeUUIDHelper.byteArrayToUUID(null);
    }

    @Test(expected = RuntimeException.class)
    public void testNot16LengthByteArrayToUUID() {
        timeUUIDHelper.byteArrayToUUID(new byte[14]);
    }

}
Из кода теста видно, что вспомогательный класс TimeUUIDHelper должен реализовать методы: извлечения количества секунд с начала эпохи из UUID объекта; генерации UUID объекта из количества секунд с начала эпохи; преобразования UUID объекта в byte[]; преобразования byte[] в UUID объект. Хочу отметить важный момент, начало эпохи и точность представления времени в Java и TimeUUID различны. Это потребует дополнительного кода при реализации. ПРЕДУПРЕЖДЕНИЕ! Используйте данный код только в ознакомительных целях. Код не предназначен для использования в реальных приложениях и может содержать ошибки.
package my.blog;

import java.util.UUID;

public class TimeUUIDHelper {

    /**
     * Кратность между представлением времени в 100 наносекунд и милисекунд
     */
    private static final long NANOHUNDREDS2MILLIS = 10000l;
    /**
     * Разность в милисекундах между началами эпох в Time UUID и Java
     */
    private static final long UUIDAGEBEGIN2UNIXAGEBEGIN = 12219336000000l;
    private static TimeUUIDHelper instance;

    public static TimeUUIDHelper getInstance() {
        if (instance == null) {
            instance = new TimeUUIDHelper();
        }
        return instance;
    }

    /**
     * Извлечь временную метку из uuid
     * 
     * @param uuid
     * @return время в милисекундах
     */
    public long extractTimeInMillis(UUID uuid) {
        if (uuid == null)
            throw new RuntimeException("uuid is null");
        if (uuid.version() != 1)
            throw new RuntimeException("uuid.version don't equals 1");
        return uuid.timestamp() / NANOHUNDREDS2MILLIS
                - UUIDAGEBEGIN2UNIXAGEBEGIN;
    }

    /**
     * Сгенерировать Time UUID из времени в милисекундах
     * 
     * @param timeInMillis
     * @return
     */
    public UUID generateTimeUUID(long timeInMillis) {
        UUID uuid = java.util.UUID.randomUUID();
        // преобразование времени из представления
        // Java в представление для TimeUUID
        long timeIn100Nanos = (timeInMillis + UUIDAGEBEGIN2UNIXAGEBEGIN)
                * NANOHUNDREDS2MILLIS;

        long leastSignificantBits = uuid.getLeastSignificantBits();
        long mostSignificantBits = 0l;

        // смотри RFC4122
        mostSignificantBits |= 
            timeIn100Nanos << 32; // time_low
        mostSignificantBits |= 
            (timeIn100Nanos & 0x0000ffff00000000l) >>> 16; // time_mid
        mostSignificantBits |= 
            (timeIn100Nanos & 0x0fff000000000000l) >>> 48;// time_hi
        mostSignificantBits = 
            mostSignificantBits | 0x1000l;// version 1

        return new UUID(mostSignificantBits, leastSignificantBits);
    }

    /**
     * Сгенерировать Time UUID из времени в милисекундах по текущему времени
     * 
     * @return
     */
    public UUID generateTimeUUID() {
        return generateTimeUUID(System.currentTimeMillis());
    }

    /**
     * Преобразовать объект UUID в byte[]
     * 
     * @param uuid
     * @return
     */
    public byte[] uuidToByteArray(UUID uuid) {
        if (uuid == null)
            throw new RuntimeException("uuid is null");
        byte[] byteArray = new byte[16];
        long mostSignificantBits = uuid.getMostSignificantBits();
        long leastSignificantBits = uuid.getLeastSignificantBits();

        int currentShift;
        for (int i = 0; i < 8; i++) {
            currentShift = (7 - i) * 8;
            byteArray[i] = 
                (byte) ((mostSignificantBits >>> currentShift) & 0xff);
            byteArray[8 + i] = 
                (byte) ((leastSignificantBits >>> currentShift) & 0xff);
        }

        return byteArray;
    }

    /**Преобразовать byte[] в объект UUID
     * 
     * @param byteArray
     * @return
     */
    public UUID byteArrayToUUID(byte[] byteArray) {
        if (byteArray == null)
            throw new RuntimeException("byteArray is null");
        if (byteArray.length != 16)
            throw new RuntimeException("byteArray.length don't equals 16");
        long mostSignificantBits = 0;
        long leastSignificantBits = 0;

        int currentShift;
        for (int i = 0; i < 8; i++) {
            currentShift = (7 - i) * 8;
            mostSignificantBits |= 
                (((long) byteArray[i]) & 0xff) << currentShift;
            leastSignificantBits |= (
                    ((long) byteArray[8 + i]) & 0xff) << currentShift;
        }
        return new UUID(mostSignificantBits, leastSignificantBits);
    }

}
Если в коде присутствуют непонятные моменты задавайте вопросы в комментариях.

Генератор кода Thrift

После сборки Thrift в поддиректори ./compiler/cpp/ появится бинарный файл thrift. Это и есть генератор кода. Подчеркну, я не устанавливал приложение, а использовал его из директории с исходными текстами, дабы не загрязнять систему. Для генерирования кода API на нужном языке программирования (в нашем случае Java) выполняем следующую команду:
путь_до_thrift/trift -gen java путь_к_файлу_конфигурации_thrift
Файл с описанием интерфейса thrift находится в директории с Cassandra в поддиректории ./interface и называется cassandra.thrift. После выполнения данной команды в текущей директории появиться поддиректория gen-java, ее код мы подключим к нашему учебному проекту.

Дополнительные источники

Комментариев нет:

Отправить комментарий