I have a love-hate relationship with browser tabs. I need a lot of them to work, but once I pass the 30-tab mark, my browser bar becomes useless.
Google Chrome actually experimented with an auto-grouping feature a while back, but they removed it. I tried finding alternatives on the Chrome Web Store, but they all had the same problem: They were lazy.
Most existing extensions group tabs based on the domain, even they calling LLM to group it. If they see youtube.com, they dump it in a “YouTube” folder. This is useless for me. If I have 5 tabs open for “Lofi Music” and 5 tabs open for “Python Tutorials,” those shouldn’t be in the same group. One is Work, the other is Background Noise.
I realized that to actually organize tabs, the software needs to read the page, not just the URL. So I spent my free time building Group Tab AI.
How it actually works
I didn’t want to over-engineer this, but I needed it to be smart. When you click the button, the extension doesn’t just look at the link. It injects a script to grab the “context” of the page, the H1 title, the meta description, and a snippet of the body text.
It sends that data to an LLM (I set it up to work with either OpenAI or Gemini). Because it reads the content, it can tell that a GitHub page for a “React Library” is different from a GitHub page for “Tracking Issues.”
I’m using Google Gemini 2.0 Flash for this mostly, with the thinkingBudget set to 0. It’s fast enough that by the time I blink, the tabs are sorted.
I spent nights tweaking prompts to make it focus on tasks, not domains with extra context from the website contents along with careful prompt to let them reasoning and choose. For example, if you’re a dev, it might make groups like “Bug Hunting” or “API Docs.” Designers get “Mockups” or “Inspo.” It works for anyone,
students with class notes, marketers with campaigns.
The feature I actually wanted: It learns
This is the part I’m most proud of. I know AI isn’t perfect. It’s going to mess up. It might group a design blog under “Development” instead of “Inspiration.”
Usually, with AI tools, you just have to live with the bad output. But I built a Learning System into this.
If the AI groups something wrong, I manually move the tab to the right group.
The extension records that move.
After I’ve corrected it a few times, I can click a button to “Analyze Behavior.”
The system looks at my corrections and rewrites its own system prompt.
Next time I run it, it knows: “Oh, he likes to keep his ‘Localhost’ tabs separate from his ‘Production’ tabs,” because it updated its own instructions based on my manual fixes.
The Tech Stack
For the frontend devs out there, I built this using Plasmo. It’s basically the Next.js of browser extensions, makes working with React and TypeScript in a chrome-extension environment actually bearable.
Everything is local. Your API keys are stored in your browser, and the learning data (your grouping habits) stays on your machine.
Try it out
It’s open source (GPL-3.0). I built it because I needed it, but if you’re tired of domain-based grouping that doesn’t actually help, give it a shot.
Như đã sơ lược qua ở phần trước, HTTP/2 Server Push (gọi tắt là HTTP/2 push) cho phép máy chủ (server) gửi thêm tài nguyên mà client (trình duyệt) chưa yêu cầu. Trước HTTP/2, HTTP chỉ là “hỏi-đáp đơn giản”: Trình duyệt yêu cầu một trang web, server trả về, rồi trình duyệt phải tải trang đó, phân tích, và yêu cầu thêm tài nguyên như CSS, JavaScript, font chữ, hình ảnh.
Ví dụ: Khi bạn mở một trang blog, trình duyệt tải HTML trước, rồi mới “thấy” cần CSS từ việc HTML khai báo rằng css cần thiết. Quá trình này tạo ra ít nhất một vòng lặp (round-trip) thừa, làm chậm thời gian hiển thị ban đầu (initial paint). Hình ảnh thì không sao (trang vẫn load với chỗ trống), nhưng CSS hay JS “critical” (quan trọng cho rendering) sẽ khiến trang “treo” đến khi tải xong.
Với HTTP/2 multiplexing (đa kênh), các yêu cầu song song giúp tốt hơn HTTP/1, nhưng vẫn cần round-trip thứ hai. HTTP/2 push “phá vỡ quy tắc” bằng cách server gửi luôn tài nguyên phụ ngay từ đầu, giảm thời gian tải từ 2 round-trip xuống còn 1.
Hình dung qua ví dụ waterfall diagram (biểu đồ thác nước):
Không push: HTML tải xong → Phân tích → Yêu cầu CSS/JS → Chờ tải → Render.
Có push: Server gửi HTML + CSS/JS cùng lúc → Render ngay!
Inline CSS
Để giảm độ trễ, các lập trình viên thường inline các tài nguyên quan trọng ngay vào HTML, nhờ đó trình duyệt có thể bắt đầu render ngay sau khi phân tích trang gốc, thay vì chờ tải thêm tài nguyên.
Tuy nhiên, inline CSS/JS tiềm ẩn nhiều hạn chế:
Khi cần sửa critical CSS (ví dụ redesign), phải cập nhật từng trang chứ không chỉ 1 file chung.
Thường chỉ chứa các style cần thiết cho lần render đầu; toàn bộ stylesheet được tải sau để giảm độ lớn mã inline.
Cần công cụ phân tích để trích đúng những phần “critical” công việc phức tạp.
Dẫn đến trùng lặp: mỗi trang website đều chứa CSS critical riêng, thay vì dùng file có thể cache giữa các trang.
Sau đó nội dung critical CSS còn được giữ trong stylesheet chính, gây trùng lặp trong mỗi trang, không chỉ giữa các trang.
Để tải CSS không quan trọng, cần dùng JavaScript thay vì thẻ <link>, bởi thẻ link nhúng CSS thông thường sẽ block rendering; thẻ link không hỗ trợ async.
Làm rõ hơn cơ chế HTTP/2 Push
HTTP/2 push phá bỏ quy tắc “1 request = 1 response”. Máy chủ có thể trả về nhiều tài nguyên kèm theo một yêu cầu. Ví dụ:
– Client: “Cho tôi trang này.” – Server: “Được thôi, đây là trang HTML, thêm cả CSS và JS cần thiết để bắt đầu rendering”.
Thể hiện dưới dạng waterfall: các tài nguyên không đến đúng lúc, có khoảng cách nhỏ giữa chúng, nhưng tổng thời gian gần 1 vòng chứ không phải 2 như trước.
Tương tự, chuỗi flow request–response có thể thấy như hình dưới, có thể thấy rõ sự tiết kiệm thời gian khi gửi các tài nguyên quan trọng cùng với trang ban đầu.
HTTP/2 Push không thay thế được WebSockets hay SSE
Điểm then chốt: push chỉ xảy ra khi có request ban đầu từ client, server không thể tự ý push bất kỳ lúc nào. WebSockets hay SSE cho phép two-way communication, nhưng HTTP/2 không thật sự hai chiều, mọi thứ đều do client khởi đầu. Sau khi stream request đầu kết thúc, server không thể tiếp tục push trừ khi client gửi request mới. Vì vậy, HTTP/2 push không thay thế WebSocket hay SSE theo chuẩn hiện tại
Cơ chế hoạt động của HTTP/2 Push trong trình duyệt
Trình duyệt xử lý HTTP/2 push lại theo một cách khác. Tài nguyên không được đẩy thẳng đến trang web, mà được đẩy vào một khu vực cache đặc biệt. Trang web vẫn được xử lý bình thường. Khi cần tài nguyên, trình duyệt kiểm tra cache, nếu có sẵn thì tải từ cache thay vì gửi yêu cầu tới server.
Cơ chế chi tiết phụ thuộc vào từng trình duyệt và không được nêu rõ trong spec HTTP/2, nhưng hầu hết hiện nay triển khai một HTTP/2 push cache riêng biệt, khác với HTTP cache thông thường
Cách hoạt động của push cache
Các tài nguyên được đẩy sẽ nằm trong một vùng nhớ riêng (HTTP/2 push cache) chờ trình duyệt yêu cầu. Khi được truy cập, tài nguyên sẽ được đưa vào trang và nếu có header phù hợp sẽ được đồng thời lưu vào HTTP cache để sử dụng sau này.
Một điểm đặc biệt: các trình duyệt dựa trên Chromium (Chrome, Opera) không cache tài nguyên nếu certificate không đáng tin (như tự ký self-signed, hiển thị ổ khoá đỏ), dù người dùng có bỏ qua lỗi. Để HTTP/2 Push hoạt động, bạn cần certificate hợp lệ (ổ khoá xanh)
Quá trình kiểm tra cache của trình duyệt theo thứ tự như sau: image cache → preload cache → service worker → HTTP cache → HTTP/2 push cache. Nếu tài nguyên đã có sẵn ở cache HTTP chính (mặc dù phiên bản mới đã bị push), trình duyệt vẫn ưu tiên dùng bản cũ theo cache-control (Jake Archibald) Service worker còn được kiểm sau preload cache
Image cache (cache hình ảnh):
Là cache tạm, nằm trong bộ nhớ (in-memory) chỉ phục vụ cho trang hiện tại.
Nó giúp trình duyệt không phải tải lại cùng một ảnh nếu trang tham chiếu tới ảnh đó nhiều lần.
Khi người dùng rời khỏi trang, cache này bị hủy.
Preload cache:
Cũng là cache tạm, trong bộ nhớ và chỉ gắn với một trang.
Dùng để giữ các tài nguyên được preload (sẽ nói kỹ hơn ở chương 6).
Lưu ý: không nên preload tài nguyên cho trang khác, vì preload cache không dùng chung giữa các trang.
Service Worker cache:
Service Worker là một loại ứng dụng nền, chạy độc lập với web page, đóng vai trò trung gian giữa web page và server.
Nó cho phép web hoạt động giống native app hơn, ví dụ vẫn có thể hoạt động khi mất mạng.
Service Worker có hệ thống cache riêng, gắn với domain.
HTTP cache (cache truyền thống):
Đây là cache chính mà lập trình viên quen thuộc nhất.
Nó được lưu trên đĩa (persistent), chia sẻ giữa nhiều trang, có dung lượng giới hạn và được dùng cho tất cả domain.
HTTP/2 push cache:
Đây là cache tạm, nằm trong bộ nhớ, gắn liền với một kết nối (connection).
Nếu server push styles.css, file này sẽ được đưa vào HTTP/2 push cache. Sau đó, khi trình duyệt thấy cần styles.css, nó không hề “biết” hay quan tâm rằng server đã push sẵn file này. Trình duyệt vẫn kiểm tra toàn bộ cache theo thứ tự:
Image cache
Preload cache
Service Worker cache
HTTP cache
HTTP/2 push cache
Nếu trong HTTP cache chính đã có một bản styles.css hợp lệ, thì trình duyệt sẽ lấy từ đó kể cả khi trong push cache đang có bản mới hơn.
Bạn có thể dùng công cụ chrome://net-export (nói trong mục 4.3.1) để xem tổng hợp các tài nguyên đã được push nhưng chưa được sử dụng (unclaimed push resources) trong tất cả các trang đang mở
Nếu một kết nối bị đóng, push cache cũng mất theo, khác với HTTP cache. Một cách hiểu ngắn gọn: push cache chỉ tồn tại gắn với kết nối. Khi kết nối không tái sử dụng, tài nguyên push có thể bị lãng phí. Một lần tài nguyên được sử dụng (“claimed”), nó sẽ bị loại khỏi push cache. Nhưng nếu có cache-control phù hợp, vẫn có thể lưu trong HTTP cache. Thậm chí tài nguyên không thể cache theo HTTP (no-cache, no-store) vẫn có thể được push và đọc từ push cache, bởi đây không thực sự là “cache” truyền thống mà là vùng đệm tạm thời
HTTP/2 Push Cache và những vấn đề phát sinh
Push cache gắn liền với connection
HTTP/2 push cache gắn trực tiếp với một kết nối (connection). Điều này dẫn đến:
Nếu kết nối không được dùng → tài nguyên push cũng không được dùng.
Nếu kết nối bị mất → push cache và tất cả tài nguyên chưa dùng cũng mất → việc push bị lãng phí.
Nếu trình duyệt mở thêm một kết nối khác → tài nguyên push có thể sẽ không được dùng.
HTTP/2 thiết kế để chỉ có một connection duy nhất, nên thoạt nhìn có vẻ không có vấn đề. Nhưng thực tế các trình duyệt triển khai khác nhau:
Chrome, Firefox: chia sẻ connection giữa các tab.
Edge: mỗi tab dùng connection riêng.
Safari: có thể mở nhiều connection ngay trong cùng một tab.
Ngoài ra, các request không kèm thông tin xác thực (noncredentialed) thường được gửi trên một connection riêng. Do đó:
Không thể push font cross-origin (từ domain khác, kể cả domain shard) vì chúng phải đi qua noncredentialed request.
Do Push cache hoạt động ở mức connection, chứ không phải mức page. Vì vậy, tuy về lý thuyết bạn có thể push tài nguyên cho trang sẽ load sau, nhưng trên thực tế điều này gần như vô ích, vì cache ngắn hạn và có thể mất khi kết nối rớt.
Push cache khác với HTTP cache
Khi một tài nguyên được lấy ra khỏi push cache, nó sẽ bị xóa khỏi đó và không thể dùng lại từ push cache lần nữa. Nhưng nếu tài nguyên có cache-control hợp lệ, nó sẽ được lưu vào HTTP cache chính để dùng sau.
Push cache có thể chứa cả tài nguyên không cache được (ví dụ header no-cache hoặc no-store). Đây là điểm khác với HTTP cache truyền thống.
Chính vì vậy, push cache không thực sự là cache theo đúng nghĩa, mà giống như một “kho tạm chứa request”
RST_STREAM – Cách từ chối tài nguyên bị push
Trình duyệt có thể từ chối một tài nguyên đang bị push bằng cách gửi một RST_STREAM frame với mã CANCEL hoặc REFUSED_STREAM. Điều này xảy ra khi:
Trình duyệt đã có sẵn tài nguyên trong cache.
Người dùng rời khỏi trang khi nó vẫn đang load → không cần tải thêm tài nguyên nữa.
Tuy nhiên, RST_STREAM có hạn chế:
Việc gửi tín hiệu RST_STREAM mất thời gian → trong lúc đó server vẫn tiếp tục gửi data (HEADERS, DATA frames). Có thể cả file đã được gửi xong trước khi server kịp ngừng lại.
Đây chỉ là tín hiệu điều khiển, không mạnh bằng việc cắt hẳn connection (HTTP/2 không cho phép ngắt connection vì sẽ ảnh hưởng tới tất cả stream khác).
Vì vậy, RST_STREAM không phải giải pháp hiệu quả để ngăn chặn việc push sai tài nguyên.
Ví dụ:
Nếu push một ảnh rất lớn nhưng trang đã được cập nhật không dùng ảnh đó nữa → trình duyệt vẫn tải hết ảnh về nhưng không dùng, gây lãng phí băng thông.
Thậm chí bạn còn không biết mình đang push nhầm, vì một số công cụ (DevTools) có thể không hiển thị tài nguyên bị push mà không dùng.
Client có thể tắt push bằng cách đặt SETTINGS_ENABLE_PUSH = 0 trong khung SETTINGS. Khi đó, server không được phép gửi PUSH_PROMISE nữa.
Request được push phải là cacheable methods (thường là GET, HEAD, hoặc một số POST đặc biệt).
Request được push phải là safe methods (thường là GET hoặc HEAD).
Request được push không được có request body (nhưng response thường có body).
Request được push chỉ được gửi đến những domain mà server có thẩm quyền (authoritative).
Chỉ server mới có quyền push, client không được push.
Resource chỉ có thể được push như phản hồi cho một request hiện tại. Server không thể tự phát khởi một push nếu không có request nào đang diễn ra.
Thực tế, vì các quy tắc trên, chỉ có GET request là thường được push.
Giới hạn về authority nghĩa là bạn chỉ được phép push tài nguyên mà server trực tiếp hoặc gián tiếp phục vụ. Ví dụ, nếu trang của bạn dùng Bootstrap từ getbootstrap.com hoặc jQuery từ jquery.com, thì server của bạn không thể push trực tiếp. Bạn có thể proxy các request đó qua server của mình, nhưng khi đó bạn phải sửa tất cả các tham chiếu để trỏ về server của bạn. Ở tình huống đó, tốt hơn hết là host luôn file đó tại chỗ thay vì tạo thêm phức tạp với proxy.
HTTP/2 Push được thiết kế để tối ưu hiệu năng, nhưng nếu lạm dụng, nó có thể làm chậm hiệu năng vì lãng phí băng thông để push những tài nguyên mà client không dùng, thay vì ưu tiên cho những tài nguyên cần thiết.
Lý tưởng nhất, chỉ nên push tài nguyên quan trọng (critical assets) mà trang chắc chắn cần.
Không nên push:
Tài nguyên không được sử dụng.
Tài nguyên mà client không thể dùng (ví dụ: image format không hỗ trợ).
Tài nguyên chỉ dùng trong một số điều kiện (ví dụ: hình ảnh cho màn hình lớn).
Nhóm Chrome đã viết một tài liệu chi tiết về “nên push cái gì” (Chrome doc on HTTP/2 Push), trong đó họ khuyến nghị:
“Chỉ push mức tối thiểu cần thiết để lấp đầy thời gian mạng rảnh, và không hơn.”
Push chỉ để tận dụng thời gian mạng rảnh (idle network time).
Không nên push toàn bộ tài nguyên mà trang cần, vì như vậy sẽ ghi đè cơ chế ưu tiên tải (prioritization) vốn được trình duyệt tối ưu tốt hơn.
This entry is part 1 of 8 in the series SWE Học A.I
Học máy (Machine Learning) là gì?
Học máy, nói một cách đơn giản, là một tập hợp những kỹ thuật mà máy tính dùng để “học” từ dữ liệu. Dữ liệu ở đây có thể là bất cứ thứ gì được biểu diễn bằng số. Có thể là giá cổ phiếu biến động qua từng ngày, khối lượng của các hành tinh, chiều cao của mọi người đến hội chợ, hay thậm chí là âm thanh khi bạn nói “Alo” vào điện thoại, những bức ảnh chụp hoa lá, mèo con, hay cả nội dung một bài báo, một cuốn tiểu thuyết. Thậm chí, sở thích nghe nhạc, xem phim, hay hành vi của bạn – như việc bạn hay đi dạo công viên hay thích ăn phở vào cuối tuần – cũng có thể được mã hóa thành số và trở thành dữ liệu.
Mục tiêu của học máy là tìm ra những thông tin “có ý nghĩa” từ đống dữ liệu đó. Nhưng “có ý nghĩa” là thế nào? Tùy bạn thôi! Có khi là tìm ra quy luật ẩn trong dữ liệu, như đoán xem bạn sẽ thích bộ phim nào dựa trên những phim bạn đã xem và chấm điểm. Hoặc là nhận diện chữ viết tay trên một mẩu giấy, hay chỉ cần vài nốt nhạc là biết ngay bài hát đó là gì. Tóm lại, học máy giúp máy tính “hiểu” và dự đoán dựa trên dữ liệu, giống như cách bạn nhận ra một người bạn cũ qua vài đặc điểm quen thuộc.
Giả sử chúng ta có một bảng dữ liệu ghi lại thông tin về thói quen xem phim của một nhóm người:
Người dùng
Phim đã xem
Thể loại yêu thích
Điểm chấm trung bình
A
Inception, Interstellar, Tenet
Khoa học viễn tưởng
9.0
B
The Notebook, Titanic, La La Land
Lãng mạn, Tình cảm
8.5
C
The Dark Knight, Joker, Logan
Hành động, Tâm lý
9.2
D
Coco, Encanto, Inside Out
Hoạt hình, Gia đình
8.8
E
Inception, The Matrix, Blade Runner
Khoa học viễn tưởng
9.1
Từ bảng dữ liệu trên, ta có thể “nhìn thấy” một số điều thú vị. Ví dụ, người dùng A và E có điểm chấm khá cao và cùng yêu thích thể loại khoa học viễn tưởng. Từ đó, một hệ thống học máy có thể đề xuất cho người dùng A những bộ phim như The Matrix hoặc Blade Runner – vốn đã được người dùng E yêu thích. Hay ngược lại, với người dùng mới có hành vi tương tự A, hệ thống có thể dự đoán họ sẽ thích Interstellar chẳng hạn.
Chính từ những mẫu dữ liệu tưởng chừng đơn giản như thế này, học máy có thể học được mối liên hệ giữa “sở thích” và “nội dung”, để từ đó đưa ra những đề xuất, dự đoán, hay thậm chí là tự động phân loại, nhận diện xu hướng mà con người khó nhìn thấy bằng mắt thường.
Hệ chuyên gia (Expert System)
Ngày xưa, khi học sâu (deep learning) chưa có được sự chú ý nhiều như bây giờ, người ta hay dùng hệ chuyên gia để dạy máy tính học từ dữ liệu.
Hệ chuyên gia là gì? Nó giống như một chương trình máy tính được thiết kế để bắt chước cách suy nghĩ của các chuyên gia con người – như bác sĩ, kỹ sư, hay thậm chí là nhạc sĩ. Ý tưởng là bạn quan sát một chuyên gia làm việc, xem họ làm gì, làm như thế nào, đôi khi còn yêu cầu họ kể lại quy trình suy nghĩ của mình. Từ đó, bạn tạo ra một bộ quy tắc để máy tính làm theo. Hy vọng là máy tính sẽ “bắt chước” được công việc của chuyên gia chỉ bằng cách tuân theo những quy tắc đó.
Ví dụ thực tế: Nhận diện số 7
Giả sử chúng ta muốn dạy máy tính nhận diện số 7 viết tay. Sau khi hỏi han, quan sát nhiều người, bạn có thể nghĩ ra ba quy tắc nhỏ để nhận diện số 7:
một, số 7 thường có một đường ngang ở gần đỉnh;
hai, nó có một đường chéo từ đông bắc xuống tây nam;
và ba, hai đường đó gặp nhau ở góc trên bên phải.
Nhưng đời không như mơ! Một ngày, bạn gặp một số 7 kỳ lạ.
Số 7 này có thêm một đường ngang cắt qua đường chéo ở giữa.
Bộ quy tắc ban đầu của chúng ta không nhận ra đây là số 7, vì chúng ta chưa nghĩ tới trường hợp đặc biệt này. Thế là phải thêm một quy tắc mới. Nhưng rồi lại có số 7 khác, với kiểu viết khác, và bạn lại phải thêm quy tắc nữa. Cứ thế, việc tạo ra một bộ quy tắc đầy đủ cho một vấn đề phức tạp trở thành một nhiệm vụ bất khả thi. Bạn phải đào bới những suy luận mà ngay cả chuyên gia cũng không nhận ra, biến chúng thành hàng tá quy tắc, rồi chỉnh sửa, gỡ lỗi, bổ sung cho những trường hợp bị bỏ sót.
Quá nhiều quy tắc
Tìm ra bộ quy tắc hoàn chỉnh không phải chuyện dễ. Chuyên gia con người thường làm việc theo trực giác, và những ngoại lệ hay trường hợp đặc biệt thì vô vàn.
Bạn thử tưởng tượng mà xem: làm sao viết được bộ quy tắc để máy tính bắt chước cách một bác sĩ X-quang đọc phim MRI để xác định một vết mờ là lành tính hay không? Hay cách một nhân viên điều khiển không lưu xử lý hàng loạt chuyến bay trong giờ cao điểm? Hoặc cách ai đó lái xe an toàn trong cơn bão lớn?
Đã vậy, công nghệ, luật lệ, hay thói quen xã hội còn thay đổi liên tục, khiến bạn phải luôn cập nhật, sửa chữa bộ quy tắc rối như tơ vò này.
Hệ chuyên gia có thể hoạt động tốt trong một số trường hợp, nhưng để áp dụng rộng rãi thì quá khó. Với những trường hợp như này, “học sâu” (deep learning) có thể thể hiện được năng lực của nó. Với đủ dữ liệu huấn luyện, các thuật toán học sâu tự tìm ra quy tắc mà không cần chúng ta phải chỉ từng bước.
Bạn không cần nói với máy tính rằng số 2 trông thế nào, hay số 7 ra sao – nó tự học từ dữ liệu. Nó có thể xác định vết mờ trên MRI là lành tính hay không, đánh giá một bức ảnh chụp từ điện thoại có phơi sáng đúng không, hay xác minh một đoạn văn có phải do một nhân vật lịch sử viết hay không.
Học có giám sát (Supervised Learning)
Nói một cách đơn giản, học máy (machine learning) là xây dựng các thuật toán để tìm ra mối quan hệ trong dữ liệu.
Học có giám sát là một phần của học máy, tập trung vào việc tìm mối liên hệ giữa các đặc điểm đã được đo lường sẵn. Chẳng hạn, trong chương này, mình sẽ nói về một bài toán thực tế: tìm mối liên hệ giữa các đặc điểm của một ngôi nhà (như số phòng, diện tích, hay khoảng cách đến trường học) và giá trị của ngôi nhà.
Giả sử ta muốn tạo một hệ thống, gọi là bộ phân loại hình ảnh, để nhận ra đồ vật chính trong ảnh. Mình sẽ đưa máy một bộ sưu tập ảnh, mỗi ảnh đi kèm một nhãn – như ảnh con hổ có nhãn “hổ”, ảnh con mèo có nhãn “mèo”, và chúng ta kì vọng khi thấy một hình ảnh con hổ khác chưa có trong bộ sưu tập, hệ thống có thể biết được nó là hổ hay mèo.
Hoặc, nếu mình có một cuốn sổ ghi lại các công thức nấu ăn và chấm điểm xem mình thích món nào, thì công thức là dữ liệu đầu vào, còn điểm số là nhãn. Sau khi được huấn luyện với cả tá dữ liệu như thế, máy có thể nhìn một công thức mới và đoán món này có ngon theo khẩu vị của mình không.
Máy Tính “Nghĩ” Như Thế Nào?
Thực chất, máy tính không hiểu món ăn ngon dở ra sao, cũng chẳng biết con hổ trông thế nào ngoài đời. Nó chỉ nhìn vào dữ liệu bạn đưa, tìm các mẫu (patterns) trong đó, và học cách khớp dữ liệu với nhãn. Khi bạn đưa vào một bức ảnh hay công thức mới, máy sẽ dùng những “quy tắc” nó học được để chọn nhãn phù hợp nhất. Nếu máy đoán đúng đủ nhiều lần, mình gọi là nó đã được huấn luyện tốt.
Nhìn vào ảnh trên, nơi một bộ phân loại hình ảnh đã được huấn luyện thử phân tích bốn bức ảnh lấy từ trên mạng. Máy chưa từng thấy những bức ảnh này bao giờ. Với mỗi bức, nó đưa ra xác suất cho 1.000 nhãn mà nó biết. Chẳng hạn, bức ảnh ở góc trên bên trái là một chùm chuối. Lý tưởng thì máy sẽ trả về nhãn “chùm chuối”, nhưng vì nó không được huấn luyện với nhãn này, nó chỉ chọn được nhãn gần nhất là “chuối”. Giống như khi bạn chỉ biết từ “mèo” và không biết từ “mèo tam thể”, bạn sẽ gọi con mèo tam thể là “mèo” thôi.
Cách Dữ Liệu Được Sắp Xếp
Mỗi đặc điểm cần được đo lường chính xác và biểu diễn bằng số. Số phòng hay diện tích thì dễ, vì chúng đã là số. Nhưng nếu bạn có thông tin kiểu như mô tả bằng ngôn ngữ tự nhiên về khu phố từ TripAdvisor, việc chuyển chúng thành số sẽ phức tạp hơn nhiều. Chuyển đổi đúng cách là yếu tố quyết định xem bạn có tìm ra được mối quan hệ hay không. Với khái niệm mơ hồ như “giá trị ngôi nhà”, mình thường chọn một con số cụ thể, như giá bán.
Khi đã có các đặc điểm dưới dạng số, mình cần tổ chức chúng sao cho dễ xử lý. Cách phổ biến là biểu diễn mỗi ngôi nhà bằng một hàng số (gọi là một “quan sát”), rồi xếp các hàng này thành từng lô (batch) dữ liệu.
Mỗi hàng là một tập hợp các đặc điểm (features), như số phòng, diện tích, hay khoảng cách đến trường. Quá trình chuyển các đặc điểm thực tế (như “nhà gạch đỏ”) thành số được gọi là kỹ thuật đặc trưng (feature engineering).
Giả sử chúng ta đang thu thập dữ liệu để dự đoán giá bán của các ngôi nhà. Dưới đây là một bảng thể hiện dữ liệu đã được xử lý thành dạng số:
ID nhà
Số phòng ngủ
Diện tích (m²)
Khoảng cách đến trường (km)
Loại tường
Có sân vườn
Điểm đánh giá khu phố
Giá bán (triệu VND)
1
3
120
0.5
1 (gạch đỏ)
1 (có)
8.2
3.200
2
2
80
1.2
0 (xi măng)
0 (không)
7.1
2.400
3
4
150
0.3
1 (gạch đỏ)
1 (có)
8.9
4.100
4
1
60
2.0
0 (xi măng)
0 (không)
6.5
1.800
Ở đây, các thông tin dạng mô tả như “loại tường” (gạch đỏ hay xi măng), hay “có sân vườn” (có/không) đã được chuyển thành số – một bước quan trọng trong feature engineering.
Từ bảng dữ liệu trên, ta có thể thấy: những ngôi nhà có diện tích lớn hơn, gần trường học hơn, và nằm trong khu phố có điểm đánh giá cao thường có giá bán cao hơn. Điều này cho thấy tầm quan trọng của việc chuyển đổi và biểu diễn đúng các đặc điểm thành dạng số: chỉ khi dữ liệu được tổ chức một cách chính xác, mô hình học máy mới có thể nhận ra các mối liên hệ như vậy để đưa ra dự đoán hợp lý.
Học không giám sát (Unsupervised Learning)
Ta đã đi qua việc giúp máy tính học được từ những dữ liệu đã được đánh nhãn (label) rõ ràng, dễ sử dụng. Tuy nhiên nếu bạn có một đống dữ liệu mà chẳng có nhãn hay gợi ý gì – giống như một mớ đồ vật lộn xộn, không biết thuộc về đâu. Lúc này, chúng ta có thể dùng học không giám sát. (Unsupervised Learning)
Các thuật toán này sẽ tự mày mò, tìm ra mối liên hệ giữa các mẩu dữ liệu mà chẳng cần ai chỉ bảo “cái này là gì, cái kia là gì”.
Ví dụ:
Khi đang đào móng cho một ngôi nhà mới thì phát hiện cả đống bình gốm cổ, mỗi cái có hoa văn khác nhau. Mình gọi bạn khảo cổ đến, nhưng cô ấy chẳng nhận ra hoa văn nào cả (dữ liệu không có nhãn). Có cái trông giống nhau, có cái lại khác biệt hoàn toàn.
Để sắp xếp đống lộn xộn này, cô ấy đã tiếp cận bằng một cách khác, dù không nhận ra hay biết về bất kì kí hiệu nào trên các món đồ cổ, nên cô không biết những món đồ này tới từ thời đại nào, quý giá hay không. Tuy nhiên, nhìn vào những kí hiệu, trang trí, ta thấy chúng có vẻ giống nhau?, và xuất phát từ 1 phong cách nào đó. Lúc này, nhà khảo cổ chia các phần có vẻ giống nhau thành từng cụm, từng nhóm.
Kĩ thuật này đưa các dữ liệu thành các nhóm liên quan (cụm), nên ta gọi quá trình này là phân cụm (clustering), hay gọi thuật toán này là thuật toán phân cụm (clustering algorithm)
Ngoài ra, học không giám sát còn giúp làm đẹp dữ liệu, như xóa nhiễu trên ảnh chụp bằng điện thoại, hay nén dữ liệu để tiết kiệm không gian mà vẫn giữ được chất lượng – giống như cách file MP3 lưu âm thanh hay JPG lưu ảnh vậy.
Ảnh chụp từ điện thoại thường có nhiễu (noise) do ánh sáng yếu, cảm biến kém, chuyển động, v.v. Học không giám sát có thể tự học cách tách tín hiệu gốc (hình ảnh thật) khỏi nhiễu mà không cần dữ liệu “sạch” để so sánh. Thông thường bài toán này hoạt động nhờ máy chỉ giữ lại thông tin phổ biến, đặc trưng, không thay đổi, còn phần ngẫu nhiên (nhiễu) sẽ bị mất đi.
Giống như file MP3 hay JPG: giữ phần quan trọng (tai người/ mắt người cảm nhận được). Loại bỏ phần dư thừa (âm thanh ngoài khả năng nghe, màu sắc không phân biệt được…).
Học tăng cường (Reinforcement Learning)
Hãy tưởng tượng bạn đang chơi một trò chơi phức tạp, như cờ vua hay sáng tác một bản nhạc. Bạn đứng trước một tình huống: nước đi tiếp theo nên là gì, hay nốt nhạc tiếp theo nên chọn ra sao? Chẳng có một đáp án nào hoàn toàn “đúng” cả. Nhưng bạn có thể cảm nhận rằng một lựa chọn thì “tốt hơn” cái kia. (thử một nốt nhạc khác và khi nghe lại thấy bài nhạc có vẻ hay hơn)
Vậy làm sao để máy tính tự tìm ra cách tốt nhất? Chúng ta để nó thử nghiệm, sai lầm, và học hỏi.
Học tăng cường là khi bạn không đưa ra một hướng dẫn chi tiết kiểu “làm thế này, thế kia”. Thay vào đó, bạn chỉ cần đánh giá những gì máy tính thử nghiệm. Máy tính sẽ dựa vào những đánh giá đó để tìm ra cách hành động tốt nhất, từng bước một, hướng tới kết quả mà chúng ta mong muốn.
Ví dụ:
Nhiệm vụ của chúng ta là quyết định xem thang máy nên “đậu” ở đâu khi không có người gọi, và khi ai đó bấm nút gọi thang, chiếc thang nào sẽ được điều động. Mục tiêu? Làm sao để thời gian chờ trung bình của mọi người là ngắn nhất.
Vấn đề là, làm sao biết được cách sắp xếp thang máy tốt nhất? Chuyện này phụ thuộc hoàn toàn vào thói quen của mọi người trong tòa nhà.
Buổi sáng, có lẽ mọi người đổ xô đến làm việc, nên thang máy nên chờ sẵn ở tầng một để đón khách. Nhưng đến giờ ăn trưa, mọi người lại muốn ra ngoài, vậy thì thang máy nên “canh” ở các tầng trên, sẵn sàng đưa mọi người xuống tầng trệt. À, mà nếu trời mưa thì sao? Có khi mọi người lại muốn lên căng-tin ở tầng cao nhất để ăn trưa. Mỗi ngày, mỗi giờ, tình hình cứ thay đổi liên tục. Vậy thì làm sao tìm được một “chiến lược” hoàn hảo?
Thực chất, chẳng có một chiến lược nào hoàn hảo cả.
Học tăng cường là một giải pháp với ý tưởng: máy tính sẽ tự nghĩ ra một kế hoạch, hoặc thử thay đổi một chút từ kế hoạch hiện tại, rồi kiểm tra xem nó hoạt động tốt thế nào.
Ví dụ
Hãy tưởng tượng bạn có một robot giao hàng trong một siêu thị nhỏ. Nhiệm vụ của robot là giao một đơn hàng từ kho hàng (điểm xuất phát) đến quầy thanh toán (điểm đích). Siêu thị có 3 con đường để đi:
Đường C: Qua khu đồ đông lạnh, trung bình về cả độ dài và đông người.
Đường A: Qua khu rau củ, ngắn nhưng đông khách, dễ bị chậm.
Đường B: Qua khu đồ khô, đường dài hơn nhưng ít người.
Robot sẽ thử đi từng đường, nhận phản hồi là thời gian đi mất bao lâu, và học để chọn đường nhanh nhất qua nhiều lần thử.
Giả lập dữ liệu (thực tế, robot không biết trước):
Đường A: 30 giây (ngắn nhưng đông, dễ chậm).
Đường B: 50 giây (dài hơn, ít người).
Đường C: 40 giây (trung bình).
Phản hồi (thưởng): Robot được điểm dựa trên thời gian:
Điểm = 100 – thời gian (giây). Nghĩa là đi càng nhanh, điểm càng cao.
Đường A: 100 – 30 = 70 điểm.
Đường B: 100 – 50 = 50 điểm.
Đường C: 100 – 40 = 60 điểm.
Mỗi lần, robot chọn một đường, đo thời gian, tính điểm, và ghi nhớ để cải thiện.
Lần thử 1:
Robot chọn ngẫu nhiên: Đường A.
Thời gian: 30 giây.
Điểm: 100 – 30 = 70 điểm.
Lần thử 2:
Robot chọn: Đường B.
Thời gian: 50 giây.
Điểm: 100 – 50 = 50 điểm.
Lần thử 3:
Robot chọn: Đường C.
Thời gian: 40 giây.
Điểm: 100 – 40 = 60 điểm.
Lần thử 4:
Robot quay lại thử Đường A (để kiểm tra lại).
Thời gian: 30 giây.
Điểm: 70 điểm.
Lần thử 5:
Robot thử Đường C lần nữa (vì nó từng cho 60 điểm, khá ổn).
Thời gian: 40 giây.
Điểm: 60 điểm.
Bây giờ, giả sử vào giờ cao điểm (buổi trưa), khu rau củ (Đường A) trở nên rất đông, làm thời gian tăng lên. Dữ liệu mới:
Robot nhận ra: Vào giờ cao điểm, Đường C là lựa chọn tốt nhất (60 điểm, 40 giây).
Robot sẽ ưu tiên Đường C vào giờ trưa, nhưng vẫn thử các đường khác để cập nhật nếu tình hình thay đổi (ví dụ: khu đồ khô bỗng dưng đông vào giờ khác).
Ta đã có 1 hệ thống biết nên sử dụng đường nào theo tham số thời gian, thông minh hơn là chỉ luôn sử dụng 1 bộ quy tắc
Áp dụng vào ví dụ thang máy
Giờ quay lại ví dụ thang máy trong tòa nhà. Giả sử có 3 thang máy và ta phải quyết định thang nào đi đến tầng được gọi (ví dụ: tầng 5).
Hành động: Gửi thang máy A, B, hoặc C đến tầng 5.
Phản hồi: Đo thời gian chờ của khách (thang nào đến nhanh thì được “thưởng” điểm cao).
Học hỏi: Máy tính thử gửi từng thang, đo thời gian chờ, và ghi nhớ thang nào nhanh nhất.
Qua nhiều lần thử, máy tính sẽ biết: “Nếu khách gọi ở tầng 5, gửi thang B vì nó gần nhất, chờ chỉ 10 giây, tốt hơn thang A (20 giây) hay thang C (15 giây).”
Học sâu (Deep Learning)
Thuật ngữ “học sâu” chỉ các thuật toán học máy sử dụng nhiều tầng tính toán, hay còn gọi là các tầng.
Khi một hệ thống học sâu gắn tên cho một khuôn mặt trong ảnh, nó không biết khuôn mặt là gì, con người là gì, hay thậm chí con người có tồn tại hay không. Máy tính chỉ đo lường các điểm ảnh và, dựa trên các mẫu học được từ dữ liệu huấn luyện, đưa ra nhãn phù hợp nhất.
Dần dần, qua hàng ngàn lần thử, mạng học được cách tạo ra kết quả chính xác. Nếu làm tốt, mạng sẽ đưa ra câu trả lời đúng cho hầu hết dữ liệu huấn luyện, sẵn sàng để trở thành một trợ thủ đắc lực – như nhận diện hình ảnh trên mạng xã hội hay dự đoán giá cổ phiếu.
Ví dụ
Dạy máy đoán táo hay cam Giả sử bạn muốn máy phân biệt táo và cam dựa trên hai đặc điểm:
Kích thước: Táo thì nhỏ hơn, còn cam thì có kích thước lớn hơn. Đặt nhỏ thì là 1, lớn thì là 2
Màu sắc: Táo thì có sắc đỏ, cam thì có màu cam. Đặt đỏ là 1, cam là 2.
Vậy giả sử nếu ai đó bảo ta rằng họ có 1 quả, và kích thước đâu đó là 1.2, và màu sắc thì khá là đỏ (khoảng 1.2, và không có nhiều ánh cam), vậy ta có thể đoán, có vẻ nó là quả táo
Tập dữ liệu giả (4 ví dụ bạn đưa cho máy):
Ví dụ 1: Kích thước 1, màu 1 → Táo.
Ví dụ 2: Kích thước 2, màu 2 → Cam.
Ví dụ 3: Kích thước 1, màu 1 → Táo.
Ví dụ 4: Kích thước 2, màu 2 → Cam.
Lúc này, ta có thể cho máy khởi tạo 1 phương trình đơn giản như:
Kết quả = (Kích thước × số A) + (Màu sắc × số B) – Số A và Số B là các tham số
Công thức mới: (2 × 0.6) + (2 × 0.4) = 1.2 + 0.8 = 2.0
Máy đoán: “Gần 2, chắc là cam!” (Đúng).
Bước 5: Lặp lại với các ví dụ khác
Máy tiếp tục thử với ví dụ 3, 4, mỗi lần đoán sai thì điều chỉnh số A và B một chút. Qua vài chục lần, máy tìm được số A và B hoàn hảo (ví dụ: A = 0.7, B = 0.5) để đoán đúng hầu hết các trường hợp.
Kết quả sau khi học Sau khi “học” cả 4 ví dụ, máy có công thức tốt:
Bây giờ, nếu bạn đưa một quả mới (kích thước 1.5, màu 1.5), máy tính: (1.5 × 0.7) + (1.5 × 0.5) = 1.05 + 0.75 = 1.8 → Đoán “Cam” (vì gần 2 hơn).
Học sâu trong thực tế khác gì với ví dụ này?
Trong học sâu, máy không chỉ dùng 2 đặc điểm (kích thước, màu) mà có thể dùng hàng ngàn đặc điểm (như mọi chi tiết trên một bức ảnh). Công thức cũng phức tạp hơn, với nhiều tầng (layers), mỗi tầng có nhiều “hộp tính toán” (nơ-ron). Nhưng ý tưởng vẫn giống:
Trong thời đại số ngày nay, dữ liệu hiện hữu ở khắp mọi nơi và đóng vai trò cực kỳ quan trọng trong cuộc sống cũng như công việc của chúng ta. Từ việc chúng ta lướt Facebook, Tiktok hàng ngày, đặt vé xem phim online, cho đến cách các công ty lớn vận hành chuỗi cung ứng phức tạp, mọi thứ đều không ngừng tạo ra và tiêu thụ dữ liệu.
Ví dụ:
Facebook biết chúng ta khi lướt trên bảng tin, sẽ dừng lại tại bài viết nào lâu, bài viết nào thu hút sự chú ý, chủ đề nào chúng ta thường xem, bạn bè nào chúng ta thường hay tương tác
Các app đặt vé xem phim có dữ liệu về phim nào nhiều người xem, bộ phim thể loại nào thì thường được giới tính, độ tuổi nào quan tâm, ….
Nhưng bạn có bao giờ dừng lại và tự hỏi, với một khối lượng dữ liệu khổng lồ và đa dạng như vậy – từ những con số khô khan trong bảng tính Excel, các giao dịch ngân hàng, đến hình ảnh, video, bài đăng mạng xã hội, hay những dòng log hệ thống dày đặc – làm thế nào để chúng ta có thể lưu trữ, quản lý và quan trọng nhất là khai thác chúng một cách hiệu quả?
Trong mọi tổ chức, thông thường sẽ xuất hiện hai nhu cầu cốt lõi:
Xử lý các hoạt động diễn ra hàng ngày
Phân tích chiến lược dựa trên dữ liệu đã tích luỹ
OLTP và OLAP
OLTP (Online Transaction Processing) – Xử lý giao dịch trực tuyến
Hãy tưởng tượng bạn đang thực hiện một giao dịch trực tuyến: đặt một chiếc vé máy bay trên website hãng hàng không, chuyển tiền qua ứng dụng ngân hàng, hay đơn giản là mua một món hàng trên sàn thương mại điện tử.
Tất cả những hành động này đòi hỏi hệ thống phải xử lý ngay lập tức, đảm bảo tính chính xác tuyệt đối (tiền phải được chuyển đúng tài khoản, vé phải được xác nhận và không bị bán trùng) và diễn ra nhanh chóng, không làm bạn phải chờ đợi. Đó chính là bản chất của OLTP – Xử lý giao dịch trực tuyến.
OLTP tập trung vào việc xử lý một khối lượng lớn các giao dịch nhỏ, riêng lẻ diễn ra liên tục trong thời gian thực. Các ví dụ đời thường của hệ thống OLTP có ở khắp nơi quanh ta: máy tính tiền ở siêu thị (quét mã sản phẩm, trừ số lượng tồn kho, tính tiền), máy ATM rút tiền, hệ thống đặt phòng khách sạn, hay việc bạn cập nhật thông tin cá nhân trên một trang web
Đặc điểm quan trọng nhất của các hệ thống OLTP là ưu tiên tốc độ xử lý cực nhanh (thường tính bằng mili giây) và tính nhất quán của dữ liệu. Chắc chắn khách hàng sẽ không muốn tiền của mình bị “bốc hơi” hay thông tin đơn hàng bị sai lệch.
Hệ thống OLTP được thiết kế để đảm bảo điều đó. Chúng thường làm việc với dữ liệu hiện tại (trạng thái mới nhất của thông tin) và thực hiện các truy vấn tương đối đơn giản và lặp đi lặp lại, như tìm kiếm thông tin một bản ghi cụ thể (point query), thêm mới, cập nhật hoặc xóa dữ liệu dựa trên hành động của người dùng.
OLAP (Online Analytical Processing) – Xử lý phân tích trực tuyến
Tuy nhiên, từ góc nhìn của một nhà quản lý doanh nghiệp. Thay vì xử lý từng giao dịch nhỏ lẻ, bạn lại có những câu hỏi lớn hơn, mang tính chiến lược:
“Trong quý vừa qua, dòng sản phẩm nào mang lại lợi nhuận cao nhất?”
“Xu hướng mua sắm của nhóm khách hàng ở độ tuổi 25-35 là gì?”
“Chiến dịch marketing giảm giá 10% vừa rồi có thực sự hiệu quả trong việc thu hút khách hàng mới không?”
Để trả lời những câu hỏi này, không thể chỉ nhìn vào một vài giao dịch đơn lẻ. Chúng ta cần phải xem xét một khối lượng dữ liệu lịch sử khổng lồ, tổng hợp thông tin từ nhiều nguồn, và phân tích chúng từ nhiều góc độ khác nhau (ví dụ: theo thời gian, theo khu vực địa lý, theo dòng sản phẩm, theo kênh bán hàng…). Đây chính là lúc OLAP – Xử lý phân tích trực tuyến phát huy vai trò
Các hệ thống OLAP được thiết kế để hỗ trợ việc phân tích dữ liệu phức tạp, giúp khám phá các xu hướng, mẫu hình ẩn giấu và cung cấp những hiểu biết sâu sắc (insights) để hỗ trợ việc ra quyết định kinh doanh (một phần quan trọng của Business Intelligence – BI). Ví dụ về các tác vụ OLAP bao gồm: phân tích doanh thu theo vùng miền qua các năm , dự báo nhu cầu thị trường cho sản phẩm mới, phân khúc khách hàng dựa trên hành vi mua sắm, hay đánh giá hiệu quả hoạt động của các phòng ban.
Khác với OLTP, các truy vấn OLAP thường rất phức tạp, đòi hỏi quét qua hàng triệu, thậm chí hàng tỷ bản ghi để tính toán các chỉ số tổng hợp (như tổng, trung bình, đếm, tỷ lệ phần trăm). Do đó, thời gian phản hồi của các truy vấn OLAP có thể lâu hơn đáng kể, từ vài giây, vài phút đến hàng giờ, tùy thuộc vào độ phức tạp và khối lượng dữ liệu. Chúng làm việc chủ yếu với dữ liệu lịch sử được tích lũy qua thời gian.
Tại sao cần phân biệt OLTP và OLAP? Mâu thuẫn thúc đẩy sự tách biệt
Bạn có thể thắc mắc: tại sao không dùng chung một hệ thống cho cả hai việc? Câu trả lời nằm ở sự mâu thuẫn cơ bản trong nhu cầu và đặc tính của OLTP và OLAP.
Hãy hình dung thế này: Hệ thống OLTP giống như một quầy giao dịch ngân hàng luôn bận rộn, cần xử lý nhanh chóng yêu cầu nạp/rút tiền của từng khách hàng. Trong khi đó, hệ thống OLAP giống như một phòng họp lớn nơi các nhà phân tích đang xem xét sổ sách kế toán của cả năm, thực hiện những phép tính toán phức tạp để lập kế hoạch chiến lược.
Nếu cố gắng thực hiện cả hai việc trong cùng một “căn phòng” (tức là trên cùng một cơ sở dữ liệu), sẽ dẫn dàng xảy ra việc các truy vấn phân tích OLAP nặng nề, đòi hỏi nhiều tài nguyên (CPU, bộ nhớ, đọc/ghi đĩa) sẽ làm chậm hoặc thậm chí “tắc nghẽn” các giao dịch OLTP đang diễn ra.
Thử tưởng tượng giao dịch mua hàng online của bạn bị treo chỉ vì bộ phận phân tích đang chạy một báo cáo doanh thu khổng lồ trên cùng hệ thống. Điều này rõ ràng là không thể chấp nhận được đối với các hoạt động kinh doanh cốt lõi, vốn đòi hỏi sự ổn định và tốc độ
Database, Data Warehouse, Data Lake
Database (Cơ sở dữ liệu) truyền thống
Database truyền thống, đặc biệt là cơ sở dữ liệu quan hệ (Relational Database), đóng vai trò như cuốn sổ cái điện tử, là nền tảng lưu trữ dữ liệu cho hầu hết các ứng dụng mà chúng ta tương tác hàng ngày, từ mạng xã hội, ứng dụng ngân hàng đến các website thương mại điện tử. Chúng được thiết kế và tối ưu chủ yếu cho các tác vụ OLTP – xử lý giao dịch trực tuyến.
Đặc điểm chính của Database truyền thống:
Loại dữ liệu: Chúng chủ yếu làm việc với dữ liệu có cấu trúc (structured data). Đây là loại dữ liệu được tổ chức một cách gọn gàng, ngăn nắp trong các bảng (tables), với các hàng (rows) và cột (columns) được định nghĩa rõ ràng, tương tự như cách bạn tổ chức dữ liệu trong một file Excel. Ví dụ: bảng Users có các cột UserID, UserName, Email, Password; bảng Orders có OrderID, UserID, OrderDate, TotalAmount.
Schema (Lược đồ/Cấu trúc): Database truyền thống sử dụng cơ chế Schema-on-Write. Điều này có nghĩa là bạn phải định nghĩa chi tiết cấu trúc của các bảng (tên cột, kiểu dữ liệu của cột là gì – số, chữ, ngày tháng…) trước khi bạn có thể lưu trữ bất kỳ dữ liệu nào vào đó. Giống như bạn phải kẻ sẵn các cột trong sổ kế toán trước khi ghi chép các khoản thu chi vậy. Lược đồ này thường khá cứng nhắc (rigid) và việc thay đổi nó có thể phức tạp, nhưng chính sự cứng nhắc này lại giúp đảm bảo tính nhất quán và toàn vẹn của dữ liệu.
Mục đích chính: Mục tiêu hàng đầu là phục vụ các hoạt động đọc, ghi, cập nhật, xóa (CRUD – Create, Read, Update, Delete) dữ liệu một cách nhanh chóng và đáng tin cậy, hỗ trợ trực tiếp cho các giao dịch OLTP của ứng dụng. Chúng cần đảm bảo mỗi giao dịch diễn ra thành công hoặc thất bại hoàn toàn (tính nguyên tử – Atomicity) và dữ liệu luôn ở trạng thái hợp lệ.
Người dùng: Người dùng trực tiếp chính của các database này thường là các Kỹ sư Backend (Backend Engineers). Họ là những người xây dựng và bảo trì các ứng dụng, viết code để tương tác (đọc/ghi) với database. Người dùng cuối như chúng ta chỉ tương tác gián tiếp với database thông qua giao diện của ứng dụng web hoặc mobile.
Ví dụ: Cơ sở dữ liệu lưu trữ thông tin tài khoản người dùng và bài viết của một diễn đàn trực tuyến, cơ sở dữ liệu quản lý thông tin sản phẩm, đơn hàng và khách hàng của một cửa hàng online, hay cơ sở dữ liệu chứa thông tin chuyến bay, lịch trình và đặt chỗ của một hãng hàng không.
Data Warehouse (Kho dữ liệu)
Khi một doanh nghiệp phát triển, họ thường có rất nhiều hệ thống hoạt động (OLTP) khác nhau: hệ thống quản lý bán hàng (POS), hệ thống quản lý quan hệ khách hàng (CRM), hệ thống quản lý kho (Inventory), hệ thống quản lý nhân sự (HR)…
Mỗi hệ thống lại có cơ sở dữ liệu riêng, dẫn đến tình trạng dữ liệu bị phân mảnh và cô lập (data silos). Việc tổng hợp dữ liệu từ tất cả các nguồn này để có một cái nhìn toàn cảnh về tình hình kinh doanh trở nên vô cùng khó khăn. Thêm vào đó, như chúng ta đã thảo luận, việc chạy các truy vấn phân tích phức tạp (OLAP) trực tiếp trên các hệ thống OLTP đang hoạt động sẽ gây ảnh hưởng nghiêm trọng đến hiệu năng.
Để giải quyết những thách thức này, khái niệm Data Warehouse (Kho dữ liệu) đã ra đời vào cuối những năm 1980, đầu 1990. Bạn có thể hình dung Data Warehouse như một thư viện trung tâm khổng lồ, được xây dựng riêng biệt với các hệ thống hoạt động hàng ngày. Nó không trực tiếp phục vụ các giao dịch tức thời, mà nhiệm vụ chính là thu thập, tích hợp và lưu trữ dữ liệu từ nhiều nguồn OLTP khác nhau trong toàn doanh nghiệp, sau đó tối ưu hóa dữ liệu đó cho mục đích phân tích (OLAP) và Business Intelligence (BI)
Quá trình “nhập sách” vào thư viện – ETL/ELT:
Làm thế nào để dữ liệu từ các hệ thống nguồn khác nhau có thể được chuyển vào Data Warehouse một cách thống nhất? Đó là nhờ một quy trình quan trọng gọi là ETL (Extract – Transform – Load).
Extract (Trích xuất): Dữ liệu được lấy ra (trích xuất) từ các hệ thống nguồn khác nhau (database OLTP, file log, thậm chí cả dữ liệu từ các dịch vụ bên ngoài như Google Analytics hay hệ thống CRM của đối tác).
Transform (Biến đổi): Đây là bước quan trọng nhất. Dữ liệu thô được trích xuất thường không đồng nhất (ví dụ: ngày tháng có định dạng khác nhau, tên khách hàng viết hoa/thường lẫn lộn, đơn vị tiền tệ khác nhau…). Ở bước này, dữ liệu sẽ được làm sạch (loại bỏ lỗi, dữ liệu trùng lặp), chuẩn hóa (đưa về cùng một định dạng, đơn vị đo lường), tích hợp (kết hợp dữ liệu từ nhiều nguồn, ví dụ: liên kết thông tin khách hàng từ CRM với lịch sử mua hàng từ hệ thống bán lẻ), và biến đổi thành một cấu trúc (schema) phù hợp cho việc phân tích.
Load (Tải): Sau khi đã được “tút tát” sạch đẹp và đúng chuẩn, dữ liệu sẽ được tải vào Data Warehouse
Đôi khi, thứ tự có thể thay đổi một chút thành ELT (Extract – Load – Transform), tức là dữ liệu được tải vào Data Warehouse trước rồi mới thực hiện biến đổi tại đó. Cách tiếp cận này thường phổ biến hơn với các nền tảng dữ liệu hiện đại có khả năng xử lý mạnh mẽ.
Đặc điểm chính của Data Warehouse:
Loại dữ liệu: Chủ yếu lưu trữ dữ liệu có cấu trúc hoặc bán cấu trúc đã được xử lý, làm sạch và tích hợp từ nhiều nguồn khác nhau. Dữ liệu ở đây không còn ở dạng thô như lúc ban đầu.
Schema (Lược đồ/Cấu trúc): Vẫn sử dụng Schema-on-Write hoặc schema được định nghĩa trước, tương tự database truyền thống. Tuy nhiên, schema trong Data Warehouse thường được thiết kế theo mô hình hướng chủ đề (subject-oriented), tập trung vào các lĩnh vực kinh doanh cốt lõi như Bán hàng (Sales), Tiếp thị (Marketing), Tài chính (Finance), Nhân sự (HR)… thay vì theo cấu trúc của từng ứng dụng nguồn. Các mô hình phổ biến là Star Schema và Snowflake Schema, giúp tối ưu cho các truy vấn phân tích.
Mục đích chính: Phục vụ các hoạt động phân tích dữ liệu lịch sử, tạo báo cáo quản trị (BI reports), khám phá insight kinh doanh và hỗ trợ ra quyết định chiến lược (OLAP). Nó giúp trả lời các câu hỏi “Tại sao?” và “Điều gì sẽ xảy ra?”.
Người dùng: Người dùng chính là các Chuyên viên phân tích nghiệp vụ/kinh doanh (Business Analysts) , các nhà quản lý, lãnh đạo doanh nghiệp – những người cần các báo cáo tổng hợp, dashboard trực quan và những phân tích sâu sắc về tình hình hoạt động của công ty.
Tính chất dữ liệu:
Tích hợp (Integrated): Dữ liệu từ nhiều nguồn được hợp nhất và chuẩn hóa, tạo ra một bức tranh toàn cảnh.
Bất biến (Non-volatile): Dữ liệu trong Data Warehouse một khi đã được ghi vào thì rất hiếm khi bị cập nhật hay xóa đi. Thay vào đó, các dữ liệu mới (ví dụ: doanh số của ngày hôm qua) sẽ được nạp thêm vào. Điều này giúp lưu giữ lịch sử thay đổi của dữ liệu theo thời gian.
Gắn nhãn thời gian (Time-variant): Dữ liệu luôn được gắn với một yếu tố thời gian (ngày, tuần, tháng, quý, năm), cho phép các nhà phân tích xem xét và so sánh dữ liệu qua các khoảng thời gian khác nhau, từ đó nhận diện xu hướng và mẫu hình phát triển.
Thường là Read-only: Các nhà phân tích chủ yếu thực hiện các truy vấn đọc dữ liệu chứ không sửa đổi dữ liệu gốc trong kho.
Việc xây dựng Data Warehouse được xem là nỗ lực quan trọng đầu tiên của các doanh nghiệp nhằm phá vỡ các “ốc đảo dữ liệu” (data silos) và tạo ra một “nguồn sự thật duy nhất” (single source of truth) cho việc báo cáo và phân tích. Trước khi có Data Warehouse, việc tạo ra một báo cáo tổng hợp đáng tin cậy, ví dụ như kết hợp dữ liệu bán hàng từ hệ thống POS với dữ liệu chi phí marketing từ phòng Marketing và dữ liệu tồn kho từ bộ phận Logistics, là một công việc cực kỳ thủ công, tốn thời gian và dễ sai sót. Quy trình ETL tự động hóa việc tích hợp và làm sạch này, cung cấp cho các nhà phân tích một bộ dữ liệu tập trung, nhất quán và đáng tin cậy để họ có thể đặt ra những câu hỏi phức tạp hơn về hoạt động kinh doanh.
Data Lake (Hồ dữ liệu)
Data Warehouse đã giải quyết rất tốt bài toán tích hợp dữ liệu cho mục đích BI và báo cáo. Tuy nhiên, cùng với sự bùng nổ của Big Data và sự trỗi dậy của Khoa học dữ liệu (Data Science) và Học máy (Machine Learning – ML), Data Warehouse bắt đầu bộc lộ những hạn chế:
Kém linh hoạt: Cấu trúc dữ liệu (schema) phải được định nghĩa trước (Schema-on-Write) khiến việc thêm nguồn dữ liệu mới hoặc thay đổi cấu trúc trở nên tốn thời gian và công sức.
Khó xử lý dữ liệu phi cấu trúc: Data Warehouse được thiết kế chủ yếu cho dữ liệu có cấu trúc hoặc bán cấu trúc. Việc lưu trữ và phân tích các loại dữ liệu phi cấu trúc như văn bản (email, bài đăng mạng xã hội, đánh giá sản phẩm), hình ảnh, video, âm thanh, dữ liệu cảm biến (IoT)… là rất khó khăn hoặc không hiệu quả.
Chi phí lưu trữ cao: Việc lưu trữ một khối lượng dữ liệu khổng lồ, đặc biệt là dữ liệu lịch sử trong nhiều năm, trên các hệ thống Data Warehouse truyền thống có thể rất tốn kém.
Mất mát thông tin tiềm năng: Quá trình Transform trong ETL có thể làm mất đi một số chi tiết hoặc sắc thái trong dữ liệu gốc, mà những chi tiết này lại có thể hữu ích cho các mô hình Machine Learning hoặc các phân tích khám phá sâu hơn.
Để khắc phục những hạn chế này và đáp ứng nhu cầu mới của thời đại Big Data, khái niệm Data Lake (Hồ dữ liệu) đã xuất hiện.
Tương tự, Data Lake là một kho lưu trữ tập trung, có khả năng chứa mọi loại dữ liệu – từ dữ liệu có cấu trúc trong các bảng, dữ liệu bán cấu trúc như file JSON, XML, đến dữ liệu phi cấu trúc như text, ảnh, video – ở định dạng gốc (raw format) của chúng. Nó được thiết kế để xử lý khối lượng dữ liệu cực lớn (terabytes đến petabytes và hơn thế nữa) với chi phí lưu trữ thấp (thường tận dụng các dịch vụ lưu trữ đối tượng trên đám mây như Amazon S3, Google Cloud Storage).
Đặc điểm chính của Data Lake:
Loại dữ liệu: Chấp nhận tất cả các loại dữ liệu: structured, semi-structured, và unstructured. Đây là điểm khác biệt lớn nhất so với Database và Data Warehouse.
Schema (Lược đồ/Cấu trúc): Sử dụng cơ chế Schema-on-Read. Nghĩa là dữ liệu được đổ vào hồ mà không cần định nghĩa cấu trúc trước. Cấu trúc (schema) chỉ được áp dụng hoặc suy ra khi dữ liệu được đọc ra để phục vụ một mục đích phân tích cụ thể. Điều này mang lại sự linh hoạt tối đa, cho phép lưu trữ dữ liệu mới một cách nhanh chóng mà không cần lo lắng về việc phải thiết kế schema trước.
Mục đích chính:
Lưu trữ tập trung mọi loại dữ liệu của tổ chức với chi phí thấp và khả năng mở rộng cao.
Cung cấp “nguyên liệu thô” cho các hoạt động Khoa học dữ liệu (Data Science) và Học máy (Machine Learning), đặc biệt là huấn luyện các mô hình phức tạp đòi hỏi dữ liệu gốc, chưa qua xử lý nhiều.
Phục vụ các tác vụ phân tích khám phá (exploratory analysis), nơi các nhà khoa học dữ liệu muốn tự do tìm tòi, khám phá dữ liệu mà không bị giới hạn bởi một cấu trúc định sẵn.
Xử lý Big Data và các luồng dữ liệu thời gian thực (streaming data).
Người dùng: Người dùng chính của Data Lake là các Nhà khoa học dữ liệu (Data Scientists) và Kỹ sư dữ liệu (Data Engineers). Họ là những người cần truy cập vào dữ liệu gốc, đa dạng và có các công cụ để xử lý, phân tích dữ liệu này (ví dụ: Python với thư viện Pandas, Scikit-learn; R; Apache Spark…). Các Business Analysts cũng có thể sử dụng dữ liệu từ Data Lake, nhưng thường là sau khi dữ liệu đã được xử lý và đưa vào một lớp có cấu trúc hơn (ví dụ: thông qua Data Lakehouse hoặc các công cụ truy vấn SQL-on-Lake).
Lợi ích:
Linh hoạt (Flexibility): Có thể lưu trữ mọi loại dữ liệu mà không cần định nghĩa schema trước.
Chi phí thấp (Low Cost): Tận dụng các giải pháp lưu trữ rẻ tiền, đặc biệt là trên cloud.
Khả năng mở rộng (Scalability): Dễ dàng mở rộng dung lượng lưu trữ khi dữ liệu tăng lên.
Hỗ trợ đa dạng công cụ: Có thể sử dụng nhiều loại công cụ và framework phân tích khác nhau trên cùng một dữ liệu.
Thách thức – Nguy cơ “Đầm lầy dữ liệu” (Data Swamp): Chính sự linh hoạt của Data Lake cũng là con dao hai lưỡi. Nếu không có quy trình quản lý, kiểm soát chất lượng và tài liệu hóa (metadata management) tốt, Data Lake rất dễ biến thành một “đầm lầy dữ liệu” – một nơi chứa đầy dữ liệu không rõ nguồn gốc, chất lượng kém, trùng lặp, khó hiểu và không thể sử dụng được. Việc quản trị (governance) trong Data Lake là một thách thức lớn.
Sự ra đời của Data Lake đã thực sự trở thành yếu tố then chốt thúc đẩy sự phát triển mạnh mẽ của Khoa học dữ liệu và Học máy trong môi trường doanh nghiệp. Các mô hình ML phức tạp thường “thích” dữ liệu thô, nơi chúng có thể tự mình khám phá các đặc trưng (features) và mẫu hình mà con người có thể bỏ qua hoặc loại bỏ trong quá trình Transform của ETL vào Data Warehouse. Data Lake cung cấp chính xác nguồn “nguyên liệu” đa dạng, khổng lồ và linh hoạt đó, cho phép các Data Scientist tự do thử nghiệm, xây dựng và huấn luyện các mô hình dự đoán, phân loại, gợi ý… mà không bị giới hạn bởi cấu trúc cứng nhắc của Data Warehouse truyền thống.
So sánh nhanh: Data Lake, Database, Data Warehouse
Đặc điểm
Database (Cơ sở dữ liệu)
Data Warehouse (Kho dữ liệu)
Data Lake (Hồ dữ liệu)
Mục đích chính
Xử lý giao dịch trực tuyến (OLTP), hoạt động hàng ngày
Phân tích kinh doanh (BI), báo cáo quản trị, hỗ trợ ra quyết định (OLAP)
Lưu trữ mọi loại dữ liệu thô, Khoa học dữ liệu, Học máy, phân tích khám phá, Big Data
Loại dữ liệu
Chủ yếu có cấu trúc (Structured)
Có cấu trúc, bán cấu trúc (Đã được xử lý, tích hợp)
Tối ưu cho đọc/ghi/cập nhật/xóa nhanh các bản ghi nhỏ
Tối ưu cho các truy vấn phức tạp, quét lượng lớn dữ liệu lịch sử (ETL/ELT trước khi tải)
Lưu trữ dữ liệu thô, xử lý linh hoạt khi cần (Thường là ELT)
Người dùng chính
Kỹ sư Backend, Ứng dụng
Chuyên viên phân tích (Business Analysts), Nhà quản lý
Nhà khoa học dữ liệu (Data Scientists), Kỹ sư dữ liệu (Data Engineers)
Tốc độ truy vấn
Rất nhanh (cho giao dịch nhỏ)
Nhanh (cho truy vấn phân tích đã tối ưu)
Có thể chậm hơn (tùy công cụ và tối ưu hóa, ưu tiên lưu trữ rẻ)
Tính linh hoạt
Thấp
Trung bình (Khó thay đổi schema)
Cao
Chi phí lưu trữ
Tùy thuộc vào quy mô, có thể cao
Thường cao
Thấp (Thiết kế cho lưu trữ rẻ)
Ví dụ điển hình
Quản lý tài khoản người dùng, đơn hàng online
Phân tích doanh thu theo quý, báo cáo hiệu quả marketing
Huấn luyện mô hình gợi ý sản phẩm, phân tích sentiment từ mạng xã hội, xử lý dữ liệu IoT
Ai/Vai trò nào làm việc với các hệ thống thông tin này?
Để những dữ liệu phức tạp này có thể được xây dựng, vận hành trơn tru và thực sự mang lại giá trị cho tổ chức, không thể thiếu vai trò của những con người với các kỹ năng chuyên môn khác nhau. Sự phát triển của các kiến trúc dữ liệu cũng kéo theo sự chuyên môn hóa ngày càng cao của các vị trí công việc trong lĩnh vực này.
Backend Engineer (Kỹ sư Backend / Lập trình viên Backend): Đây là những người thường làm việc trực tiếp nhất với các Database truyền thống (OLTP). Nhiệm vụ chính của họ là xây dựng và duy trì phần “hậu trường” (backend) của các ứng dụng web hoặc mobile – chính là những ứng dụng tạo ra và tiêu thụ dữ liệu hàng ngày. Họ viết code để ứng dụng có thể đọc, ghi, cập nhật và xóa dữ liệu trong database một cách chính xác, hiệu quả và an toàn, đảm bảo các giao dịch OLTP diễn ra suôn sẻ.
Business Analyst (Chuyên viên phân tích nghiệp vụ / kinh doanh – BA): BA là những người dùng của Data Warehouse. Họ là cầu nối giữa bộ phận kinh doanh và kỹ thuật. Sử dụng dữ liệu đã được làm sạch và tích hợp trong Data Warehouse, BA thực hiện các phân tích, tạo ra các báo cáo trực quan (dashboards), khám phá các xu hướng kinh doanh và cung cấp những insight giá trị giúp ban lãnh đạo đưa ra các quyết định sáng suốt hơn (hoạt động Business Intelligence – BI). Họ cần hiểu rõ về nghiệp vụ kinh doanh và có kỹ năng sử dụng các công cụ BI như Tableau, Power BI cũng như SQL để truy vấn dữ liệu.
Data Scientist (Nhà khoa học dữ liệu): Đây là những “nhà thám hiểm” của thế giới dữ liệu, thường làm việc nhiều nhất với Data Lake (nhưng cũng có thể khai thác cả Data Warehouse). Công việc của họ không chỉ dừng lại ở việc phân tích dữ liệu quá khứ mà còn đi sâu vào việc khám phá dữ liệu thô, áp dụng các thuật toán thống kê và Machine Learning (Học máy) để xây dựng các mô hình dự đoán, phân loại, phát hiện bất thường, hoặc tìm ra những insight hoàn toàn mới lạ mà con người khó nhận biết. Họ cũng có thể tạo ra các tính năng sản phẩm dựa trên dữ liệu, ví dụ như hệ thống gợi ý sản phẩm “người mua X cũng mua Y” trên các trang thương mại điện tử. Họ cần nền tảng vững chắc về toán, thống kê, lập trình (thường là Python hoặc R) và kiến thức về các thuật toán ML.
Data Engineer (Kỹ sư dữ liệu): Có thể nói Data Engineer là những người “thợ xây” chính của toàn bộ hạ tầng dữ liệu. Vai trò của họ cực kỳ quan trọng, đảm bảo “dòng chảy” dữ liệu được thông suốt và hiệu quả. Họ chịu trách nhiệm thiết kế, xây dựng, kiểm thử và bảo trì các “đường ống” ( dữ liệu (data pipelines), bao gồm cả việc triển khai quy trình ETL/ELT để di chuyển và biến đổi dữ liệu từ các hệ thống nguồn (OLTP) vào Data Warehouse hoặc Data Lake. Họ đảm bảo dữ liệu luôn sẵn sàng, đáng tin cậy và có thể truy cập được cho các Data Analyst và Data Scientist sử dụng. Kỹ năng cần thiết bao gồm lập trình, hiểu biết về các hệ thống database, data warehouse, data lake, các công cụ ETL và các nền tảng Big Data.
Analytics Engineer (Kỹ sư phân tích): Đây là một vai trò tương đối mới nhưng đang ngày càng trở nên quan trọng, đặc biệt trong bối cảnh của Modern Data Stack (Ngăn xếp dữ liệu hiện đại). Analytics Engineer hoạt động ở lớp trung gian, nối liền khoảng cách giữa Data Engineer và Data Analyst/Scientist. Trong khi Data Engineer tập trung vào việc xây dựng hạ tầng và đưa dữ liệu thô vào Lake/Warehouse, Analytics Engineer lại tập trung vào việc biến đổi (transform) dữ liệu thô đó thành những bộ dữ liệu (data models) sạch sẽ, có cấu trúc tốt, dễ hiểu, đáng tin cậy và được tối ưu hóa cho mục đích phân tích và báo cáo BI. Họ thường là những người rất giỏi SQL, thành thạo các công cụ mô hình hóa dữ liệu như dbt (data build tool), và có hiểu biết tốt về nhu cầu phân tích của nghiệp vụ.
Một số xu hướng mới
Data Lakehouse: Như mình đã nhắc đến ở phần so sánh, Data Lakehouse là một kiến trúc lai đầy hứa hẹn, đang thu hút nhiều sự chú ý. Mục tiêu của nó là kết hợp những ưu điểm tốt nhất của cả Data Warehouse và Data Lake vào một nền tảng duy nhất. Cụ thể, nó cố gắng mang lại sự linh hoạt và chi phí thấp của Data Lake (lưu trữ mọi loại dữ liệu trên bộ nhớ rẻ tiền như object storage) cùng với các tính năng quản lý dữ liệu mạnh mẽ của Data Warehouse như cấu trúc dữ liệu (schema enforcement), đảm bảo tính toàn vẹn giao dịch (ACID transactions), quản trị dữ liệu (data governance) và hiệu năng truy vấn SQL cao. Bằng cách này, Data Lakehouse hướng tới việc đơn giản hóa kiến trúc dữ liệu tổng thể, giảm thiểu việc sao chép dữ liệu giữa Lake và Warehouse, và cho phép nhiều loại workload khác nhau (từ BI, SQL analytics đến Data Science, ML) cùng hoạt động hiệu quả trên một bản sao dữ liệu duy nhất.
DataOps: Lấy cảm hứng từ thành công của DevOps trong lĩnh vực phát triển phần mềm, DataOps ra đời với mục tiêu áp dụng các nguyên tắc tương tự vào toàn bộ vòng đời của dữ liệu, từ khâu thu thập, xử lý, đến phân tích và cung cấp insight. DataOps nhấn mạnh vào Tự động hóa (Automation) các quy trình dữ liệu (như kiểm thử, triển khai pipeline), Giám sát (Monitoring) liên tục chất lượng và hiệu năng dữ liệu, và thúc đẩy Hợp tác (Collaboration) chặt chẽ giữa các nhóm liên quan (Data Engineers, Analysts, Scientists, nghiệp vụ). Mục tiêu cuối cùng của DataOps là tăng tốc độ đưa dữ liệu và insight đến người dùng, cải thiện chất lượng và độ tin cậy của dữ liệu, đồng thời giảm thiểu lỗi và các công việc thủ công lặp đi lặp lại.
Reverse ETL: Nếu như ETL/ELT truyền thống tập trung vào việc đưa dữ liệu VÀO Data Warehouse hoặc Data Lake để phân tích, thì Reverse ETL lại làm điều ngược lại. Nó lấy những dữ liệu đã được xử lý, làm giàu, hoặc những insight giá trị (ví dụ: điểm số khách hàng tiềm năng, phân khúc khách hàng, dự đoán churn rate) từ chính Data Warehouse/Lakehouse và đẩy chúng TRỞ LẠI các hệ thống hoạt động hàng ngày mà các bộ phận nghiệp vụ thường xuyên sử dụng, như hệ thống CRM (Quản lý quan hệ khách hàng), công cụ Marketing Automation, nền tảng quảng cáo, hay công cụ hỗ trợ khách hàng. Việc này giúp “kích hoạt” dữ liệu phân tích, biến insight thành hành động cụ thể một cách nhanh chóng và tự động, giúp các nhóm Sales, Marketing, Customer Success… có thể cá nhân hóa tương tác, tối ưu chiến dịch và cải thiện trải nghiệm khách hàng dựa trên dữ liệu cập nhật nhất.
Data Products & Data Mesh: Đây là một xu hướng mang tính chiến lược và tổ chức hơn là chỉ về công nghệ. Nó đề xuất một cách tiếp cận mới trong việc quản lý và chia sẻ dữ liệu trong các tổ chức lớn, đặc biệt là những nơi có nhiều bộ phận nghiệp vụ (domains) khác nhau. Thay vì tập trung tất cả dữ liệu và đội ngũ data vào một nhóm trung tâm (thường dẫn đến tắc nghẽn), kiến trúc Data Mesh đề xuất phân tán quyền sở hữu và trách nhiệm quản lý dữ liệu về cho chính các nhóm nghiệp vụ (domain teams) – những người hiểu rõ nhất về dữ liệu của mình. Mỗi domain team sẽ chịu trách nhiệm biến dữ liệu của họ thành các “Sản phẩm dữ liệu” (Data Products). Một Data Product không chỉ là dữ liệu thô, mà là một đơn vị logic hoàn chỉnh, bao gồm dữ liệu đã được làm sạch, mô hình hóa, có tài liệu rõ ràng, đáng tin cậy, dễ khám phá, dễ truy cập (thông qua các cổng ra – output ports được định nghĩa tốt) và có chủ sở hữu chịu trách nhiệm về chất lượng. Cách tiếp cận này nhằm mục đích tăng cường khả năng mở rộng, tính linh hoạt, chất lượng dữ liệu và giúp dữ liệu thực sự trở thành tài sản được quản lý tốt như các sản phẩm phần mềm khác trong tổ chức.
Để tổng kết lại một cách ngắn gọn nhất, bạn có thể nhớ:
Database (Cơ sở dữ liệu): Lựa chọn hàng đầu cho các hoạt động hàng ngày, cần xử lý giao dịch nhanh chóng, đáng tin cậy với dữ liệu có cấu trúc. Nó là trái tim của các ứng dụng OLTP.
Data Warehouse (Kho dữ liệu): Ngôi nhà lý tưởng cho việc phân tích kinh doanh (BI), tạo báo cáo quản trị, khám phá insight từ dữ liệu lịch sử đã được tích hợp và làm sạch, chủ yếu là dữ liệu có cấu trúc. Nó là trung tâm của các hệ thống OLAP.
Data Lake (Hồ dữ liệu): Giải pháp tối ưu khi bạn cần lưu trữ mọi loại dữ liệu (bao gồm cả phi cấu trúc) ở dạng thô, với chi phí thấp và quy mô lớn, phục vụ cho các nhu cầu phân tích nâng cao, Khoa học dữ liệu và Học máy.
Camunda 7 is a powerful, open-source platform designed for workflow and process automation. It offers a comprehensive suite of tools for managing business processes, including capabilities for modeling, automating, and optimizing workflows.
Before Camunda BPM 7.18
In both approaches, you need to register your method via the function mapper. If you want to call a method from a bean using an expression like ${myBean.method()}, the process happens automatically without any additional code. However, if you prefer to use the method in the format ${runner:method()} or ${method()}, here’s how you can do it.
In the ProcessEngineConfigurationImpl class provided by Camunda, you’ll find the ExpressionManager. This class exposes all the necessary methods for you to use.
To register a new function mapper, you need to create a class that implements the FunctionMapper interface. Here’s a sample implementation to get you started.
Now, you can create a process engine plugin to initialize the function mapper when Camunda’s process engine starts configuring.
Now you can use ${jsonPath(a, b) in workflow
After Camunda BPM 7.18
After Camunda 7.18, the addFunctionMapper function was removed. You might need an alternative workaround, and this link might be helpful for you.
Camunda 7 is a powerful, open-source platform designed for workflow and process automation. It offers a comprehensive suite of tools for managing business processes, including capabilities for modeling, automating, and optimizing workflows. One of the key features of Camunda is its ability to track and retrieve historical data, such as process variables and form data, from completed process instances. However, retrieving form data from completed instances can be tricky as Camunda 7 does not provide an out-of-the-box API for this purpose.
Retrieving Historical Data
When working with Camunda, it’s often necessary to access historical data from completed process instances. This data can include process variables and form data, which provide insights into the execution and outcomes of the process. Retrieving this data is essential for reporting, auditing, and analysis purposes.
In this solution:
The BPMN model instance is retrieved using the repositoryService.
The task is fetched from the BPMN model using the task definition key.
If the task is found, the form key is retrieved from the task attributes.
The deployment ID is obtained from the process definition query.
The form resource is retrieved using the GetDeploymentResourceCmd.
The form data is read into a map and set into the result DTO as a JSON string.
Accessing Historical Form Data
By default, Camunda 7 does not provide an out-of-the-box API to retrieve form data from completed process instances. To work around this limitation, you can use a custom approach to fetch the form data stored as process variables.
Camunda is a popular open-source platform for workflow and process automation. It enables developers to model, automate, and optimize business processes. Camunda integrates well with Java applications, offering robust capabilities for managing complex workflows and business rules.
Understanding the Error
When performing integration testing in Camunda, you might encounter the following error:
org.camunda.bpm.engine.exception.NullValueException: No startFormHandler defined in process 'SalesOrderProcess_v2:1:cee4056f-a1d8-11eb-9f61-6683657f7a30': startFormHandler is null
This error occurs because the start event of the process expects a form handler to be defined, but none is provided. The startFormHandler is responsible for managing the form associated with the start event. When it is not defined, Camunda throws a NullValueException. This problem likely cause because of, usually camunda will have BpmnParser class that when reading the XML bpmn file, it will construct the event handler. But in your test, you not inject it
Solution: Mocking the Start Event Handler
To resolve this issue, you can mock the start event handler in your integration tests. The following example demonstrates how to set up a mock start event handler to bypass the error and proceed with the testing.
Truncate table to make sure it clean state before test
Ở một số trang web, bạn sẽ thấy, bạn không thể đi đến thẳng trang cuối, hoặc nhảy đến 1 trang bất kì, mà thông thường sẽ có nút để sang trang kế và trang phía trước. Như vậy ta có thể assume rằng:
Người dùng sẽ chỉ mở trang 10 sau khi mở trang 9.Vậy, ta chỉ cần nhớ vị trí cuối cùng của bài viết ở trang 9 là ở id bao nhiêu, rồi dùng WHERE để truy vấn từ điểm đó, chứ không cần bỏ dần để đi đến điểm đó nữa
SELECT * FROM my_table WHERE id > 21 ORDER BY id LIMIT 5
Tối ưu truy vấn Pagination (phân trang) sử dụng Spring Boot (Java) 38
Ví dụ: Sort theo thời gian
SELECT *
FROM my_table
WHERE (update_date = '2017-12-21' AND id > 21)
OR update_date > '2017-12-21'
ORDER BY update_date,id LIMIT 5
Cơ bản là ta sẽ chỉ lấy các bài viết có cùng thời gian đăng như bài viết cuối và id > , hoặc thời gian lớn hơn bài viết cuối
Để hình dung rõ hơn, hãy thử nhìn vào bảng sau:
ID
Name
1
An
2
Nam
3
Quan
4
Tien
5
Hoang
6
Nguyen
7
Duc
8
Thanh
9
Hai
10
Minh
Ta mong muốn nhận được dữ liệu ở Page 1 như sau
ID
Name
1
An
2
Nam
3
Quan
4
Tien
5
Hoang
Ta sẽ có câu query để lấy page 2 dạng như sau
Lấy page 1: SELECT * FROM users ORDER BY ID LIMIT 5
SELECT * FROM users WHERE ID > '5' ORDER BY ID LIMIT 5
Ta có thể thêm HAL bằng Spring HateOAS để response trả về ở dạng như sau, giúp ta lấy dữ liệu dễ dàng hơn
Cursor sẽ như một con trỏ chỉ tới một record nào đó trong dữ liệu, và khi ta truyền cursor đó vào, backend cần biết ta đang muốn lấy trang tiếp theo hay trang trước từ vị trí con trỏ đó
Có nhiều cách để làm điều này, ví dụ thêm prefix dạng như prev___, hay next___ như ví dụ trên
Hoặc đơn giản hơn, ta sẽ truyền thêm một biến chỉ ra là đó là next hay prev
Null Cursor
Lúc này, từ phía backend, ta có thể sử dụng count hoặc tương tự để kiểm tra xem có trang ở trước đó hay ở sau đó không, rồi trả response ra là null để thể hiện rằng không còn trang nào khác để đi tới ở hướng đó.
Next Cursor
Có thể bạn đã nắm được về cách để đi tới trang tiếp theo với cursor pagination.
Ví dụ, ta có một danh sách id tăng dần, thì từ trang có id [1,2,3,4,5], ta sẽ where id > 5 để tới trang tiếp theo
SELECT * FROM entries WHERE (myCol > 5) ORDER BY myCol ASC LIMIT 2; -> 6,7
SELECT * FROM entries WHERE (myCol > 7) ORDER BY myCol ASC LIMIT 2; -> 8,9
Với một danh sách id giảm dần, ví dụ trang là [10,9,8,7,6] thì ta sẽ where id < 6 để tới trang tiếp theo.
SELECT * FROM entries WHERE (myCol < 5) ORDER BY myCol DESC LIMIT 2; -> 4,3
SELECT * FROM entries WHERE (myCol < 3) ORDER BY myCol DESC LIMIT 2; -> 2,1
Previous Cursor
Tuy nhiên, việc đi tới trang trước đó trở lên khó khăn hơn. Bởi vì dữ liệu sẽ được trả ra từ phía trái đầu
Ví dụ, ta đang ở trang id [8,9] , và ta thực hiện truy vấn lấy trang trước đó, 2 phần tử
SELECT * FROM entries WHERE (myCol < 8) ORDER BY myCol ASC LIMIT 2;
Kết quả sẽ trả về 0,1. Bởi vì nó đang đọc từ trái sang, và khi limit sẽ lấy từ đầu trái chứ không lấy 6,7
Cách giải quyết lấy previous cursor
Cách để giải quyết vấn đề này, ta sẽ đảo ngược chiều order by, và sau đó khi limit để lấy đúng vùng dữ liệu xong, ta sẽ đảo một lần nữa về chiều đúng
SELECT pagination.* FROM(SELECT * FROM entries WHERE (myCol < 8) ORDER BY myCol DESC LIMIT 2) AS pagination ORDER BY myCol ASC;
Các bước vừa xảy ra
1. Đầu tiên, select các row có id < 8 và order by desc. ta có 7,6,…,1,0
Sau đó limit 2 để lấy 7,6
Rồi order by asc lần nữa để lấy thành 6,7 . Là thứ tự ASC như ban đầu ta muốn
Time Complexity
Next Page Travesal: O(log(N) + L) , ta mấy log(N) để tìm tới điểm bất kì vì nó thực hiện như binary search và ta đã index database. Và L là Limit là số ta phải lặp đến để lấy vùng đầu tiên đã chọn
Previous Page Traversal: O(log(N) + 2L), sở dĩ là 2L vì ta đã phải đảo chiều 1 lần.
Thông thường limit được đặt ở 5-20 cho 1 trang, vì vậy có thể coi L là constant và không đáng kể
Triển khai trong code
Ta có một class thực hiện việc lấy cursor trước hoặc sau. Sử dụng để decode một giá trị cursor base64 hoặc encode 1 giá trị
Ví dụ ta đang cursor pagination theo column 35, ta sẽ có hàm getEncodeCursor để tạo ra giá trị mã hoá cursor
Hàm getDecodedCursor sẽ chuyển giá trị mã hoá đó thành số để dùng paging
Đây là class thực hiện việc paging và nhảy trang. Có thể thấy logic sẽ kiểm tra xem ta đang lấy trang trước hay trang sau, sau đó sort cho phù hợp.
Class xử lý việc trả về. Tại đây ta sẽ lấy ra element tại đầu list và cuối list của trang hiện tại, rồi tìm thử xem phía trước đó còn trang nào không, hay sau đó còn trang nào hay không
1. Làm quen với Exception: Checked và Unchecked Exception, Error
1.1 Lời mở đầu
Trong cuộc sống, không phải lúc nào mọi việc cũng diễn ra suôn sẻ, không phải lúc nào mọi thứ cũng theo đúng kế hoạch. Đôi khi, có những tình huống không mong muốn xảy ra, và chúng ta cần phải xử lý chúng.
Khi một ứng dụng ngày càng lớn lên, chắc chắn ta không thể đảm bảo ứng dụng luôn hoạt động như cách ta muốn 100% mọi lúc được. Đến lúc nào đó, sẽ có lỗi xảy ra.
Ví dụ:
Khi một ứng dụng cần đọc một file từ ổ cứng, nhưng file đó không tồn tại.
Khi ứng dụng cần quyền chụp ảnh từ camera, nhưng người dùng không cho phép.
Khi ứng dụng kết nối tới một server, nhưng server đang bị mất điện và không kết nối được.
Khi đó, hệ thống cần có những cách xử lý riêng cho người dùng biết vấn đề đang xảy ra, và có những cách xử lý riêng cho hệ thống có thể tiếp tục hoạt động mà không bị ảnh hưởng quá nhiều.
Ví dụ:
Khi file không tồn tại, ứng dụng cần thông báo cho người dùng biết rằng file không tồn tại và yêu cầu người dùng chọn file khác, hoặc kiểm tra lại đường dẫn file.
Khi người dùng không cho phép ứng dụng chụp ảnh, ứng dụng cần thông báo cho người dùng biết rằng không thể chụp ảnh và yêu cầu người dùng cấp quyền.
Khi server bị mất điện, ứng dụng cần thông báo cho người dùng biết rằng không thể kết nối tới server và yêu cầu người dùng thử lại sau, hoặc kiểm tra xem đường truyền wifi, 3G, 4G có vấn đề gì không.
1.2 Exception là gì?
Exception dịch ra tiếng Việt có nghĩa là ngoại lệ. Exception là một sự kiện xảy ra trong quá trình thực thi chương trình, làm ảnh hưởng đến luồng thực thi của chương trình.
1.3 Checked và Unchecked Exception
1.3.1 Checked Exception
Ảnh ví dụ: Khi ta thử đọc 1 file, IDE ngay lập tức thông báo có thể xảy ra FileNotFoundException (Ngoại lệ file không tồn tại) và yêu cầu ta phải xử lý nó.
Checked Exception thường là những lỗi phổ biến mà chúng ta có thể dự đoán được sẽ xảy ra cho một thao tác nào đó.
Ví dụ, khi đi dã ngoại, để phòng ngừa việc có mưa, ta mang theo áo mưa. Việc trời mưa là một ngoại lệ mà ta đã dự đoán trước và lên trước kế hoạch để xử lý nó.
1.3.2 Unchecked Exception
Đây là những Exception không được kiểm tra tại thời điểm biên dịch (compile time). Tức là chúng ta không nhận được thông báo lỗi từ trình biên dịch khi mà nó phát hiện ra rằng chúng ta chưa xử lý nó.
Đây là các exception khá nguy hiểm, khi mà chúng ta không thể dự đoán được chúng sẽ xảy ra khi nào. Bởi vì Exception này xảy ra khi ứng dụng đang thực thi, nên nó còn có thể gọi là Runtime Exception.
Ví dụ, khi một ứng dụng chia một số cho 0, nó sẽ ném ra một ngoại lệ ArithmeticException (Ngoại lệ toán học).
Trong đoạn code trên, nếu ta nhập vào 1 số không tồn tại trong mảng, nó sẽ bị một Exception ArrayIndexOutOfBoundsException (Ngoại lệ vượt quá giới hạn của mảng)., đây là một ví dụ về Unchecked Exception. Vì ta không thể biết được trước khi chạy chương trình, nó sẽ xảy ra khi nào.
1.4 Error
Error là một dạng ngoại lệ mà chúng ta không thể xử lý được. Error thường xảy ra khi mà hệ thống gặp phải những vấn đề nghiêm trọng, không thể khắc phục được.
Ví dụ, khi một ứng dụng chạy hết bộ nhớ, nó sẽ ném ra một ngoại lệ OutOfMemoryError (Lỗi hết bộ nhớ). Đây là một ví dụ về Error. Đây là một ngoại lệ ta không thể xử lý được, và nó thường xảy ra khi mà hệ thống gặp phải những vấn đề nghiêm trọng, không thể khắc phục được.
2. Bắt Exception với try-catch
Bài tập
Hãy viết chương trình nhập vào 1 String, và sử dụng Integer.parseInt để chuyển đổi String đó sang kiểu int. Thử nếu nhập vào 1 chuỗi không phải là số, xem chương trình sẽ báo lỗi như thế nào.
Khi chạy chương trình trên, ta sẽ nhận được một ngoại lệ NumberFormatException (Ngoại lệ không đúng định dạng số). Đây là một ví dụ về Unchecked Exception. Vì lúc compile, ta không nhận lỗi nào báo trước cả, do IDE không biết trước được String truyền vào khi chạy chương trình
2.1 Cách bắt Exception
Để bắt Exception, ta sử dụng cấu trúc try-catch. Cấu trúc try-catch sẽ giúp chúng ta bắt ngoại lệ, và xử lý nó một cách an toàn.
Cấu trúc try-catch có dạng như sau:
Trong đó:
try: Là một khối lệnh, chứa các câu lệnh có thể ném ra ngoại lệ.
catch: Là một khối lệnh, chứa các câu lệnh xử lý ngoại lệ. Trong đó, Exception e là một biến, chứa thông tin về ngoại lệ xảy ra.
2.2 Ví dụ
Ví dụ, ta sẽ sử dụng cấu trúc try-catch để bắt ngoại lệ NumberFormatException khi chuyển đổi một chuỗi không phải là số sang kiểu int.
Khi chạy chương trình trên, ta sẽ nhận được thông báo “Chuỗi không phải là số” khi chuyển đổi chuỗi “4.5a” sang kiểu float.
2.3 catch nhiều Exception
Ta cũng có thể bắt nhiều ngoại lệ trong cùng một cấu trúc try-catch, bằng cách sử dụng nhiều khối catch.
Bài tập
Hãy viết chương trình nhập vào số n, sau đó nhập vào n string. Sau đó nhập vào 1 số m, và in ra string thứ m. Nếu truy cập ngoài mảng, hãy in ra thông báo “Truy cập ngoài mảng”, còn nếu không in ra số được chuyển đổi từ string đó sang kiểu int, nếu không thể chuyển đổi được, hãy in ra thông báo “Chuỗi không phải là số”.\
Như ví dụ trên, ta thấy ta có thể catch nhiều Exception trong cùng một cấu trúc try-catch.
2.4 Catch Exception cha
Ta cũng có thể bắt Exception cha của một Exception con. Ví dụ, ta có thể bắt Exception cha của NumberFormatException là Exception.
Bài tập
Tương tự bài tập trên, nhưng chỉ dùng 1 catch, và có bất kì lỗi gì xảy ra ta đều in ra “Lỗi xảy ra + tên lỗi”.
Như ví dụ trên, ta thấy ta có thể bắt Exception cha của NumberFormatException là Exception.
3. Sử dụng finally
3.1 finally là gì?
finally là một khối lệnh, chứa các câu lệnh sẽ được thực thi sau khi khối lệnh try-catch kết thúc.
finally sẽ được thực thi sau khi khối lệnh try-catch kết thúc, bất kể có ngoại lệ xảy ra hay không.
3.2 Ví dụ
Ví dụ, ta sẽ sử dụng finally để đóng Scanner sau khi sử dụng xong.
Trong đoạn code trên, ta sẽ đóng Scanner sau khi sử dụng xong, bất kể có ngoại lệ xảy ra hay không.
Tác dụng
finally thường được sử dụng để giải phóng tài nguyên, như đóng file, đóng kết nối, đóng Scanner, … Đây là một practice nên làm đó là giải phóng tài nguyên sau khi sử dụng xong, tránh lãng phí tài nguyên.
3.3 Một số câu hỏi
3.3.1 Tại sao không sử dụng Exception là xong mà cần catch Exception cụ thể, có nhiều class Exception con
Khi sử dụng Exception là xong, ta sẽ bắt được tất cả các Exception, nhưng đôi khi ta cần xử lý một cách cụ thể cho từng Exception. Vì khi có lỗi xảy ra, cứ chỉ báo là đã có lỗi xảy ra thì thứ nhất người dùng rất khó chịu, ta khi sửa lỗi cũng không biết lỗi xảy ra là gì, ở đâu. Thứ hai, ta cũng không thể xử lý lỗi một cách cụ thể.
Việc catch đúng Exception cụ thể sẽ giúp ta xử lý lỗi một cách cụ thể. Ví dụ: Nếu lỗi là chia cho 0, ta báo cho người dùng rằng họ nhập sai để họ biết sai ở đâu và sửa lại, còn nếu lỗi là không tìm thấy file, ta báo cho người dùng biết rằng file không tồn tại để họ biết cần phải tạo file mới. Hoặc nếu là các lỗi người dùng không hiểu được, ta cần báo các lỗi cụ thể, có định danh rõ ràng để lập trình viên có thể biết và sửa lỗi
Ví dụ, trong window đôi lúc ta bị màn hình xanh, ta có các mã lỗi kiểu như 0x0000000A, 0x0000000B, … những mã lỗi này không phục vụ cho người dùng hiểu, nhưng người dùng có thể dùng nó để báo cáo, và lập trình viên có thể dùng nó để sửa lỗi.
3.3.2 Có thể Try không có Catch không?
Có thể, nhưng ta cần có một khối finally để giải phóng tài nguyên.
Ví dụ, ta có thể sử dụng try-finally để giải phóng tài nguyên, mà không cần catch Exception.
4. Cây phân cấp Exception
Trong Java, Exception được phân cấp theo một cấu trúc cây, với Throwable là gốc của cây, Error và Exception là 2 nhánh con của Throwable.
Interface Iterable nhằm ám chỉ những class bên dưới kế thừa có tính “có thể ném ra được”
Lí do tại sao cần 1 cây Exception sẽ được giải thích bên dưới, mục câu hỏi tại sao không catch mọi exception
4.1 Throw, tung ra ngoại lệ
Đến hiện tại, ta đã biết các Exception có sẵn trong Java, và cách bắt nó bằng cấu trúc try-catch.
Ta nhận thấy, các Exception đều triển khai từ Throwable, và có thể tung ra (throw) một Exception bằng cách sử dụng từ khóa throw.
Ví dụ: Ta cần nhập vào một số là tuổi của người dùng, ta sử dụng nextInt của Scanner để nhập vào, nhưng nếu người dùng nhập vào một chuỗi không phải là số, ta sẽ tung ra một Exception InputMismatchException (Ngoại lệ nhập không đúng).
Tuy nhiên, người dùng vẫn có thể nhập vào 1 số âm, lúc này ta có thể thêm các điều kiện của riêng mình và throw ra Exception nếu muốn
4.2 Throws
throws là một từ khóa, dùng để khai báo một phương thức có thể ném ra một ngoại lệ.
Ví dụ: Ta chia 1 hàm riêng để nhập vào thông tin nhân viên, nếu nhập sai ta sẽ trả ra 1 số Exception như InputMismatchException, NumberFormatException, …. Tuy nhiên ta muốn nơi gọi đến hàm này tự xử lý ngoại lệ, ta có thể sử dụng throws để khai báo phương thức có thể ném ra ngoại lệ.
Như code trên, ở hàm inputInfo ta sử dụng throws để khai báo phương thức có thể ném ra ngoại lệ. Lúc này ở nơi gọi đến hàm inputInfo sẽ bị yêu cầu xử lý ngoại lệ InputMismatchException mà ta đã khai báo.
Như ảnh trên, ta thấy ở bên phải là luồng gọi hàm, và bên trái là luồng Exception được quay ngược lên trên để tìm nơi xử lý nó.
4.3 Tạo ra Exception của riêng mình
Đôi khi, ta cần tạo ra Exception của riêng mình, để phục vụ cho mục đích của riêng mình.
Tên Throwable có nghĩa là “có thể ném ra”, nghĩa là các Exception bên dưới của chúng ta có thể sử dụng các từ khoá throws, throw để ném ra.
Để tạo ra Exception của riêng mình, ta cần tạo ra một class kế thừa từ Exception hoặc RuntimeException. Để tạo 1 Checked Exception, ta kế thừa từ Exception, còn để tạo 1 Unchecked Exception, ta kế thừa từ RuntimeException.
Ví dụ, ta cần tạo ra 1 Exception AgeException (Ngoại lệ tuổi), nếu tuổi nhập vào nhỏ hơn 0, ta sẽ ném ra Exception này.
Ví dụ, ta sẽ sử dụng AgeException để ném ra Exception khi tuổi nhập vào nhỏ hơn 0.
Như code trên, ta sử dụng AgeException để ném ra Exception khi tuổi nhập vào nhỏ hơn 0.
Khi nào dùng interface, khi nào dùng abstract class?
Bài tập về interface và trừu tượng
Interface là gì?
Interface là một khái niệm trừu tượng, nó chỉ ra những gì mà một đối tượng có thể làm, nhưng không nói ra làm thế nào để đối tượng đó làm được điều đó.
Ví dụ cú pháp khai báo interface:
Trong ví dụ trên, interface Animal chỉ ra rằng một Animal có thể làm được 2 việc là ăn và di chuyển, nhưng không nói ra làm thế nào để ăn và di chuyển.
Lúc này, ví dụ ta có 1 class Dog và Cat thì ta có thể implement interface Animal vào 2 class này như sau:
Lúc này, ta có thể khởi tạo 2 đối tượng Dog và Cat và gọi các phương thức eat() và travel() mà không cần quan tâm đến cách thức của 2 phương thức này.
Kết quả khi chạy chương trình:
Dog is eating
Dog is travelling
Cat is eating
Cat is travelling
Take away | Tổng hợp
Như vậy, interface có thể nhìn nhận như 1 bản hợp đồng, tôi quy định trước, tôi chỉ định nghĩa là, các class implement interface này phải làm được những gì tôi quy định.
Như vậy, khi bên khác implement interface này, bên đó phải làm theo những gì mà interface quy định, nếu không làm theo, bên đó sẽ bị lỗi compile.
Còn nếu bên nào sử dụng interface, ta có thể khởi tạo đối tượng của class implement interface đó, và gọi các phương thức, và biết chắc phương thức tồn tại và có thể gọi được ở class đó, vì class đó đã implement interface đó.
Abstract class
Abstract Class – Lớp trừu tượng, là 1 lớp trong Java, nhưng có thể chứa cả các phương thức trừu tượng (abstract method) và các phương thức thường (non-abstract method).
Abstract Class không thể khởi tạo đối tượng, nhưng có thể khai báo biến kiểu Abstract Class, và khởi tạo đối tượng của class con của Abstract Class.
Ví dụ:
Kết quả khi chạy chương trình:
Dog is eating
Animal is travelling
Cat is eating
Animal is travelling
Take away | Tổng hợp
Như vậy, Abstract Class nhìn chung giống Class, nhưng mà có thể tạo thêm phương thức trừu tượng, các class kế thừa từ Abstract Class này phải implement các phương thức trừu tượng này, nếu không sẽ bị lỗi compile.
Các phương thức trừu tượng này, có thể hiểu là các phương thức mà Abstract Class này quy định, các class kế thừa từ Abstract Class này phải có, và phải implement các phương thức này.
Các phương thức thường (non-abstract method) thì không cần implement, vì các class kế thừa từ Abstract Class này đã có sẵn các phương thức này.
Abstract Class thì không thể khởi tạo đối tượng, nhưng có thể khai báo biến kiểu Abstract Class, và khởi tạo đối tượng của class con của Abstract Class.
Ví dụ
Lí do, vì abstract class hay interface chứa những hàm trừu tượng, những hàm chỉ định nghĩa, chưa có logic và giao việc triển khai logic cho các nơi ghi đè nó, nên không thể khởi tạo đối tượng từ abstract class hay interface được. (Vì nó chưa có logic, chưa có gì để chạy cả)
So sánh Interface và Abstract Class
Interface
Abstract Class
Interface chỉ có thể chứa các phương thức trừu tượng
Abstract Class có thể chứa cả các phương thức trừu tượng và các phương thức thường
Interface không thể khởi tạo đối tượng
Abstract Class không thể khởi tạo đối tượng
Biến trong interface mặc định là public static final
Biến trong abstract class không có mặc định gì cả
Interface không có constructor
Abstract Class có constructor
Interface không có gì để ghi đè
Abstract Class có thể có các phương thức để ghi đè
Interface có thể kế thừa nhiều interface khác
Abstract Class chỉ có thể kế thừa 1 abstract class khác
Các class implement interface có thể implement nhiều interface khác
Các class kế thừa từ abstract class chỉ có thể kế thừa 1 abstract class khác
Note: Sau này interface có thể có các phương thức thường, nhưng mà vẫn không thể khởi tạo đối tượng từ interface được.
Về biến trong interface, mặc định là public static final, nên khi khai báo biến trong interface, không cần khai báo public static final nữa. Tức là các biến trong interface là tồn tại duy nhất, không đổi được, ở dạng static.
Về biến trong abstract class, không có mặc định gì cả, nên có thể tạo các “biến thường”, tức là mỗi object tạo ra bởi 1 class chứa giá trị khác nhau.
Special Question: Diamond Problem
Diamond Problem là gì?
Diamond Problem là một vấn đề xảy ra khi đa kế thừa, tức là nếu 1 class kế thừa từ 2 nơi có trùng thứ gì đó, thì nó làm sao biết nên dùng cái nào ???
Mặc định thì, Java không cho phép kế thừa từ 2 class, nhưng nếu kế thừa từ 2 interface thì được. (Vì interface chỉ chứa các phương thức trừu tượng, không có gì để ghi đè)
Ví dụ:
Khi nào dùng interface, khi nào dùng abstract class?
Khi nào dùng interface, khi nào dùng abstract class?
Câu trả lời đơn giản: Khi nào cần thì dùng, tuỳ vào mục đích của mình
Nhưng mà, để hiểu rõ hơn, ta cùng xem ví dụ sau:
Như vậy, ta có thể thấy, khi dùng interface, ta phải implement lại cả phương thức travel(), trong khi khi dùng abstract class, ta không cần implement lại phương thức travel().
Tuy nhiên, interface lại cho phép ta kế thừa nhiều interface khác, trong khi abstract class thì không.
Tổng kết
Có thể thấy, abstract class cho phép ta có các khả năng trừu tượng, và có thể chứa các logic có sẵn ta muốn dùng cho nhiều class con. Lúc này ta có thể cân nhắc sử dụng abstract class
Thay vì điều đó, interface thường được dùng để chỉ behavior - tính cách, cách xử sự của 1 đối tượng. Tức là 1 cái ô tô thì có thể xử lý các interface là ChayXe, ChayDongCo, ChayDien, ChayDau… Như vậy, interface thường được dùng để chỉ ra tính chất của 1 đối tượng, còn logic thì để class implement interface đó tự triển khai.
Với abstract class, ta dùng cho những case ta có logic tập trung, có thể reuse ở class con. Vừa có khả năng trừu tượng mà các class con sẽ có đặc tính riêng. Tuy nhiên nếu class đó quá lớn, ví dụ class PhuongTien có đủ thứ hàm như ChayXe, ChayDongCo, ChayDien, ChayDau… , thì ví dụ XeDap kế thừa lại có các hàm chạy động cơ, chạy điện => Như thế bị thừa, không cần thiết, làm confuse cho người đọc code => Ta nên tách ra thành interface.
Với interface, ta dùng cho những case ta chỉ cần chỉ ra tính chất của 1 đối tượng, còn logic thì để class implement interface đó tự triển khai.
Tính trừu tượng
Tính trừu tượng là gì?
Tính trừu tượng là một khái niệm trong lập trình hướng đối tượng, nó cho phép ta chỉ ra những gì mà một đối tượng có thể làm, nhưng không nói ra làm thế nào để đối tượng đó làm được điều đó.
Tức là, khi ta dùng xe máy, ta chỉ biết là nó có thể chạy, có thể dừng, có thể bật đèn, có thể bật còi, nhưng ta không cần biết làm thế nào để nó chạy, dừng, bật đèn, bật còi.
Tính trừu tượng là một khái niệm quan trọng trong lập trình hướng đối tượng, nó cho phép ta tách biệt được phần logic của đối tượng và phần giao tiếp với đối tượng. Ta chỉ quan tâm đến phần giao tiếp với đối tượng, còn phần logic của đối tượng thì ta không cần quan tâm. Điều này giúp ta có thể dễ dàng thay đổi phần logic của đối tượng mà không ảnh hưởng đến phần giao tiếp với đối tượng.
Tại sao cần sử dụng interface
Giả sử, ta có 1 interface là “Athlete” – Vận động viên với 2 hàm là play() – là chơi thể thao nào đó
Giả sử, ta có 1 class Team – 1 đội chơi, sẽ có thể thêm vận động viên vào, ta nhận thấy, nếu có thể, ta luôn lên sử dụng interface thay vì khai báo thẳng 1 class cụ thể, vì khi đó, ta có thể thêm bất cứ vận động viên nào vào đội chơi, miễn là vận động viên đó implement interface Athlete.
Như code trên, bất kì vận động viên nào implement interface Athlete đều có thể thêm vào đội chơi, và có thể chơi được. Điều này giúp code dễ mở rộng, flexible hơn, sau này ta có thể thêm bất cứ vận động viên nào vào đội chơi, miễn là vận động viên đó implement interface Athlete.
Đây cũng được biết là quy tắc prefer interface over class hay prefer composition over inheritance
Interface làm đúng sự phụ thuộc của module level cao
Giả sử, ta có class DienThoai, khi này khi ta muốn sạc điện thoại, ta sẽ gọi 1 class bên trong DienThoai là sạc điện bằng củ sạc Samsung
Ta thấy, với thiết kế hiện tại, ta sẽ truyền tham số là cường độ vào củ sạc Samsung để class đó thực hiện sạc.
Tuy nhiên, bất kì sự thay đổi nào ở class CuSac vào phương thức sac có thể yêu cầu class điện thoại truyền thêm tham số, hay phải dùng tên hàm khác, …
Điều này là không đúng, điện thoại rõ ràng là module cấp cao hơn, và nên có quyền quyết định dùng củ sạc nào, chứ không phải củ sạc quyết định điện thoại phải làm gì để dùng.
Vì vậy, ta nên tạo ra 1 interface là CuSac để điện thoại có thể sạc, và class CuSacSamSung implement interface CuSac này.
Lúc này, sự phụ thuộc đã thay đổi. Giờ DienThoai chỉ sử dụng interface củ sạc, ví dụ như Iphone quy định là tôi dùng cổng lightning, máy Samsung bảo tôi dùng cổng sạc USB-C vậy. Các nhà sản xuất củ sạc phải đi theo họ.
Thì ở đây, DienThoai dùng interface để định nghĩa là tôi sẽ muốn sạc như thế nào. Còn củ sạc thì implement interface đó, và tự triển khai logic riêng của mình, nhưng phải tuân thủ theo hợp đồng interface mà DienThoai sử dụng