Intervjuer iz Kuaishou: Kako dizajnirati sistem komentara?
Ovo je pitanje scenarija, nekad se susreće na razgovoru, zato Autor vam svima priča, glavna ideja članka dolazi od Autorovog dobrog prijatelja Drug Nikole: Ideje dizajna sistema komentara
Bilo da čitate članke sa javnih naloga ili gledate kratke video snimke, sada postoji funkcija komentara, i ovi komentari u osnovi podržavaju "zgradu u zgradi", to jest, ispod članka postoje komentari, ispod komentara postoje odgovori, ispod odgovora postoje novi odgovori, odgovori se mogu nastaviti odgovarati...

Ako količina podataka nije velika, ovu tabelu je u stvari vrlo lako dizajnirati, jedna tabela može rešiti problem, slično strukturi tabele provincija, gradova, okruga:

Izjava kreiranja tabele:
CREATE TABLE administrative_divisions (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
parent_id INT DEFAULT NULL,
level TINYINT NOT NULL,
country_code VARCHAR(10),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_parent_id (parent_id),
INDEX idx_level (level)
);Za ovakvu strukturu tabele, ako količina podataka nije velika, nema problema. Tehnički grupa trenutno koristi ovu strukturu tabele.

Ako je količina podataka velika, performanse upita i održavanje će postati problem. Sledeće, hajmo da diskutujemo kako bi bilo bolje dizajnirati ovu tabelu komentara.
1. Dizajn tabele
Bilo da se radi o člancima ili kratkim video snimcima, broj komentara je često vrlo velik, stoga sistem pri prikazu obično prvo sklapa komentare, prikazuje samo ukupan broj, tek kada korisnik klikne da se proširi će se prikazati, uz primer WeChat javnog naloga, obično je ovako:
Prvo ispod članka će se prikazati ukupan broj komentara:

Nakon otvaranja sekcije komentara, ovde postoje dve vredne pažnje informacije:
- Iznad sekcije komentara će se prikazati ukupan broj komentara
- Odgovori na komentare se neće odjednom prikazati, ali će se prikazati koliko odgovora ima, korisnik mora kliknuti da bi video sadržaj odgovora.

Treba obratiti pažnju na to, ako je broj odgovora posebno velik, neće se odjednom sve prikazati.
Ovakav dizajn je razuman, verujem da svi nemaju prigovora, razlog što se odgovori na komentare ne prikazuju odjednom je s jedne strane razmatranje performansi, s druge strane razmatranje korisničkog iskustva. Neki komentari imaju posebno mnogo odgovora, ako se sve odjednom prikaže, uticaće na pregledavanje komentara prvog nivoa.
Zatimo pogledajmo kako dizajnirati tabelu za ovakvu funkciju komentara.
Da bismo zadovoljili ove zahteve, dizajniraćemo tri tabele: Articles (tabela članaka), Comments (tabela indeksa komentara) i CommentsContent (tabela sadržaja komentara). Tabela članaka će beležiti broj korenih komentara članka, tabela indeksa komentara će beležiti hijerarhiju komentara, broj odgovora i broj lajkova, tabela sadržaja komentara će čuvati stvarni sadržaj komentara.
Ovaj način dizajna poboljšava performanse upita odvajanjem čuvanja sadržaja komentara i indeksa komentara, ujedno olakšava održavanje podataka, i kroz tabelu članaka beleženje broja korenih komentara članka, olakšava brzu statistiku.
Articles (tabela članaka)
| Naziv polja | Tip podataka | Opis |
|---|---|---|
| article_id | BIGINT AUTO_INCREMENT | Primarni ključ, auto-increment |
| title | VARCHAR(255) | Naslov članka |
| root_comment_count | INT DEFAULT 0 | Broj korenih komentara članka |
| created_at | DATETIME | Vreme kreiranja |
| updated_at | DATETIME | Vreme ažuriranja |
Comments (tabela indeksa komentara)
| Naziv polja | Tip podataka | Opis |
|---|---|---|
| comment_id | BIGINT AUTO_INCREMENT | Primarni ključ, auto-increment |
| article_id | BIGINT | ID članka |
| parent_id | BIGINT DEFAULT 0 | ID nadređenog komentara, za realizaciju višeniveljskih komentara, kod komentara najvišeg nivoa ovo polje je 0 |
| user_id | BIGINT | ID korisnika koji je napisao komentar |
| reply_count | INT DEFAULT 0 | Broj odgovora na ovaj komentar |
| like_count | INT DEFAULT 0 | Broj lajkova na ovaj komentar |
| created_at | DATETIME | Vreme kreiranja |
| updated_at | DATETIME | Vreme ažuriranja |
CommentsContent (tabela sadržaja komentara)
| Naziv polja | Tip podataka | Opis |
|---|---|---|
| comment_id | BIGINT | Strani ključ, vezuje se za comment_id u tabeli indeksa komentara |
| comment_text | TEXT | Sadržaj komentara |
| comment_time | DATETIME | Vreme komentara |
SQL izjave za kreiranje tabela
-- Kreiranje tabele članaka
CREATE TABLE articles (
article_id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
root_comment_count INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_root_comment_count (root_comment_count)
);
-- Kreiranje tabele indeksa komentara
CREATE TABLE comments (
comment_id BIGINT AUTO_INCREMENT PRIMARY KEY,
article_id BIGINT NOT NULL,
parent_id BIGINT DEFAULT 0,
user_id BIGINT NOT NULL,
reply_count INT DEFAULT 0,
like_count INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_article_id (article_id),
INDEX idx_parent_id (parent_id)
);
-- Kreiranje tabele sadržaja komentara
CREATE TABLE comments_content (
comment_id BIGINT PRIMARY KEY,
comment_text TEXT NOT NULL,
comment_time DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (comment_id) REFERENCES comments(comment_id)
);Trigeri
Trigeri se uglavnom koriste za ažuriranje broja korenih komentara članka i broja odgovora i lajkova komentara, međutim ovo nije obavezno, ovo se može obraditi kroz poslovnu logiku, ovde je uglavnom svima daje referencu.
DELIMITER //
CREATE TRIGGER after_comment_insert
AFTER INSERT ON comments
FOR EACH ROW
BEGIN
IF NEW.parent_id = 0 THEN
UPDATE articles SET root_comment_count = root_comment_count + 1 WHERE article_id = NEW.article_id;
END IF;
END; //
DELIMITER ;
CREATE TRIGGER after_comment_like_insert
AFTER INSERT ON comments
FOR EACH ROW
BEGIN
UPDATE comments SET like_count = like_count + 1 WHERE comment_id = NEW.comment_id;
END; //
DELIMITER ;
CREATE TRIGGER after_comment_reply_insert
AFTER INSERT ON comments
FOR EACH ROW
BEGIN
UPDATE comments SET reply_count = reply_count + 1 WHERE comment_id = NEW.parent_id;
END; //
DELIMITER ;
CREATE TRIGGER after_comment_delete
BEFORE DELETE ON comments
FOR EACH ROW
BEGIN
IF OLD.parent_id = 0 THEN
UPDATE articles SET root_comment_count = root_comment_count - 1 WHERE article_id = OLD.article_id;
ELSE
UPDATE comments SET reply_count = reply_count - 1 WHERE comment_id = OLD.parent_id;
END IF;
END; //
DELIMITER;Kreiranje indeksa na poljima article_id i parent_id u tabeli Comments može poboljšati performanse upita komentara određenog članka i podkomentara.
Na osnovu gore navedenog dizajna, kada hoćemo da dobijemo ukupan broj komentara, ukupan broj odgovora, možemo direktno upitati, nije potrebno statistički računati, vrlo je pogodno.
2. Dodavanje komentara
Da bismo se suočili sa visokom konkurencijom, možemo kombinovati MQ za realizaciju dodavanja komentara.
- Prvo frontend šalje komentar na server.
- Server nakon prijema poruke komentara šalje je MQ na obradu.
- Potrošač konzumira poruku sa MQ i dodaje komentar u bazu podataka.
- Nakon završetka dodavanja, šalje poruku o uspešnom dodavanju klijentu.

Gore navedeni koraci moraju osigurati pouzdanost poruka MQ, detalji kako osigurati pouzdanost, Drug Nikola je u prethodnim člancima razgovarao sa svima (Četiri strategije za osiguravanje pouzdanosti slanja poruka RabbitMQ-a).
Druga tačka je ako je potrebna analiza sadržaja komentara, tada nakon završetka 4. koraka, ponovo konzumiranjem MQ poruka čuvaj komentar u ES; ili kroz alate kao Canal, sinhronizujte podatke iz tabele u ES.
Tehnički grupa kod dodavanja komentara je ovde.

Obaveštenje o objavljivanju komentara je ovde:

3. Pretraga komentara
Popularne komentare možete čuvati u kešu, kako biste izbegli svaki put upit baze podataka.
public class CommentService {
private static final String COMMENT_CACHE_KEY = "comment:article:";
@Autowired
private RedisTemplate<String, List<Comment>> redisTemplate;
@Autowired
private CommentRepository commentRepository;
public List<Comment> getCommentsByArticleId(Long articleId) {
String cacheKey = COMMENT_CACHE_KEY + articleId;
// Upit iz Redis keša
List<Comment> cachedComments = redisTemplate.opsForValue().get(cacheKey);
if (cachedComments != null) {
return cachedComments;
}
// Ako keš nema podataka, upit iz baze podataka
List<Comment> comments = commentRepository.findByArticleId(articleId);
// Čuvanje rezultata upita u keš, postavljanje vreme isteka
redisTemplate.opsForValue().set(cacheKey, comments, Duration.ofHours(1));
return comments;
}
}Druga aspekt je razdvajanje čitanja i pisanja. Podaci o komentarima su tipičan scenario više čitanja manje pisanja, stoga možemo sa nivoa baze podataka izvršiti razdvajanje čitanja i pisanja, da bismo poboljšali performanse.
Konfiguracija master-slave izvora podataka:
spring:
datasource:
master:
url: jdbc:mysql://localhost:3306/master_db
username: root
password: root
slave:
url: jdbc:mysql://localhost:3306/slave_db
username: root
password: rootRutiranje dinamičkog izvora podataka:
@Component
@Primary
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSource(String dataSource) {
CONTEXT_HOLDER.set(dataSource);
}
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
@Override
protected Object determineCurrentLookupKey() {
return CONTEXT_HOLDER.get();
}
}AOP realizacija prebacivanja dinamičkog izvora podataka:
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(com.example.annotation.ReadOnly)")
public void useSlaveDataSource() {
DynamicDataSource.setDataSource("slave");
}
@Before("@annotation(com.example.annotation.WriteOnly)")
public void useMasterDataSource() {
DynamicDataSource.setDataSource("master");
}
@After("@annotation(com.example.annotation.ReadOnly) || @annotation(com.example.annotation.WriteOnly)")
public void clearDataSource() {
DynamicDataSource.clearDataSource();
}
}Oznaka operacije anotacijom:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WriteOnly {}Priprema operacije pisanja:
@Service
public class CommentService {
@ReadOnly
public List<Comment> getComments(Long articleId) {
// Čitanje komentara iz slave baze
return commentRepository.findByArticleId(articleId);
}
@WriteOnly
public void addComment(Comment comment) {
// Pisanje u master bazu
commentRepository.save(comment);
}
}Dobro, to je uglavnom ideja, ako imate različito mišljenje, dobrodošli da ostavite komentar i diskutujte.
