- PVSM.RU - https://www.pvsm.ru -
В предыдущей [1] статье я доходчиво рассказал как Cassandra хранит данные. Настоятельно рекомендую хотя бы пробежаться глазами. В этой статье мы создадим простенькую БД, чтобы использовать её в следующей статье [2], которая будет полностью посвящена выборке/поиску данных.
Допустим у нас есть ad network, который откручивает рекламу. Люди кликают на баннеры, заказчик рекламы платит, мы (сеть), реселлеры (распространители) и хостеры рекламного места имеем на этом доход. Реселлеры рекламного места работают за 20%. Этот процент растёт из-за различных факторов, самое главное, что он не постоянен и новый процент может применяться, например, на клики месячной давности.
Нужно: быстро уметь считать доход каждого реселлера за любой промежуток дней, вести график кликов в режиме реального времени.
Давайте считать, что у нас 6 нод.
Запустим cqlsh.
Connected to Test Cluster at localhost:9160.
[cqlsh 4.1.0 | Cassandra 2.0.2 | CQL spec 3.1.1 | Thrift protocol 19.38.0]
cqlsh>
Создадим keyspace (базу данных).
CREATE KEYSPACE ad_network WITH replication = {
'class': 'SimpleStrategy',
'replication_factor': '3'
};
USE ad_network;
replication_factor — это количество нод, которые будут хранить строку.
Создадим таблицу реселлеров и заполним данными. В таблице будем хранить историю изменений процентной ставки реселлера.
CREATE TABLE reseller (
id text,
effective_since text, -- day in the format of 'YYYY-MM-DD'
reward_percent float, -- value from 0.0 to 1.0
PRIMARY KEY (id, effective_since)
)
WITH CLUSTERING ORDER BY (effective_since DESC);
INSERT INTO reseller (id, effective_since, reward_percent) VALUES ('supaboobs', '2011-02-13', 0.2);
INSERT INTO reseller (id, effective_since, reward_percent) VALUES ('supaboobs', '2012-01-22', 0.25);
INSERT INTO reseller (id, effective_since, reward_percent) VALUES ('supaboobs', '2013-11-30', 0.3);
Кажется, что мы создали три строки. Но те, кто читал предыдущую статью [1], знают, что мы создали одну строку с распределительным ключом 'supaboobs' и тремя кластерными ключами: '2011-02-13', '2012-01-22' и '2013-11-30'. Эта строка, и все последующие, будет храниться на трёх из наших шести нод.
Посмотрим содержимое:
cqlsh:ad_network> SELECT * FROM reseller WHERE reseller='supaboobs';
id | effective_since | reward_percent
-----------+-----------------+----------------
supaboobs | 2013-11-30 | 0.3
supaboobs | 2012-01-22 | 0.25
supaboobs | 2011-02-13 | 0.2
В дальнейшем, когда нам понадобится текущая процентная ставка, мы выполним следующее:
cqlsh:ad_network> SELECT * FROM reseller WHERE id = 'supaboobs' LIMIT 1;
id | effective_since | reward_percent
-----------+-----------------+----------------
supaboobs | 2013-11-30 | 0.3
В этой будем хранить клики на наши баннеры.
Колонки: ID реселлера, день (для ускорения поиска), дата+время клика, ID баннера, полная стоимость клика.
CREATE TABLE ad_click (
reseller_id text,
day text, -- day in the format of 'YYYY-MM-DD'
time timestamp,
ad_id text,
amount float,
PRIMARY KEY ((reseller_id, day), time, ad_id)
)
WITH CLUSTERING ORDER BY (time DESC);
Добавим немного данных.
INSERT INTO ad_click (reseller_id, day, time, ad_id, amount) VALUES ('supaboobs', '2013-11-28', '2013-11-28 02:16:52', '890_567_234', 0.005);
INSERT INTO ad_click (reseller_id, day, time, ad_id, amount) VALUES ('supaboobs', '2013-11-28', '2013-11-28 07:17:35', '890_567_234', 0.005);
INSERT INTO ad_click (reseller_id, day, time, ad_id, amount) VALUES ('supaboobs', '2013-11-29', '2013-11-29 17:18:51', '890_567_211', 0.0075);
INSERT INTO ad_click (reseller_id, day, time, ad_id, amount) VALUES ('supaboobs', '2013-11-29', '2013-11-29 22:20:37', '890_567_211', 0.0075);
INSERT INTO ad_click (reseller_id, day, time, ad_id, amount) VALUES ('supaboobs', '2013-11-30', '2013-11-30 11:21:56', '890_567_234', 0.005);
INSERT INTO ad_click (reseller_id, day, time, ad_id, amount) VALUES ('supaboobs', '2013-12-01', '2013-12-01 12:21:59', '890_567_010', 0.01);
Посмотрим на них.
cqlsh:ad_network> SELECT * FROM ad_click;
reseller_id | day | time | ad_id | amount
-------------+------------+---------------------+-------------+--------
supaboobs | 2013-12-01 | 2013-12-01 12:21:59 | 890_567_010 | 0.01
supaboobs | 2013-11-30 | 2013-11-30 11:21:56 | 890_567_234 | 0.005
supaboobs | 2013-11-28 | 2013-11-28 07:17:35 | 890_567_234 | 0.005
supaboobs | 2013-11-28 | 2013-11-28 02:16:52 | 890_567_234 | 0.005
supaboobs | 2013-11-29 | 2013-11-29 22:20:37 | 890_567_211 | 0.0075
supaboobs | 2013-11-29 | 2013-11-29 17:18:51 | 890_567_211 | 0.0075
Так как распределительный ключ у нас составной — (reseller_id, day), то здесь фактически создалось 4 строки (если сложно понять почему, то прочтите предыдущую статью [1], и всё встанет на свои места). Получается, что для каждого реселлера мы будем создавать каждый день новую строку и заполнять её данными. Кластерный ключ тоже составной — time, ad_id.
Так как процентная ставка не может меняться чаще, чем раз в день, то можно на этом выиграть немного миллисекунд и процессорного времени. Создадим ещё одну таблицу, которая будет хранить те же деньги, но без разбивки на клики:
CREATE TABLE amount_by_day (
reseller_id text,
day text, -- day in the format of 'YYYY-MM-DD'
amount double,
PRIMARY KEY (reseller_id, day)
)
WITH CLUSTERING ORDER BY (day DESC);
Она должна будет заполнятся раз в сутуки. Отдельный код в нашей системе будет собирать данные из ad_click, суммировать и записывать в amount_by_day .
Естественно, нам важно знать сколько раз кликнули на какой баннер. Но так как запускать SELECT COUNT(0) FROM ad_click WHERE ad_id='...' по всем шести нодам — это было бы слишком накладно (да и не существует операции COUNT в CQL), то в C* есть такая вещь как counter-ы.
Counter — это тип колонки, т.е. синтаксически используется точно так же как timestamp, text, double, и пр. Но есть ограничения. Если в таблице есть хоть один counter, то все остальные колоки тоже должны быть типа counter (исключая PRIMARY KEY, конечно же). Создадим же таблицу:
CREATE TABLE clicks_per_ad (
ad_id text,
clicks counter,
PRIMARY KEY (ad_id)
);
А вот так в таблице можно изменять значение колонки clicks.
cqlsh:ad_network> SELECT * FROM clicks_per_ad;
(0 rows)
cqlsh:ad_network> UPDATE clicks_per_ad SET clicks = clicks + 1 WHERE ad_id = '890_567_234';
cqlsh:ad_network> SELECT * FROM clicks_per_ad;
ad_id | clicks
-------------+--------
890_567_234 | 1
(1 rows)
cqlsh:ad_network> UPDATE clicks_per_ad SET clicks = clicks + 1 WHERE ad_id = '890_567_234';
cqlsh:ad_network> SELECT * FROM clicks_per_ad;
ad_id | clicks
-------------+--------
890_567_234 | 2
(1 rows)
cqlsh:ad_network> UPDATE clicks_per_ad SET clicks = 0 WHERE ad_id = '890_567_234';
cqlsh:ad_network> SELECT * FROM clicks_per_ad;
ad_id | clicks
-------------+--------
890_567_234 | 0
(1 rows)
Таким образом можно считать что угодно, если оно signed int. Т.е. можно считать исключительное целые числа, но зато в диапазоне -2^63 — +2^63.
Примечательно, что сначала не было строк в таблице, но после команды UPDATE одна вдруг появилась. Это особенность CQL. INSERT и UPDATE — суть одна и та же комманда. Оговорюсь, что в С* есть возможность не обновлять/вставлять данные если они уже (или 'ещё не') существуют. Она называется «лёгкие транзакции» (lightweight transactions [3]), которые работают медленно относительно обычной операции записи данных.
Конечно, количество кликов можно собирать по любым критериям (ключам). Например:
CREATE TABLE clicks_per_reseller_per_day (
reseller_id text,
day text, -- day in the format of 'YYYY-MM-DD'
clicks counter,
PRIMARY KEY ((reseller_id, day))
);
CREATE TABLE clicks_per_reseller (
reseller_id text,
clicks counter,
PRIMARY KEY (reseller_id)
);
В RDBMS мы привыкли назначать строкам уникальный идентификатор типа int. Почему бы не делать идентификаторы текстовыми, которые бы означали что-либо осмысленное? Да потому, что производительность тогда пострадает. Лично меня это сильно угнетало. Мы привыкли, что ID наших водительских прав — цифры, ID страхового полиса — цифры. Но ведь часто приходится примешивать буквы, например ID пасспорта — две буквы и 6 цифр, номера домов часто с буквами или дефисами, и пр.
В С* не принято использовать сухие цифры как ключи, потому что они не несут ускорения работы в отличие от RDBMS. Да и auto increment в С* отсутствует (зато есть timeuuid [4], если вдруг понадобится уникальный ID). Непривычным может показаться text как тип колонки reseller_id. В С* распределительный ключ (partition key) ищется путём сравнения хешей. Т.е. не происходит прямое сравнение строк, а значит не приседает производительность.
Чтобы записать один клик нам надо будет делать аж 4 UPDATE операций. Я не сошел с ума. Запись в C* чересчур быстаря операция, производительность не пострадает. Скорость записи при 6-ти нодах будет примерно в 100 раз быстрее, чем в MongoDB или в 2-5 раз быстрее, чем в HBase [5], не говоря уж об RDBMS. Последние версии С* умеют самостоятельно оптимизировать место на диске (compaction [6], compression [7]), поэтому и с местом на жестком диске всё будет хорошо.
Чтобы убедиться, что все INSERT-ы сработали, существует такое понятние как BATCH [8]-и.
BEGIN BATCH
-- INSERT, UPDATE, DELETE ...
APPLY BATCH;
Batch-и — не замена транзакциям в RDBMS. С помощью них С* передаёт все команды одним пакетом, а не несколькими коммандами, таким образом оптимизируя работу сети. Существует два вида batch-ей.
BEGIN UNLOGGED BATCH — обычный batch. Но если координирущая нода (coordinator node — та, которая ответственна за пришедшую к ней CQL комманду и за связь с другими нодами) умрёт посреди batch-а, то может испортиться целостность (consistency) данных.BEGIN BATCH — в этом случае С* удостоверится, что или все данные записаны, или ничего. Но эта операция примерно на 30% медленнее.
Этой теме посвящена следующая статья [2]. Операция SELECT ... FROM ... WHERE имеет множество ограничений по сравнению с RDBMS, поэтому на это следует акцентировать особое внимание.
Задачей этого поста было показать на сколько отличается подход к моделированию БД в Кассандре от RDBMS. Подход отличается кардинально, как можно заметить. И пусть вас не смущет схожесть CQL и SQL, на самом деле сопадает только синтаксис. Здесь было показано только несколько приёмов и отличительных особенностей, но их гораздо-гораздо больше.
Автор: koresar
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/nosql/50199
Ссылки в тексте:
[1] предыдущей: http://habrahabr.ru/post/203200/
[2] следующей статье: http://habrahabr.ru/post/205176/
[3] lightweight transactions: http://www.datastax.com/dev/blog/lightweight-transactions-in-cassandra-2-0
[4] timeuuid: http://cassandra.apache.org/doc/cql3/CQL.html#timeuuidFun
[5] в 100 раз быстрее, чем в MongoDB или в 2-5 раз быстрее, чем в HBase: http://www.datastax.com/wp-content/uploads/2013/02/WP-Benchmarking-Top-NoSQL-Databases.pdf
[6] compaction: http://www.datastax.com/docs/1.1/operations/tuning#tuning-compaction
[7] compression: http://www.datastax.com/docs/1.1/operations/tuning#configure-compression
[8] BATCH: http://www.datastax.com/dev/blog/atomic-batches-in-cassandra-1-2
[9] Источник: http://habrahabr.ru/post/204026/
Нажмите здесь для печати.