Базы данных: введение, часть четвертая
Илья Тетерин
2012-10-15
(use arrow keys or PgUp/PgDown to move slides)
Илья Тетерин
2012-10-15
(use arrow keys or PgUp/PgDown to move slides)
ИТАР ТАСС: В работе российского почтового интернет-сервиса "Мейл.ру" произошел сбой
МОСКВА, 10 октября. /ИТАР-ТАСС/. Пользователи крупнейшего российского почтового сервиса "Мейл.ру" ... испытывают трудности с доступом к своим почтовым ящикам. При попытке проверить почту или открыть письмо на экране появляются сообщения то об ошибке сервера, то о перегрузке базы данных.
Проблемы с доступом к почтовому сервису начались примерно в полдень, при этом остальные сервисы портала "Мейл.ру" работают нормально.
roem.ru 11.10.2011: В результате внедрения новой более оптимальной системы хранения данных произошел программно-аппаратный сбой. Наши специалисты устранили проблему в течении 2-х часов. Сбой состоял в недоступности почты для примерно 8-10% пользователей. Никаких потерь или утечей данных не было.
wikipedia:mail.ru ... почта (22,7 млн человек ежемесячно) ...
pulser: 22.7м в месяц ... 756k в день ... 30 000 человек в час ... 10% - это 3 000 человек в течение часа
это не много, не мало - это проблемы availability в кластере ...
... но они заметны на уровне ИТАР-ТАСС ...
Маленький сервис.
Что-то типа: get USD 15/10/2015
Дабы можно было в моих интернет магазинах пересчитывать цены в рубли, доллары и евро.

Яндекс: Динамика курса USD ЦБ РФ, руб.
Наверное надо "парсить" сайт ЦБ РФ...
Ой, а у них есть технические ресурсы: http://www.cbr.ru/scripts/root.asp.
И там есть Получение данных, использую XML
$ curl http://www.cbr.ru/scripts/XML_dynamic.asp?\ date_req1=12/10/2012\&date_req2=15/10/2012\&VAL_NM_RQ=R01235 <?xml version="1.0" encoding="windows-1251" ?> <ValCurs ID="R01235" DateRange1="12/10/2012" DateRange2="15/10/2012" name="Foreign Currency Market Dynamic"> <Record Date="12.10.2012" Id="R01235"> <Nominal>1</Nominal><Value>31,1667</Value></Record> <Record Date="13.10.2012" Id="R01235"> <Nominal>1</Nominal><Value>30,9738</Value></Record> </ValCurs>
public static void main(final String[] args) throws Exception {
// http://www.cbr.ru
// /scripts/XML_dynamic.asp?date_req1=12/10/2012
// &date_req2=15/10/2012&VAL_NM_RQ=R01235
final URL url = new URL("http", "www.cbr.ru", 80, "" +
"/scripts/XML_dynamic.asp" +
"?date_req1=12/10/2012" +
"&date_req2=15/10/2012" +
"&VAL_NM_RQ=R01235");
final URLConnection uc = url.openConnection();
uc.connect();
final BufferedReader ir = new BufferedReader(
new InputStreamReader(uc.getInputStream()));
String line;
while ((line = ir.readLine()) != null) {
System.out.println(line);
}
ir.close();
}
Ура! Всё работает!
Можно делать сервис!
"/scripts/XML_dynamic.asp?" +
"date_req1=12/10/2012" +
"&date_req2=15/10/2012" +
"&VAL_NM_RQ=R01235");
Хотим на произвольную дату, а тут строки надо вводить.
Плюс торги только в рабочий день, а что на выходных?
final String date1 = getDateAsParameter(new Date(
date.getTime() - 7 * 3600 * 24 * 1000));
final String date2 = getDateAsParameter(date);
final URL url = new URL("http", "www.cbr.ru", 80, "" +
"/scripts/XML_dynamic.asp?" +
"date_req1=" + date1 +
"&date_req2=" + date2 +
"&VAL_NM_RQ=R01235");
private static String getDateAsParameter(final Date dt) {
return new SimpleDateFormat("dd/MM/yyyy").format(dt); }
public static void testDate() {
System.out.println("mark = " + getDateAsParameter(new Date())); }
public static Pair<Date, Double> parseLine(final String line) throws Exception {
final Pattern pattern = Pattern.compile("" +
"<Record Date=\"(.+?)\" Id=\"R01235\"><Nominal>1</Nominal>" +
"<Value>(.+?)</Value></Record>");
final Matcher m = pattern.matcher(line);
if (!m.matches()) {
return Pair.of(new Date(0), 0d);
}
return Pair.of(
new SimpleDateFormat("dd.MM.yyyy").parse(m.group(1)),
Double.parseDouble(m.group(2).replaceAll(",", ".")));
}
public static void testPattern() {
final Pattern pattern = Pattern.compile("" +
"<Record Date=\"(.+?)\" Id=\"R01235\"><Nominal>1</Nominal>" +
"<Value>(.+?)</Value></Record>");
final String sample =
"<Record Date=\"13.10.2012\" Id=\"R01235\"><Nominal>1</Nominal>" +
"<Value>30,9738</Value></Record>";
final Matcher m = pattern.matcher(sample);
if (!m.matches()) return;
System.out.println(m.groupCount());
System.out.println("m1 = " + m.group(1));
System.out.println("m2 = " + m.group(2));
}
В Python есть понятие Tuple (x,y) ... в Java нет ... добавим :)
public static class Pair{ public final K first; public final V second; private Pair(final K k, final V v) { this.first = k; this.second = v; } public static Pair of(final K k, final V v) { return new Pair (k, v); } public String toString() { return "[" + first + "," + second + "]"; } } public static void testPair() { // [7,gugu] System.out.println(Pair.of(7, "gugu")); }
Q: What makes Java so manly?
A: It forces every programmer to grow a Pair.
http://james-iry.blogspot.com/2010/05/anatomy-of-annoyance.html
public static Double getCurrency(final Date date) throws Exception {
final String date1 = getDateAsParameter(new Date(
date.getTime() - 7 * 3600 * 24 * 1000));
final String date2 = getDateAsParameter(date);
final URL url = new URL("http", "www.cbr.ru", 80, "" +
"/scripts/XML_dynamic.asp?" +
"date_req1=" + date1 +
"&date_req2=" + date2 +
"&VAL_NM_RQ=R01235");
final URLConnection uc = url.openConnection();
uc.connect();
final BufferedReader ir = new BufferedReader(
new InputStreamReader(uc.getInputStream()));
Pair max = Pair.of(new Date(0), 0d);
String line;
while ((line = ir.readLine()) != null) {
final Pair parsed = parseLine(line);
if (parsed.first.after(max.first)) {
max = parsed;
}
}
ir.close();
return max.second;
}
1. Делаем внутренний интерфейс:
/**
* Сервис получения курса валюты
*/
interface MyCurrency {
/**
* Возвращает курс доллара на заданную дату.
* Данные выдаются по самой последней дате торгов.
* @param date Date на которую нужна информация
* @return значение курса на дату или 0 в случае ошибки
*/
Double getCurrency(Date date) throws Exception;
}
2. Заворачиваем в http обертку, через которую наши клиенты получают информацию.
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
public class HttpServerEx implements HttpHandler {
Тормозит, бывает ... и не только на нашей стороне
Бывает банят - отказывают в обслуживании, ибо слишком много запросов и слишком часто посылаем.
Ответ: обернем getCurrency() в мемкеш:
// psevdo code !!!
public Double getCurrency(Date date) throws Exception {
Double out = memcached.get(asKey(date));
if ( out == null ) {
out = main.getCurrency(date);
memcached.put(asKey(date),out);
}
return out;
}
Наш "сервис" - это клиент к удаленной базе. Почему он "тормозит"?
final String date1 = getDateAsParameter(new Date(
date.getTime() - 7 * 3600 * 24 * 1000));
final String date2 = getDateAsParameter(date);
final URL url = new URL("http", "www.cbr.ru", 80, "" +
"/scripts/XML_dynamic.asp?" +
"date_req1=" + date1 +
"&date_req2=" + date2 +
"&VAL_NM_RQ=R01235");
final URLConnection uc = url.openConnection();
uc.connect();
final BufferedReader ir = new BufferedReader(new InputStreamReader(uc.getInputStream()));
Pair max = Pair.of(new Date(0), 0d);
String line;
while ((line = ir.readLine()) != null) {
final Pair parsed = parseLine(line);
if (parsed.first.after(max.first)) {
max = parsed;
}}
ir.close();
return max.second;
1. Собрать исходный запрос
- в нашем случае это две даты
2. Установить соединение по сети
- DNS resolve, буфера всякие, ожидания и таймауты
3. Дождаться обработки на удаленной стороне
- кто его знает, что ЦБ внутри делает для ответа
4. Выкачать к себе результаты
- сеть, несколько пакетов TCP etc
5. Распарсить результат из строк (сериализованная форма)
- в нашем случае это дата и дробное число
6. Закрыть сетевое соединение
- может включать сообщение дальней стороне "вешай трубку"
private static String getDateAsParameter(final Date dt) {
return new SimpleDateFormat("dd/MM/yyyy").format(dt); }
Ой, внутри же большой жирный регекс матчинг из-за "гибкого" формата dd/MM/yyyy - вещь универсальная и не быстрая.
public static Pair<Date, Double> parseLine(final String line) throws Exception {
final Pattern pattern = Pattern.compile("" +
"<Record Date=\"(.+?)\" Id=\"R01235\"><Nominal>1</Nominal>" +
"<Value>(.+?)</Value></Record>");
final Matcher m = pattern.matcher(line);
if (!m.matches()) {
return Pair.of(new Date(0), 0d);
}
return Pair.of(
new SimpleDateFormat("dd.MM.yyyy").parse(m.group(1)),
Double.parseDouble(m.group(2).replaceAll(",", ".")));
}
Здесь и capturing регекс и формат и парсинг числа ...
На примере: дайте седьмую запись
Мы точно знаем что мы хотим получить и хотим получить одну единственную запись.
http://host:port/select?id=7
get 7\r\n
select * from users where id = 7
-- ключевое слово - команда
select
-- какие поля ( * )
fio, phone
-- ключевое слово - разделитель
from
-- имя коллекции
phones
-- ключевое слово - ограничения
where
-- ограничения через AND / OR
id = 7
String sql = "select * from persons where id = " + id
Person p = new Person(); // всё остальное null
p.setLastName("Smith");
ObjectCollection oc = QueryExecutor.execute(p);
Query q = new Query();
q.From("PERSON").Where(new EqCriteria("PERSON.LAST_NAME", "Smith"));
ObjectCollection oc = QueryExecutor.execute(q);
Class.forName("com.mysql.jdbc.Driver");
String myDatabaseURL = "jdbc:mysql://mydomain.com/database?user="
+ myUsername + "&password=" + myPassword;
java.sql.Connection con = DriverManager.getConnection(myDatabaseURL);
public interface Command {
void process(Processor p, String cmd) throws IOException;
boolean isApplicable(String cmd); }
public abstract class AbstractDump implements Command {
public boolean isApplicable(String cmd) {
return "/dump".equals(cmd) || "Dump".equals(cmd); }}
public class MasterDump extends AbstractDump {
public void process(Processor p, String cmd) throws IOException {
boolean result = PhoneBook.getPhoneBook().dump();
p.writeResponse(result ? "OK" : "ERROR"); }}
COMMANDS = new ArrayList();
COMMANDS.add(new MasterCommit());
COMMANDS.add(new MasterDump());
for (MasterCommand cmd : COMMANDS) {
if (cmd.isApplicable(string)) {
cmd.process(this, string);
break; }}
Person p = new Person();
p.setName(request.getParameter("name"))
p.setPhone(request.getParameter("phone"));Что можно сделать, дабы было быстрее
Используем "горячий" connection - всегда держим соединение, пока жив наш клиент.
Connection pool (Apache DBCP) - держим N соединений и отдаем их round-robin клиенту.
Чем плохо?
На стороне сервера открыт ответный сокет, удерживаются буфера етс - занята память.
Пример?
Сервер - Оракл, у нас кластер на 32 клиента, в каждом по 16 коннектов в пуле = 2 Mb * 2^5 * 2^4 = 2^10Mb памяти = 1Gb памяти сервера всегда занято.
PreparedStatement ps = null;
try {
ps = connection.prepareStatement("select name from user where user_id = ?");
ps.setLong(1, 928303);
final ResultSet rs = ps.executeQuery();
return rs.next() ? rs.getString(1) : "UNKNOWN";
} finally {
if (ps != null) {
ps.close();
}
}
Отдельно идет запрос, отдельно параметры.
Запрос "обезличен" и может кешироваться как клиентской библиотекой, так и сервером.
Экономия на парсинге и на проверке валидности запроса.
Плохо? - Параметры не учитываются при принятии решений, а это может сказаться на эффективности.
Коллекция, содержащая описание коллекций базы - таблиц, полей, индексов, констрейнов етс.
Подвержено фрагментированию и "торможению".
Важно при парсинге запросов.
http://code.google.com/apis/protocolbuffers/docs/overview.html
Why not just use XML?
Protocol buffers:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
Зародился в Facebook для внутреннего RPC.
С какого-то момента стал Apache Thrift - http://thrift.apache.org/ .
namespace java pulser
struct UserProfile {
1: i32 uid,
2: string name,
3: string phone
}
service UserStorage {
void store(1: UserProfile user),
UserProfile retrieve(1: i32 uid)
}
thrift --gen java sample.thrift
pulser$ wc -l gen-java/pulser/*.java
581 gen-java/pulser/UserProfile.java
1552 gen-java/pulser/UserStorage.java
/**
* Autogenerated by Thrift Compiler (0.8.0)
*
* DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
* @generated
*/
package pulser;
import org.apache.thrift.TException;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TServer.Args;
import org.apache.thrift.server.TSimpleServer;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TServerTransport;
public class MyThriftServer {
public static void startServer(final UserStorage.Processor processor)
throws Exception {
final TServerTransport serverTransport = new TServerSocket(9090);
final TServer server = new TSimpleServer(
new Args(serverTransport).processor(processor));
// Use this for a multithreaded server
// TServer server = new TThreadPoolServer(new
// TThreadPoolServer.Args(serverTransport).processor(processor));
System.out.println("Starting the simple server...");
server.serve();
}
public static void main(final String[] args) throws Exception {
startServer(new UserStorage.Processor(new MyHandler()));
}
}
public class MyThriftClient {
public static void main(final String[] args) {
try {
final TTransport transport = new TSocket("localhost", 9090);
transport.open();
final UserStorage.Client client = new UserStorage.Client(
new TBinaryProtocol(transport));
System.out.println(client.retrieve(7));
transport.close();
} catch (TTransportException e) {
e.printStackTrace();
} catch (TException x) {
x.printStackTrace();
}
}
}
class MyHandler implements UserStorage.Iface {
public void store(final UserProfile user) throws TException {
System.out.println("user = " + user);
}
public UserProfile retrieve(final int uid) throws TException {
return new UserProfile(7, "Ivanov", "8-800-MEGA-623");
}
}