Java: fill the gaps
12.9K subscribers
7 photos
215 links
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк

🔥Тот самый курс по многопочке🔥
https://fillthegaps.ru/mt

Комплименты, вопросы, предложения: @utki_letyat
Download Telegram
Что выведет следующий код?
Anonymous Poll
32%
0
6%
20
45%
30
17%
Ошибку компиляции
👍31🔥105👎3
Best practice: имена методов в ООП и функциональном подходе

В идеальном мире не нужно читать документацию и изучать внутрянку класса, чтобы правильно с ним работать. В этом посте расскажу best practice, о котором мало говорят, но который здорово повышает дружелюбность API.

Начнём с вопроса перед постом. Если не искать подвох, то в sum мы ждём 0+10+20=30.

Но подвох, разумеется, есть. BigInteger — неизменяемый класс. sum.add(10) создаёт новый объект, а исходная переменная не меняется. В итоге ни один add не влияет на sum. В консоль напечатается 0.

Давным-давно основной парадигмой разработки было ООП. Вся работа строилась на изменении объектов, и этот подход отражался в названиях методов: sum.add(4), user.setName("Alisa").

Исключения встречались редко, их нужно было просто запомнить. С юных лет все знают, что String — неизменяемый, и просто вызвать метод недостаточно:

String str = "   Java   ";
str.trim();
str = str.trim();

Последние годы растёт тренд на неизменяемость. При вызове метода должно быть понятно:
▫️ меняется текущий объект, и надо просто вызвать метод
или
▫️ создаётся новый объект, который надо куда-то присвоить

Для такого случая есть best practice:
Когда метод меняет внутреннее состояние объекта, имя метода начинается с глагола
Методы НЕизменяемых объектов используют другие конструкции

Простой пример. Чтобы изменить внутренние поля, используем метод set*:

order.setDeliveryDate(…);

Создать новый объект на основе текущего — метод with*:

order = order.withDeliveryDate(…);

Для более сложных операций нужно включить креативность. Здесь помогут:

🔸 Причастия:

order.cancel(); // изменить текущий объект
Order o = order.cancelled(); // создать новый

🔸 Предлоги и союзы:

String s = str.toLowerCase();
LocalDate l = now().plusDays(12);
// вместо addDays

🔸 Существительное в чистом виде:

String sub = str.substring(1);

Цель здесь одна — показать, что текущий объект не меняется

❗️ Исключение: если класс использует Fluent API, обычно используются глаголы:

Optional opt = …
opt.map(…).filter(…)

Итого: с изменяемыми и неизменяемыми объектами работа идёт по-разному. Имена методов подсказывают, как правильно пользоваться классом. Хорошей практикой считается использовать глаголы для изменяемых объектов, и что-то другое для неизменяемых☀️
🔥140👍7919👎2
VM Options

— это параметры, которые указываются при запуске JVM. В этом посте расскажу, чем они отличаются, и как безопасно перейти на новую версию java. В конце будет список самых популярных (и полезных) опций.

Все JVM опции делятся на три группы:

⚙️ Стандартные
Пишутся через минус и поддерживаются всеми JVM.
Пример: -classpath, -server, -version

⚙️ Нестандартные
Начинаются на -Х и определяют базовые свойства JVM. Могут не работать во всех JVM, но если поддерживаются, то вряд ли удалятся.
Пример: -Xmx, -Xms

⚙️ Продвинутые
Начинаются на -ХХ и касаются внутренних механизмов JVM. Не поддерживаются всеми JVM, часто меняются и удаляются.
Пример: -XX:MaxGCPauseMillis=500

Некоторые продвинутые опции требуют дополнительных флажков. Для экспериментальных фич обязателен -XX:+UnlockExperimentalVMOptions. Многие фичи диагностики не заработают без -XX:+UnlockDiagnosticVMOptions

Количество опций часто меняется. В 11 версии OpenJDK 1504 опции, а в 17 на 200 опций меньше.

Цикл отключения опций не совсем стандартный. В обычном коде что-то помечается Deprecated, и спустя время удаляется. VM Options используют более длинный цикл:

🔸 Deprecate: функционал работает, при запуске появляется warning
🔸 Obsolete: функция не выполняется, JVM пишет предупреждения
🔸 Expired: JVM не запускается

Многие опции очень нестабильны и часто меняются. Чтобы безопасно обновить версию java, нужно проверить набор опций через JaCoLine . Он подсветит устаревшие или уже бесполезные опции.

Полезные опции для java 11
(да, недавно вышла java 20, но самая популярная версия всё ещё 11)

1️⃣ Память

▫️ Начальный размер хипа: -Xms256m в абсолютных значениях, -XX:InitialRAMPercentage=60 - в процентах от RAM
▫️ Максимальный размер хипа: -Xmx8g или -XX:MaxRAMPercentage=60
▫️ Снять heap dump при переполнении памяти: -XX:+HeapDumpOnOutOfMemoryError. Адрес выходного файла задаётся в -XX:HeapDumpPath

2️⃣ Сборщик мусора

▫️ Serial GC: -XX:+UseSerialGC
▫️ Parallel GC: -XX:+UseParalllGC
▫️ CMS: -XX:+UseConcMarkSweepGC
▫️ G1: -XX:+UseG1GC (вариант по умолчанию)
▫️ ZGC: -XX:+UnlockExperimentalVMOptions -XX:+UseZGC
▫️ Shenandoah: -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC

Вывести статистику сборщика при завершении работы: -XX:+UnlockDiagnosticVMOptions ‑XX:NativeMemoryTracking=summary ‑XX:+PrintNMTStatistics

Базовое логгирование коллектора: -Xlog:gc
Максимально информативное: -Xlog:gc*

3️⃣ Посмотреть все доступные опции
⚙️ Нестандартные: java -X
⚙️ Продвинутые: java -XX:+UnlockDiagnosticVMOptions -XX:+UnlockExperimentalVMOptions -XX:+PrintFlagsFinal
🔥118👍434
HashMap и принципы SOLID

На большинстве собесов спрашивают одно и то же. Как работает HashMap, принципы ООП и SOLID, разница абстрактного класса и интерфейса, жизненный цикл бина.

Когда я была junior/middle, 80% вопросов повторялись на каждом собеседовании. И вопросы возникали уже у меня:
🤔 Как интервьюеры поймут, что я топчик, если спрашивают такую банальщину?
🤔 Почему в вакансии целая страница требований и технологий, но мы ничего из этого не обсуждаем?
🤔 Может на проекте всё очень плохо, а код написан на java 6?

И только когда я начала собеседовать людей, то поняла, зачем это всё.

Всё дело в специфике найма джуниоров и мидлов.

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

Поэтому важно, чтобы человек имел крепкий фундамент и был приятным в общении на технические темы. И стандартные вопросы подходят для этого великолепно:

1️⃣ Это база🤌

Желание обсудить саги и агрегаты это здорово, но большую часть времени разработчик проводит с кодом. Stream API, коммиты, структуры данных — в этом не должно быть пробелов.

В далёкие времена на собесах обсуждали только теорию и давали тесты с безумным синтаксисом и пропущенными скобками. Это довольно бесполезно, но всё ещё встречается.

Сегодня собесы больше ориентируются на практику. Бывает, что кандидат лихо объясняет synchronization order, но не видит ошибок в простом многопоточном коде. Не смущайтесь лёгких заданий, они не так просты, как кажутся:)

Время собеседования очень ограничено. Выделенные 30-60 минут лучше потратить на базу. Если с ней всё хорошо — остальное приложится

2️⃣ Легко сравнить кандидатов между собой

Если 10 человек расскажут устройство HashMap, получится 10 разных ответов.

Первый кандидат расскажет так, что ничего не понятно. Второй закопается в объяснении работы хэша и свернёт с темы. Третий оттараторит заученный текст и не поймёт дополнительный вопрос. Из четвёртого придётся тащить каждое предложение.

Поэтому часто стандартных вопросов достаточно, чтобы понять, насколько приятно общаться с человеком и как глубоко он понимает базу. Умничку видно сразу🙂

Если спрашивают банальщину — значит проект скучный?

Для младших грейдов вопросы могут вообще не коррелировать с будущими задачами. И наоборот — алгоритмы и system design не означают, что будущая работа будет интересной и разнообразной

Как выделиться среди других кандидатов, если задают только общие вопросы?

Если вас позвали на собес, значит вы уже прошли жёсткие фильтры, и резюме оставило приятное впечатление. Ваша задача — его закрепить

Если интервьюер чем-то заинтересуется в вашем опыте, он обязательно спросит:)

❗️ Я отвечал на всё правильно, но оффер не прислали!

Причина может быть вообще не в вас. Может пришёл кандидат с более релевантным опытом, или бюджет перераспределили на другие цели.

Найм — очень субъективный процесс. Интервьюер всегда найдёт, к чему прицепится, если у вас фамилия, как у его бывшей жены. И наоборот, если с первых минут установился контакт, даже небольшие ошибки не испортят впечатления.
🔥98👍5914👎6
Intellij IDEA: комментарии TODO

Часто встречаются ситуации, когда нужно запомнить место в коде:
⭐️ Внести изменения по задаче, но чуть позже
⭐️ Отметить непокрытый тестами код
⭐️ Обсудить метод с коллегой

Для таких случаев в IDEA есть специальный тип комментариев. Он начинается со слов TODO и выглядит так:
// TODO добавить тесты

Все такие комментарии можно посмотреть в окне TODO внизу экрана. Через него же можно перейти в нужное место кода в один клик.

Если списка нет, ищите его через View → Tool Windows → TODO

Помимо стандартных TODO и FIXME можно добавить свои метки, например, OPTIMIZE, ASK, TEST. Сделать это можно в File → Settings → Editor → TODO

Очень удобно использовать TODO для текущих задач, чтобы ничего не забыть. Чтобы отметить код, который исправит кто-то другой, не забудьте закинуть соответствующую задачу в бэклог:)
🔥77👍549
Структура проекта и качество кода, часть 1

Структура проекта — это то, как мы раскладываем классы по папочкам. Хорошая структура помогает не только ориентироваться в проекте, но и писать более качественный код.

Сейчас покажу, как это работает.

Разделение по слоям

Начнём с структуры, которая встречается в большинстве туториалов и пет-проектах начинающих:

📂 controller
— UserController, TicketController
📂 service
— UserService, TicketService
📂 repository
— UserRepository, TicketRepository

Чтобы классы могли использовать друг друга, все классы и методы должны быть public.

В такой структуре естественным путём повышается связность. Если в UserService хочется узнать номер билета, то самое простое — добавить TicketRepository и вызвать нужный метод.

Всё связано со всем. Поменяешь в одном месте — сломается в другом. В пет-проекте с этим можно справиться, но для коммерческих проектов такая структура не подходит

Разделение по функциям

Складываем в один пекедж всё, связанное с какой-то сущностью. Оставляем 1-2 класса с модификатором public, остальным даём дефолтный модификатор доступа:

📂 user
— UserController, UserService, UserRepository
📂 ticket
— TicketController, TicketService, TicketRepository
📂 export
— ExportService, ExcelFormatter

Дефолтный модификатор ограничивает доступ между пэкеджами. Если UserService хочет сформировать отчёт по пользователям, он вынужден идти через ExportService, потому что ExcelFormatter ему не виден.

Связность классов снижается, упрощается поддержка и тестирование

😐 Каждый класс решает не бизнес-задачу, а инфраструктурную. UserRepository — точка доступа к таблице users. UserService — класс по работе с классом User. Классы становятся огромными

😐 Высокая связность между бизнес-кейсами. Появляются десятки универсальных методов, которые "переиспользуются" в бизнес-сценариях. Например, создание и редактирование пользователя часто делают через один метод. Меняем одно — неизбежно задеваем похожие сценарии.

Разделение по бизнес-кейсам

Складываем в один пекедж все классы, связанные с бизнес-процессом. Большинство классов стоит с default модификатором и недоступна за пределами пэкеджа:

📂 newUser
— NewUserController, NewUserService, UserRepository
📂 buyTicket
— BuyTicketController, BuyTicketService, TicketRepository
📂 refundTicket — …
📂 export — …

Количество классов увеличивается, но они становятся меньше и более изолированными. Связность между бизнес-сценариями максимально снижается.

Итого: чёткая структура проекта и модификаторы доступа снижают связность между компонентами на уровне компиляции.

Однако очень мало проектов используют эту практику. Не потому что разработчики плохие, а потому что на большинстве проектов этот подход не сработает. Почему так получается и кто виноват — расскажу в следующем посте:)
🔥111👍6415👎10
Структура проекта и качество кода, часть 2

В прошлом посте мы рассмотрели основные структуры, по которым делаются проекты. Структура помогает легко ориентироваться в коде, плюс снижает связность между компонентами за счёт модификаторов доступа.

Просто так использовать default класс из другого пэкеджа (то есть повысить связность) не получится, код не скомпилируется. Либо придётся менять модификатор доступа, что точно будет заметно на ревью.

Но кое-что разрушает эту прекрасную картину: фреймворки

Чтобы Spring мог сотворить волшебство, приходится немного жертвовать изоляцией. Начиная с public репозиториев и заканчивая одним контекстом на всё приложение.

При использовании спринга (или других фреймворков) связность между компонентами меньше ограничивается и с течением времени растёт.

Но выход есть!

Поделить функциональность не на пэкеджи, а на Maven/Gradle модули:

📂 registration
📂 src
— Controller, Service, Repository
📂 test
pom.xml

📂 export

⚠️ Обратите внимание, каждый модуль — просто набор классов и тестов, а не отдельный микросервис!

Связность при таком подходе снижается ещё больше:
У каждого модуля свой набор зависимостей
Нет общего контекста

Можно, наверное, поделить приложение на java модули, но модули Maven/Gradle встречаются гораздо чаще.

Совсем большие проекты идут ещё дальше. В Hexagonal/Clean/Onion/… architecture каждый бизнес-сценарий делится на модули бизнес-правил, адаптеров, инфраструктуры и тд.

Минимальная связность, ультра простое тестирование
😐 Количество модулей, классов и интерфейсов увеличивается в разы
😐 Легко скатиться в карго-культ, нужен опыт для правильной реализации

Резюме

Spring — классный фреймворк, и здорово облегчает рутинные задачи. Но у него есть тёмная сторона — благодаря общему контексту связность кода неизбежно повышается. Чтобы проект не превратился в болото, в первую очередь нужен высокий профессиональный уровень всей команды.

Если приложение большое, имеет смысл поделить его на отдельные модули. У каждого бизнес-процесса будет свой контекст и набор зависимостей. Поддерживать такую структуру будет гораздо проще👍
👍86🔥20👎137
Анонс курса по многопоточке

Старт: 5 июня
Длительность: 9 недель

Кто давно ждал и уже готов → http://fillthegaps.ru/mt

Теперь подробнее. У курса две основные задачи:

Научиться писать хороший многопоточный код

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

Подготовимся к собеседованиям, где требуется concurrency. Обсудим стандартные и нестандартные вопросы, порешаем тестовые задания

Что говорят ученики:

👨‍🦱 “Курс понравился тем, что он "от разработчиков разработчикам": примеры реальных библиотек для разбора, приближенные к реальным задачи для кодинга”
👨‍🦱 ”Курс очень интенсивный, охватывает не только многопоточку, но и смежные темы, учит разным лайфхакам полезным для практического использования, обращает внимание на темы, которые легко или упустить, изучая тему самостоятельно, или вообще можно никогда не узнать без курса”
👨‍🦱 ”Есть очень много свежей информации, которую сконцентрировано в едином источнике не получить”
👨‍🦱 “Это не с нуля совсем курс, и больше про правду разработки, разбавленную вопросами с собесов, а не про чистые знания.”

Отзывы целиком можно почитать тут

Для какого уровня курс?

Middle и выше

✔️ Есть рассрочка на 3 и 6 месяцев
✔️ Принимаются карты любых банков
✔️ Курс можно оплатить за счёт компании

Аналогов у курса нет. Вообще:)

С каждым потоком программа становится лучше, задания интереснее, а учёба приятнее. Если хотите разобраться с многопоточкой, и вам близок мой стиль изложения — записывайтесь, будет очень полезно!

http://fillthegaps.ru/mt
👍34🔥253
Что изменилось в этом потоке?

Курс — мой любимый пет-проект, который я развиваю уже третий год.

Казалось бы, уже 7 потоков прошло. Всем всё нравится — половину мест с обратной связью уже разобрали, отзывы отличные. Что ещё улучшать? А вот есть что:)

Практические задания

Практика — самая сильная часть курса. Теория осталась плюс-минус такой же с 2021 года, а практическая часть постоянно развивается. Тесты, написание кода, анализ реального кода, лабораторные работы — только так появляется уверенность при работе с многопоточкой.

Для этого потока добавила пару классных примеров из Spring core на разбор, отшлифовала формулировки тестов, написала несколько гайдов для самопроверки для тарифа без обратной связи

Предобучение

До прохождения курса многие вообще не трогали многопоточку. И для некоторых учеников нагрузка оказывается очень серьёзной.

Чтобы чуть снизить уровень стресса, учёба теперь делится на два шага:

1. Предобучение — в спокойном темпе изучить основы и потренироваться на простых примерах
2. Основной курс — закрепить основы и углубиться в детали

В итоге
▫️ Новички чуть больше работают с базой и основной курс зайдёт легче (я надеюсь)
▫️ Опытные ребята пропускают предобучение и не тратят время на лёгкие задачки

Подготовительный этап совсем небольшой, поэтому решила сделать его открытым. Если хотите подтянуть основы многопоточности — welcome

Налоговый вычет

Если вы платите налоги в России, то в следующем году можно подать заявление в налоговую и вернуть 13% стоимости курса!!!

Ученики февральского потока тоже могут оформить вычет! Как это сделать и какие документы нужны — написала на сайте в разделе "популярные вопросы".

https://fillthegaps.ru/mt
🔥38👍147
Последний день для ранних пташек

Сегодня последний день, когда можно вписаться на курс как early bird🦅

Курс строится вокруг java.util.concurrent — боевой лошадки каждого нагруженного сервиса. В деталях изучим все классы, концепты и практическое применение. Разберём огромное количество кейсов, лучших практик и возможных ошибок.

Ну и по мелочи — разберёмся с тестированием многопоточки, сравним разные подходы (реактивщина, Project Loom, корутины), подготовимся к собеседованиям. Всё шаг за шагом и с картинками:)

👨‍🦱 “Курс великолепный, не пожалел ни одного рубля, что потратил. Это уникальный курс в своем сегменте, особенно на русском рынке. Всем советую, на курсе вы найдете все ответы на интересующие вас вопросы. Более того из курса вы сможете узнать то, что просто нет в открытом доступе нигде, исключительно авторские наработки. Однозначно советую всем бэкэнд разработчикам, даже если вы не особо используете многопточку - это очень поможет вам в понимании многопоточного кода фреймворков и вообще сильно улучшит ваш кругозор. Советую брать с обратной связью - сильно увеличивает пользу от курса.”

Старт: 5 июня
Длительность: 9 недель

Оплата за счёт компании
Рассрочка на 3 или 6 месяцев
Налоговый вычет 13%

Завтра цена вырастет, присоединяйтесь сегодня!
http://fillthegaps.ru/mt
🔥9👍63
Зачем нужна конструкция <E extends Enum<E>> в определении Enum?
👍9
Self-referential generic, часть 1

Кто участвовал в декабрьском адвенте, точно помнит, что еnum компилируется в наследник класса Enum:

public enum Animal {WOLF, TIGER}

public class Animal extends Enum {
public static final Animal WOLF;
public static final Animal TIGER;
}
Подробнее об этом и енамах в целом можно почитать тут — раз, два и три.

В определении класса Enum используется конструкция, которая называется self-referential generic (или self-bound type, или recursive generic):

EnumᐸE extends EnumᐸEᐳᐳ

В этом посте расскажу, что это такое и зачем нужно.

Чтобы понять, какая проблема решается, представим, что этой конструкции нет. И определение енама выглядит так:

public abstract class MyEnum implements ComparableᐸMyEnumᐳ

Пользователь определяет enum Animal и enum Converter. Компилятор превращает это в классы

Animal extends MyEnum
Converter extends MyEnum

Каждый класс должен реализовать интерфейс ComparableᐸMyEnumᐳ и метод compareTo. Чтобы не сравнивать животных и конвертеры, придётся использовать instanceof:

public final int compareTo(MyEnum o) {
if (o instanceOf Animal other) {
// сравниваем зверюшек
// return ...
}
throw IllegalArgumentException();
}

В самом instanceOf нет ничего плохого. Тем более этот код генерируется при компиляции и остаётся за кадром.

Есть более важный момент. Пользователь может спокойно сравнить животное и конвертер, ошибка возникнет только в рантайме. Это выглядит странно, ведь enum Animal и enum Converter никак не связаны между собой.

Здесь дженерик выходит на сцену:

public abstract class EnumᐸE extends EnumᐸEᐳᐳ implements ComparableᐸEᐳ

🔸 Добавляем параметр E, совместимый с классом Enum
🔸 Используем E в интерфейсе Comparable
🔸 Компилируем enum Animal в
public class Animal extends EnumᐸAnimalᐳ
🔸 Теперь Comparable использует тип Animal, и метод compareTo станет таким:
public int compareTo(Animal o)

Убрали instanceOf, код стал меньше и быстрее
При компиляции происходит проверка типов:

Animal zebra = Animal.ZEBRA;
Converter csv = Converter.CSV;
zebra.compareTo(csv); // не скомпилируется!

Self-referential generic позволяет использовать дочерний тип в интерфейсах и методах родителя. Для некоторых кейсов этот приём здорово упрощает код и снижает количество ошибок. В следующем посте покажу ещё один пример использования.

Ответ на вопрос перед постом: self-referential generic помогает ограничить сравнение разных enum между собой.
🔥91👍2913
Self-referential generic, часть 2

В прошлом посте мы выяснили, зачем self-referential generic нужен в Enum: для использования дочернего типа при реализации интерфейса родителя. Это довольно экзотичный кейс. Сегодня покажу более практичный пример, как дженерики облегчили работу с иерархией и неизменяемыми переменными.

Дано: класс Delivery с информацией о доставке. Метод cancelled делает заказ недействительным. У класса есть наследник FastDelivery, в котором дополнительно хранится ID курьера:

class Delivery {
final long id;
final boolean isActive;

public Delivery(long id, boolean isActive) {…}

public Delivery cancelled() {
return new Delivery(this.id, false);
}
}

class FastDelivery extends Delivery {
private final long courierId;

public FastDelivery(…) {…}

public long getCourierId() {
return courierId;
}
}

Проблема: метод cancelled возвращает объект типа Delivery, и мы теряем информацию о курьере:

FastDelivery fast = new FastDelivery(…);
Delivery cancelled = fast.cancelled();

long id = fast.getCourierId();

В такой ситуации помогут self-referential generic и небольшой обходной манёвр:

🔸 Добавляем параметр в родителя
public class DeliveryᐸT extends DeliveryᐸTᐳᐳ

🔸 Создаём метод create, который возвращает нужный экземпляр
protected T create(long id, boolean isActive) {
return (T) new Delivery(id, isActive);
}

🔸 Используем этот метод в cancelled
public T cancelled() {
return create(this.id, false);
}

🔸 Определяем параметр в наследнике
public class FastDelivery extends DeliveryᐸFastDeliveryᐳ

🔸 Переопределяем метод create в наследнике
protected FastDelivery create(long id, boolean isActive) {
return new FastDelivery(this.id, this.isActive, courierId);
}

Всё! Теперь информация не теряется:
FastDelivery fast = new FastDelivery(…);
FastDelivery cancelled = fast.cancelled();

long id = cancelled.getCourierId();

Здесь используется комбо двух приёмов:

🔹 Метод create и его переопределение позволяют использовать поля, доступные в наследнике и вернуть нужный объект
🔹 Self-referential generic помогает вернуть нужный тип в методе cancelled

Готовый код доступен здесь

Резюме

Рассмотрите использование self-referential generic, когда

▫️ У вас есть иерархия
▫️ Родительский тип упоминается в аргументах или возвращаемом значении

Дополнительная типизация снизит количество кода, вытащит ошибки на этап компиляции и для некоторых случаев окажется очень изящным решением
🔥85👍2812👎2
Понятия в БД, часть 1. ACID

Когда я была беспечным джуниором, то представляла базу данных как всемогущий цилиндр. Записываешь туда что-то, а потом читаешь.

Но база данных — это обычная программа со своими сложностями и ограничениями. В следующих постах расскажу о принципах работы БД. Где на неё можно положиться, а где надо решать проблемы самому.

Есть популярный вопрос на собесах про свойства БД. Предполагается, что кандидат назовёт аббревиатуру ACID и cкажет 4 главных слова — Atomicity, Consistency, Isolation, Durability Сегодня и поговорим об этих прекрасных буквах.

Когда БД обозначена как ACID-compliant, ожидается:

🔸 A — Atomicity

Можно объединить несколько операций в одну транзакцию. Если произойдёт ошибка, уже сделанные операции в группе отменятся.

Благодаря этому можно не держать в коде несколько версий данных "на всякий случай" и восстанавливаться после ошибок гораздо проще

🔸 C — Consistency

Ограничения в БД (constraints) соблюдаются всегда и везде. Если две транзакции захотят записать одинаковые значения в UNIQUE колонку, одна транзакция завершится ошибкой.

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

🔸 I — Isolation

Каждая транзакция выполняется так, как будто других транзакций не существует.

На практике внутри БД происходит лютая многопоточка, с одними структурами одновременно работают десятки и сотни транзакций.

Единственный способ надёжно изолировать транзакции друг от друга — запускать их последовательно. Это медленно, поэтому у БД есть менее строгие уровни изоляции. С ними база работает быстрее, но возможны аномалии в данных.

Важный момент: constaints для одной строки (CHECK, UNIQUE и тд) железно выполняются. Аномалии встречаются в транзакциях, где изменения состоят из нескольких шагов или меняются несколько строк.

Чтобы перевести 100 рублей с одного аккаунта на другой, нужно снять с первого аккаунта 100 рублей и добавить их на баланс второго. Целостность данных в середине процесса ненадолго нарушится. От уровня изоляции зависит, заметят ли это несоответствие другие транзакции.

Подробно поговорим об этом в следующем посте!

🔸 D — Durability

Если данные записаны в БД, они не потеряются. Здесь два пути:
▫️ Запись на носитель, например, жёсткий диск или SSD
▫️ Отправка копий на другие сервера

100% надёжности на тысячи лет не будет, но сохранность данных — наименее проблемный пункт из всех остальных

Резюме

ACID не даёт гарантий на уровне "записал и забыл". Целостность данных лежит на бизнес-логике, а в коде учитываются возможные ошибки неполной изоляции.

Поэтому сегодня ACID чаще встречается не в техническом описании, а в маркетинговых текстах рядом с цифровой трансформацией и дизайн-мышлением. Многие БД не берут на себя грех называться ACID-compliant, а используют более мягкую аббревиатуру BASE. Её тоже обсудим чуть позже
🔥84👍3714👎6
Ваня и Даша — брат и сестра, у каждого на счету 500р. Ваня перевёл Даше 100р. Банк делает это в рамках одной транзакции. Мама смотрит в приложении балансы и видит 400 и 500 рублей. Обновляет данные и видит 400 и 600. На каком уровне изоляции это возможно?
Anonymous Poll
6%
Ни на одном
66%
READ_UNCOMMITED
14%
READ_COMMITED
6%
REPETABLE_READ
3%
SERIALIZABLE
4%
На всех
👍22🔥82👎1
Понятия в БД, часть 2. Уровни изоляции

Изоляция в ACID говорит: транзакция должна выполняется так, как будто других транзакций нет.
Единственный надёжный способ добиться этого — запускать транзакции последовательно. Это медленно, поэтому БД поддерживает менее строгие модели изоляции. База работает быстрее, но возможны аномалии данных.

В этом посте углубимся в детали: что за аномалии, что за уровни изоляции, и какие проблемы они решают.

Проблемы давно известны — dirty reads, write skews и тд. Чем больше проблем решает БД, тем больше кода нужно выполнить, и тем медленнее она работает. Уровни изоляции позволяют найти баланс между скоростью и корректностью.

В SQL стандарте их 4:
▫️ READ_UNCOMMITED
▫️ READ_COMMITED
▫️ REPEATABLE_READ
▫️ SERIALIZABLE

Каждый уровень гарантирует решение чёткого списка проблем. Остальные решаются либо в коде сервиса, либо никак (если проблема не актуальна).

В стандарте SQL три основные проблемы:

🔸 Dirty reads — грязные чтения

Транзакция 1 обновляет поле Х. Другие транзакции видят новое значения Х до того, как транзакция 1 завершится.

В вопросе с переводом денег как раз возникла такая ситуация. Транзакция перевода ещё не завершилась, а другая транзакция прочитала промежуточные значения.

Проблема возникает только на уровне READ_UNCOMMITED.

🔸 Nonrepeatable reads — неповторяющиеся чтения

Транзакция 2 читает поле X и работает с ним. В это время транзакция 3 обновляет поле Х. В итоге транзакция 2 работает с устаревшим значением.

Более формально, "неповторяющееся чтение" означает, что чтение одного поля в начале и конце транзакции даёт разные результаты. Но редко кто читает одно поле дважды, на практике получается либо бесполезная транзакция с устаревшими данными, либо несогласованные данные внутри транзакции.

Проблема остро проявляется для долгих запросов, например, бэкапов или аналитики. Решается на уровне REPEATABLE_READ и выше.

🔸 Фантомные чтения

Транзакция 3 проверяет условие по большому количеству записей. Транзакция 4 меняет выборку, например, добавляет новую запись. Если условие в транзакции 3 перестанет выполнятся, транзакция 3 этого не заметит.

Важно, что условие касается не одного поля, а многих. В этом разница с неповторяющимся чтением. Там меняется одно конкретное поле, а в фантомном чтении меняется вся выборка, по которому проверяется условие.

Проблема решается на уровне SERIALIZABLE.

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

Каждая БД сама решает, какие проблемы и вариации на каких уровнях решать. У MS SQL 5 уровней изоляции, у Oracle 3. Многие NoSQL базы не поддерживают транзакции, поэтому для них указывать тип изоляции бессмысленно. В универсальных адаптерах типа JDBC, Hibernate и Spring Data уровней столько, сколько в стандарте — 4.

Ещё одна проблема, которой нет в SQL стандарте, но которая встречается на практике:

🔸 Потерянный апдейт

Транзакции работают с одними данными и не учитывают друг друга.

Пример: транзакция 5 и транзакция 6 одновременно прочитали значение счётчика. Каждая транзакция прибавила к значению единицу и обновила поле счётчика. Вначале они прочитали одно значение, и получается, что один инкремент потерялся.

Проблема решается не только уровнями изоляции, но и SQL конструкциями:

🔹 Атомарный апдейт:
UPDATE test SET x=x-1 where id=1;

🔹 Блокировка строки:
SELECT * FROM test WHERE id = 1 FOR UPDATE;

Итого. Как учитывать внутрянку БД в написании кода:

⭐️ Выбирать уровень изоляции с учётом вероятности и критичности проблем
⭐️ Уточнить в документации БД, какие проблемы решает выбранный уровень
⭐️ Писать код с учётом возможных аномалий
⭐️ Помнить о потерянных апдейтах
👍85🔥3114