Asynchroniczne zapytania do bazy danych w kontrolerze

Asynchroniczne zapytania do bazy danych w kontrolerze
EL
  • Rejestracja:około 13 lat
  • Ostatnio:3 miesiące
0

Mam endpointy w kontrolerze napisane w Springu które zwracają pewne dane z bazy danych (wcześniej trzeba je trochę obrobić).
Niektóre selecty po stronie bazy wykonują się za długo (~2 sekundy) a w tym czasie wątek jest zablokowany. Kontroler co prawda musi czekać na dane z bazy więc i tak zwróci je po ~2 sekundach, ale można by wykonywać inne operacje zanim baza nie zwróci wyników (np. obrabianie danych które przyszły z kolejnego requesta do wykonania następnego selecta).
Jest to dla mnie ważne bo zapytań jest na tyle dużo że muszą być kolejkowane przez ograniczoną pulę wątków.
Zacząłem eksperymentować z ComplatableFuture ale koniec końców nie działa to tak jak bym oczekiwał więc może moje rozumowanie jest złe albo wykonana implementacja. Zacznijmy od tego pierwszego.
Oczekuję:

  1. Kontroler łapie zapytanie
  2. Obrabia dane
  3. Robi select'a do bazy (~2-3 sekundy)
  4. W czasie trwania zapytania do bazy kontroler odbiera kolejny request, obrabia dane i robi zapytanie do bazy.
  5. Jeśli poprzednie zapytanie do db już się zakończyło to zwróć wyniki, jeśli nie to przyjmij kolejny request.

Natomiast u mnie tak nie działa. Kontroler może wykonać kilka operacji asynchronicznie w ramach tego samego requesta w czasie kiedy baza pracuje nad zwróceniem jakichś wyników, natomiast nie odbierze kolejnego zapytania dopóki pierwsze w całości się nie zakończyło.
Pytanie więc czy jest to w ogóle możliwe? Aktualnie zrobiłem to na Springu ale mogę to zastąpić czymkolwiek, byle podstawą była Java.

edytowany 2x, ostatnio: eL
0

Może przyda się Spring WebFlux (ReactiveApi)?

YA
  • Rejestracja:prawie 10 lat
  • Ostatnio:około 14 godzin
  • Postów:2367
0

O ile kojarzę są 2 modele:

  • polling (klient co jakiś czas, być może krótki, odpytuje, czy jest już odpowiedź na mój request appX_UID_XYZ i dostaje responsa z payloadem)
  • notification / callback - przy requstowaniu dodajesz parametr, gdzie ma być wysłana notyfikacja o zakończeniu przetwarzania requestu "call me back at http://foo/bar/notify/request/appX_UID_XYZ") i tam możesz dorzucać jakiś payload

A komu niby ma być zwrócona odpowiedź? I w jaki sposób? Dla GUI to może wyglądać inaczej (websockets?) i inaczej dla aplikacji backendowych.

edytowany 1x, ostatnio: yarel
Patryk27
Z tego co rozumiem, @eL pyta o asynchroniczność wewnątrz serwera (na zasadzie async + await, Futureów, Promiseów czy Tasków znanych z innych języków), nie na zewnątrz (w odniesieniu do klienta).
EL
  • Rejestracja:około 13 lat
  • Ostatnio:3 miesiące
0

Trochę się nie zrozumieliśmy.
Klienci pukają do moich endpointów o konkretne dane. Kontroler łapie request, przygotowuje zapytanie do bazy i pyta bazę o dane X. Operacja selecta trwa czasami 2-3 sekundy. Jak się wykona ta operacja to dane z bazy muszą być jeszcze przygotowane, obrobione itp po czym można je zwrócić.
Z punktu widzenia klienta całość będzie synchroniczna bo wszystkie te operacje muszą się wykonać aby otrzymał wyniki natomiast wątki po stronie serwera kiedy robione jest zapytanie do bazy po dane stoją i nic nie robią a mogłyby w tym czasie na warsztat brać kolejny request który trzeba zwalidować lub też przetworzyć wyniki z bazy.

@Patryk27 dodał komentarz który krótko to podsumowuje.

NO
  • Rejestracja:ponad 7 lat
  • Ostatnio:prawie 4 lata
  • Postów:35
0

Zacznijmy od tego że żeby osiągnąć to co napisałeś w Springu nie musisz wykorzystać CompletableFuture.

Dla każdego requesta domyślnie Spring sam zrobi nowy wątek i zapytania będzie wykonywał równolegle. Jeżeli u ciebie tak nie jest to musiałeś coś pokombinować, np. zrobić customowy config, transakcje na poziomie SERIALIZABLE, manualne lockowanie wątków czy jeszcze coś innego.

Żeby ci dalej pomóc musiałbyś dać minimalny weryfikowalny przykład. (https://stackoverflow.com/help/mcve) i dał więcej informacji m.in. z jakiej bazy korzystasz.

edytowany 1x, ostatnio: Noozen
YA
  • Rejestracja:prawie 10 lat
  • Ostatnio:około 14 godzin
  • Postów:2367
0

Hmm, wewnątrz serwera to nie wystarczy zwykłe ExecutorService z dedykowaną pulą wątków i parę kolejek (w zależności od tego jak bardzo złożony jest flow przetwarzający)?

Jak dla mnie requesty nie biorą się znikąd i trzeba w końcu odpowiedź wysłać do klienta i albo synchronicznie (tak jak masz teraz, że wątek odbierający request jest przyblokowany, aż obliczenia będą gotowe) albo asynchronicznie (callback do klienta). 2-3 sek dla CPU to dużo, więc może być tak, że nie będziesz miał co przetwarzać, bo wąskie gardło będzie na bazie :-)

EL
  • Rejestracja:około 13 lat
  • Ostatnio:3 miesiące
0
Noozen napisał(a):

Zacznijmy od tego że żeby osiągnąć to co napisałeś w Springu nie musisz wykorzystać CompletableFuture.

Dla każdego requesta domyślnie Spring sam zrobi nowy wątek i zapytania będzie wykonywał równolegle.

Jak najbardziej się z tym zgadzam. Domyślna pula wątków dla Tomcata uruchomionego jako internal w Spring Boot wynosi 200. Mogę to edytować ale problem jest w tym że mi to nie wystarcza. Zdarza się że requestów jest kilka tysięcy.
Kod produkcyjny póki co nie jest istotny bo tam wszystko jest synchronicznie zrobione przez co serwer nie wyrabia i chciałbym to zrobić od nowa. Odpowiedzi do klientów nie mogę zwracać asynchronicznie, natomiast w momencie kiedy wątek nic nie robi bo zrobił synchroniczne zapytanie do DB, chciałbym je zamienić na async i w tym czasie przetwarzać kolejny request z kolejki.

YA
No ale ten wątek co teraz przetwarza jest zablokowany, bo jest synchroniczna komunikacja między klientem a Tomcatem, czyż nie? Zostanie zwolniony jak odeślesz response. Baza odpowiada 2-3 sekundy, jak wpuścisz więcej wątków (nie 200, a np. 400), to będzie odpowiadała szybciej przy większym obciążeniu? ;)
YA
Ja bym spróbował zmierzyć się z tematem od strony tuningu bazy danych, bo 2-3 sekundy to nie brzmi dobrze.
QB
W sumie racja... jakoś automatycznie założyłem, że ta operacja na bazie musi być skomplikowana i autor wie, że nie da się jej zoptymalizować
QB
  • Rejestracja:ponad 9 lat
  • Ostatnio:3 dni
  • Lokalizacja:Lublin
  • Postów:170
1

W takim wypadku zainteresuj się WebFlux. Albo podnieś pulę do paru tys (nie rób tego :D)

EDIT: WebFlux oznaczałby również zmianę w kodzie klientów, bo odpowiedzi byłyby już event streamem

Jeśli nie chcesz zmieniać API clientów i zwiększać puli połączeń na tomcacie, pomyślałbym o postawieniu jakiegoś API gatewaya, który robiłby delay jeśli byłoby dużo przychodzących requestów.
Np: Nginx https://www.nginx.com/blog/rate-limiting-nginx/, lub Zuul (nie znam)

edytowany 7x, ostatnio: qbns
EL
  • Rejestracja:około 13 lat
  • Ostatnio:3 miesiące
0

@yarel: odpowiem tutaj w nawiązaniu do komentarza bardziej szczegółówo.

Komunikacja między klientem a serwerem jest synchroniczna i tego póki co nie ruszam bo nie mam możliwości modyfikować klienta. Ruch jest już rozłożony na kilka maszyn ale to nie wystarcza. Zależy mi tylko żeby szybciej rozładować kolejkę requestów. Dla uproszczenia weźmy taki przykład.
Pula wątków dla tomcata wynosi 10. W jednym czasie dostaję 100 requestów o jakieś dane z bazy. 10 requestów będzie obsługiwanych natomiast 90 pozostałych będzie czekać. Aktualnie wygląda to tak że 10 requestów będzie walidowanych, sklejane będą jakieś tam dane i robiony są zapytania do bazy które czasami trwają np. 2 sekundy a wątek w tym czasie czeka na odpowiedź z DB. Odebrał dane to je zwraca do klienta i kolejny request jest przetwarzany.
Koniec końców obsłużenie wszystkiego zajmuje jakieś 25 sekund z czego większość czasu te wątki czekają na odpowiedź z bazy przez to że są to zapytania synchroniczne.
Chciałbym więc to poprawić. Jeśli dla analogicznej sytuacji wpadnie taka sama liczba zapytań to 90 z nich i tak będzie w kolejce natomiast jeśli 10 aktualnie pracujących wątków wyśle asynchroniczne zapytanie do bazy to mogłoby brać z kolejki kolejny request i już przygotowywać zapytanie do bazy danych. Nie przyspieszy to jakoś diametralnie bo nadal zapytania do bazy będą trwały 2-3 sekundy, natomiast wątki które teraz po wykonaniu takiego synchronicznego zapytania nie robią nic, mogłyby w tym czasie obsługiwać kolejne zapytania. Tym bardziej że wiele zapytań zajmuje znacznie mniej czasu (np. 20-30ms) a i tak są zblokowane przez to że w kolejce wcześniej trafiły sie zapytania trwające znacznie dłużej.

Ps. Podany przeze mnie czas 2-3 sekund na zapytanie bazodanowe to bardziej takie zwrócenie uwagi na to że część zapytań wykonuje się dużo dłużej niż pozostałe. Mam konkretne metryki ale też nie chcę tutaj o nich pisac więc przyjąłem że jest to po prostu długo trwające zapytanie.

edytowany 1x, ostatnio: eL
QB
  • Rejestracja:ponad 9 lat
  • Ostatnio:3 dni
  • Lokalizacja:Lublin
  • Postów:170
0

Hmm, jeśli umiesz rozróżnić na podstawie requestu, które zapytania będą ciężkie, a które nie, mógłbyś utworzyć osobne Tomcat connector pools. Jedna byłaby do obsługiwania szybkich zapytań, druga - do wolnych. Wiąże się to z tym, że serwis wystawiony byłby na dwóch portach, ale jeśli pomiędzy klientem a serwerem masz jakieś proxy, to możesz wdrożyć taką zmianę bez zauważalnej różnicy dla klienta.

edytowany 1x, ostatnio: qbns
NO
  • Rejestracja:ponad 7 lat
  • Ostatnio:prawie 4 lata
  • Postów:35
1

Rozumiem że chciałbyś zrobić nowy wątek przed zapytaniem SQL i robić coś sobie w tle. Niestety w praktyce nic tym nie osiągniesz. Dopóki SQL się nie skonczy to trzymasz połączenie na Tomcacie, innymi słowy nigdy nie będziesz ptrzetwarzał więcej niz te 200 połączeń w puli naraz.

Jeżeli chcesz zwolnić wątek zanim dasz odpowiedź do klienta, to tak jak napisał qbns, musisz wykorzystać np. WebFluxa który nie bazuje na serwletach.

Oprócz tego pozostaje ci: optymalizacja SQL, zwiększenie puli wątków, skalowanie wszerz i ew. żonglowanie połaczeniami co też już opisał qbns.

edytowany 1x, ostatnio: Noozen
YA
  • Rejestracja:prawie 10 lat
  • Ostatnio:około 14 godzin
  • Postów:2367
0

Prędkość wejściowa: 100 requestów / sekundę
Czas przetwarzania E2E: 5 sekundy na request => 1 wątek przetwarza 0,2 req /sekundę

Chcesz przetworzyć 100 requestów przetwarzasz w 100 / 0,2 sekund => 500 sekund.

Chcesz skrócić czas oczekiwania na przetworzenie -> więcej otwartych okienek
10 okienek -> 50 sekund czekania
20 okienek -> 25 sekund czekania w kolejce
50 okienek -> 10 sekund czekania w kolejce
200 okienek -> 2.5 sekundy czekania w kolejce

Dlaczego przy 200 wątkach nie masz 2.5 sekundy na obsługę całego requestu? (Nie masz, bo piszesz, że czas obsługi na bazie 2-3 sek.)
Dlaczego masz 2-3 sek na odpowiedź z bazy?

edytowany 1x, ostatnio: yarel
EL
Jeśli chodzi o czasy to tak jak pisałem nie chcę po prostu rozpisywać się z konkretnymi metrykami. Część zapytań trwa krótko, np. 20ms natomiast pisząc 2-3 sekundy miałem na myśli zapytania które trwają po prostu za długo (dużo dłużej niż pozostałe które czekają na obsłużenie przez ograniczoną pulę wątków). W praktyce te czasy nie są aż tak wysokie, natomiast zajmują więcej czasu od pozostałych i przez to że są synchroniczne blokują wątki które mogłyby obsługiwać kolejne zapytania w tym czasie.
YA
Skoro tak, to pewnie jesteś w stanie z logów zobaczyć ile requestów wpada i czy dotyczą "ciężkich" zapytań, czy "lekkich" i zwiększyć pulę wątków na Tomcacie? Nie wiem, 200 wątków na obsługę ciężkich zapytań i 50 na obsługę lekkich?
0

moźe lepszym rozwiązaniem było by skeszowanie wyniku zapytania i filtrowanie dla kazdego requsta ?

EL
  • Rejestracja:około 13 lat
  • Ostatnio:3 miesiące
0

@yarel: Pomysł z podziałem na krótkie i długie zapytania jest jak najbardziej ciekawy i zaraz to sobie przemyślę a tym czasem podrzucę mój pomysł który zaimplementowałem. Znalazłem taki artykuł:
https://nickebbitt.github.io/blog/2017/03/22/async-web-service-using-completable-future
Muszę to testować na jakichś mniejszych obciążeniach które sam jestem wstanie wygenerować więc ustawiłem sobie pulę wątków dla Tomcata w Spring boot na 2 wątki.

Kopiuj
server:
  port: 9095
  tomcat:
    max-threads: 2

Zaimplementowałem najpierw zwykły kontroler. Kontroler dostaje jakieś ID i zwraca customera z bazy ale żeby móc testować to podczas walidacji wątek jest usypiany na 1 sekundę a podczas pobierania z bazy danych na 3 sekundy. Cała operacja więc od przyjęcia ID do zwrócenia customera trwa 4 sekundy. Jeśli przyjdą 2 requesty to mając pulę 2 wątków obsłużone zostaną oba jednocześnie w ciągu 4 sekund. Jeśli trafią się 4 zapytania to pierwsze 2 będą zwrócone po 4 sekundach a 2 następne będą czekały w kolejce (najpierw czekają na zakończenie poprzednich 4 sekundy + 4 sekundy podczas wykonywania operacji = łącznie 8s). Itd itd.

Kopiuj
@GetMapping("/customer2/{id}")
    ResponseEntity<Customer> getById2(@PathVariable long id) throws InterruptedException {
        logger.info("Request received");

        dataValidation();
        Customer customer = fetchFromDbSync(id);

        logger.info("Servlet thread released");

        return ResponseEntity.ok(customer);
    }

    private void dataValidation() throws InterruptedException {
        logger.info("Start data validation");
        //some important operation
        TimeUnit.SECONDS.sleep(1);
        logger.info("Completed data validation");
    }

    private Customer fetchFromDbSync(long id) throws InterruptedException {
        logger.info("Start processing request");

        //imitation of long db operation
        TimeUnit.SECONDS.sleep(3);
        Customer customer = repository.findById(id);

        logger.info("Completed processing request");
        return customer;
    }

Zrobiłem test Apache Bench:

Kopiuj
ab -n 10 -c 10 http://localhost:9095/customer2/2

10 zapytań na 10 wątkach więc 8 będzie wrzuconych do kolejki.
Wynik jest taki:
title
Pierwszy request jest najkrótszy - 4 sekundy a każdy kolejny trwa dłużej ponieważ wszystko jest synchroniczne.

Natomiast jak zmodyfikowałem metodę kontrolera:

Kopiuj
  @GetMapping("/async/customer2/{id}")
    CompletableFuture<ResponseEntity<Customer>> asyncGetById2(@PathVariable long id) throws InterruptedException {
        logger.info("Request received");

        dataValidation();

        CompletableFuture<Customer> completableFuture =
                CompletableFuture.supplyAsync(() -> fetchFromDbSync(id));

        logger.info("Servlet thread released");

        return completableFuture
                .thenApplyAsync(ResponseEntity::ok);
    }

To wynik jest znacznie lepszy:
title
Najkrótszy request zajął 5 sekund (pewnie dlatego że jak metoda fetchFromDbSync zakończyła działanie to wynik nie mógł być zwrócony ponieważ wszystkie wątki były zajęte) ale za to łączny czas wykonania wszystkich requestów spadł do 8 sekund (poprzednio było 20 s) a średni czas wyniósł 6 sekund na request (wcześniej było 12s).

Jak się przejrzy logi:

2018-11-13 14:08:16.674 INFO 22188 --- [nio-9095-exec-1] c.t.customer.AsyncCustomerController : Request received
2018-11-13 14:08:16.674 INFO 22188 --- [nio-9095-exec-1] c.t.customer.AsyncCustomerController : Start data validation
2018-11-13 14:08:16.674 INFO 22188 --- [nio-9095-exec-2] c.t.customer.AsyncCustomerController : Request received
2018-11-13 14:08:16.674 INFO 22188 --- [nio-9095-exec-2] c.t.customer.AsyncCustomerController : Start data validation
2018-11-13 14:08:17.675 INFO 22188 --- [nio-9095-exec-2] c.t.customer.AsyncCustomerController : Completed data validation
2018-11-13 14:08:17.675 INFO 22188 --- [nio-9095-exec-1] c.t.customer.AsyncCustomerController : Completed data validation
2018-11-13 14:08:17.675 INFO 22188 --- [nio-9095-exec-2] c.t.customer.AsyncCustomerController : Servlet thread released
2018-11-13 14:08:17.675 INFO 22188 --- [nPool-worker-13] c.t.customer.AsyncCustomerController : Start processing request
2018-11-13 14:08:17.675 INFO 22188 --- [nio-9095-exec-1] c.t.customer.AsyncCustomerController : Servlet thread released
2018-11-13 14:08:17.675 INFO 22188 --- [onPool-worker-8] c.t.customer.AsyncCustomerController : Start processing request
2018-11-13 14:08:17.677 INFO 22188 --- [nio-9095-exec-2] c.t.customer.AsyncCustomerController : Request received
2018-11-13 14:08:17.677 INFO 22188 --- [nio-9095-exec-1] c.t.customer.AsyncCustomerController : Request received
2018-11-13 14:08:17.677 INFO 22188 --- [nio-9095-exec-2] c.t.customer.AsyncCustomerController : Start data validation
2018-11-13 14:08:17.677 INFO 22188 --- [nio-9095-exec-1] c.t.customer.AsyncCustomerController : Start data validation
...

To wygląda na to że wszystkie operacje wykonywane są na wątku 1 i 2 a zapytanie do bazy danych jest asynchroniczne i wątek nie czeka na odpowiedź z bazy tylko zaczyna obsługiwać kolejne zapytanie (Request received).
Nie wiem czy mój tok rozumowania jest poprawny ale wydaje mi się że działa tak jak bym chciał. Pytanie tylko czy w takim podejściu przy dużej liczbie zapytań ta asynchroniczność nie wyjdzie bokiem i wszystko się nie rozjedzie?

YA
Infrastrukturę do testów masz :-) Możesz teraz sprawdzać jak wygląda różnica, w przypadku, gdy zmieniasz ilość wątków i concurrency. Ciekawie wyglądałby taki excelik, w którym miałbyś wymiary: N (pool size) i C (concurrency) i jako wartość w komórce N/C stosunek czasu wykonania w przypadku 1 : do czasu wykonania w przypadku 2. Później takie excelowe pokolorowanie komórek względem wartości (duże wartości na zielono - bo duża różnica między rozwiązaniami, a małe wartości na czerwono bo niewielki zysk) pokazałoby czy ta różnica rośnie w określonym kierunku, czy załamuje się.
Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
0
  1. Takie completable future jakie odpalasz używa common fork join pool i ma n-1 wątków, gdzie n to liczba rdzeni które masz. Myśle ze jednak chciałeś tam wrzucić custom pool jakiś? Bo teraz to trochę bez sensu benchmark.
  2. Chcesz mieć ciastko i zjeść ciastko, a tak się nie da. Jak chcesz low latency to niestety będzie trzeba zrezygnować z synchronicznego blokowania odpowiedzi.

"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)