Đánh số hóa đơn an toàn khi có đồng thời — tránh trùng và khoảng trống
Học các mẫu thực tế để đánh số hóa đơn an toàn khi có đồng thời, giúp nhiều người tạo mà không trùng hoặc sinh khoảng trống bất ngờ.

Điều gì sai khi hai người tạo bản ghi cùng lúc
Hãy tưởng tượng một văn phòng bận rộn lúc 16:55. Hai người hoàn tất hóa đơn và bấm Lưu trong vòng một giây. Cả hai màn hình đều thoáng hiện “Hóa đơn #1042”. Một bản thắng, bản kia thất bại, hoặc tệ hơn, cả hai được lưu với cùng một số. Đây là triệu chứng phổ biến nhất ngoài thực tế: số trùng chỉ xuất hiện khi có tải cao.
Ticket (vé hỗ trợ) cũng gặp vấn đề tương tự. Hai nhân viên tạo ticket cho cùng một khách hàng cùng lúc, và hệ thống của bạn cố “lấy số tiếp theo” bằng cách nhìn vào bản ghi mới nhất. Nếu cả hai request đọc cùng giá trị “mới nhất” trước khi một trong hai ghi, cả hai có thể chọn cùng một số tiếp theo.
Triệu chứng thứ hai tinh tế hơn: số bị bỏ sót. Bạn có thể thấy #1042 rồi #1044, thiếu #1043. Điều này thường xảy ra sau lỗi hoặc retry. Một request dự trữ số, nhưng lưu không thành công do lỗi xác thực, timeout, hoặc người dùng đóng tab. Hoặc một job nền retry sau sự cố mạng và lấy số mới mặc dù lần thử đầu đã tiêu một số.
Với hóa đơn, điều này quan trọng vì số hóa đơn là một phần của dấu vết kiểm toán. Kế toán mong mỗi hóa đơn có định danh duy nhất, và khách hàng có thể tham chiếu số hóa đơn khi thanh toán hoặc gửi thư hỗ trợ. Với ticket, số là tay nắm mọi người dùng trong hội thoại, báo cáo và xuất dữ liệu. Trùng số gây nhầm lẫn. Số bị thiếu có thể làm dấy lên câu hỏi khi rà soát, dù không có hành vi gian lận.
Một kỳ vọng quan trọng cần đặt sớm: không phải mọi phương pháp đánh số đều có thể vừa an toàn khi có đồng thời vừa không bỏ sót. Đánh số an toàn đồng thời (không trùng ngay cả khi nhiều người) là khả thi và cần phải được ưu tiên. Không bỏ sót cũng có thể thực hiện được, nhưng đòi hỏi quy tắc bổ sung và thường thay đổi cách bạn xử lý nháp, thất bại và hủy.
Một cách hữu ích để đặt vấn đề là hỏi số của bạn phải đảm bảo điều gì:
- Không được lặp lại (luôn duy nhất)
- Nên tăng dần phần lớn thời gian (tốt khi có)
- Không được bỏ sót (chỉ khi bạn thiết kế cho điều này)
Khi đã chọn quy tắc, việc chọn giải pháp kỹ thuật sẽ dễ dàng hơn nhiều.
Tại sao trùng và bỏ sót xảy ra
Hầu hết ứng dụng theo mẫu đơn giản: người dùng bấm Lưu, app hỏi số hóa đơn hoặc ticket tiếp theo, rồi chèn bản ghi mới với số đó. Cách làm này an toàn ở môi trường một người.
Vấn đề bắt đầu khi hai thao tác lưu xảy ra gần như cùng lúc. Cả hai request có thể tới bước “lấy số tiếp theo” trước khi bất kỳ cái nào hoàn thành insert. Nếu cả hai đọc cùng giá trị “tiếp theo”, cả hai sẽ cố ghi cùng số. Đó là race condition: kết quả phụ thuộc vào thời điểm, không phải logic.
Một timeline điển hình như sau:
- Request A đọc số tiếp theo: 1042
- Request B đọc số tiếp theo: 1042
- Request A chèn hóa đơn 1042
- Request B chèn hóa đơn 1042 (hoặc bị lỗi nếu có ràng buộc unique)
Trùng xảy ra khi không có cái gì trong cơ sở dữ liệu ngăn insert thứ hai. Nếu bạn chỉ kiểm tra “số này đã có chưa?” trong mã ứng dụng, bạn vẫn có thể thua cuộc đua giữa kiểm tra và insert.
Bỏ sót là vấn đề khác. Chúng phát sinh khi hệ thống “dự trữ” một số nhưng bản ghi không bao giờ thành hóa đơn/ticket thực sự đã commit. Nguyên nhân phổ biến là thanh toán thất bại, lỗi xác thực phát hiện muộn, timeout, hoặc người dùng đóng tab sau khi số đã được gán. Ngay cả khi insert thất bại và không có gì được lưu, số đó có thể đã bị tiêu.
Sự đồng thời ẩn làm mọi thứ tồi tệ hơn vì hiếm khi chỉ là “hai người bấm Lưu.” Bạn còn có thể có:
- Các client API tạo bản ghi song song
- Import chạy theo lô
- Job nền sinh hóa đơn qua đêm
- Retry từ app mobile khi kết nối kém
Tóm lại: nguyên nhân gốc là (1) xung đột thời điểm khi nhiều request đọc cùng giá trị counter, và (2) số bị cấp trước khi chắc chắn giao dịch sẽ thành công. Mọi kế hoạch đều phải quyết định chấp nhận kết quả nào: không trùng, không bỏ sót, hay cả hai, và trong trường hợp nào (nháp, retry, hủy).
Quyết định quy tắc đánh số trước khi chọn giải pháp
Trước khi thiết kế đánh số an toàn đồng thời, hãy ghi rõ số phải mang ý nghĩa gì trong nghiệp vụ. Sai lầm phổ biến là chọn phương pháp kỹ thuật trước, rồi phát hiện ra luật kế toán hoặc pháp lý mong khác.
Bắt đầu bằng cách tách hai mục tiêu thường bị trộn:
- Unique: không hai hóa đơn/ticket nào cùng số.
- Gapless: số là duy nhất và còn liên tiếp nghiêm ngặt (không thiếu số).
Nhiều hệ thống thực tế hướng tới chỉ duy nhất và chấp nhận khoảng trống. Khoảng trống có thể xảy ra vì lý do bình thường: người dùng mở nháp rồi bỏ, thanh toán thất bại sau khi số đã được dự trữ, hoặc một bản ghi bị tạo rồi hủy. Với helpdesk, khoảng trống hầu như không quan trọng. Ngay cả với hóa đơn, nhiều đội chấp nhận khoảng trống nếu có thể giải thích bằng dấu vết kiểm toán (voided, canceled, test,...). Số không bỏ sót có thể thực hiện được, nhưng sẽ bắt buộc các quy tắc bổ sung và thường gây ma sát hơn.
Tiếp theo, quyết định phạm vi của counter. Những khác biệt nhỏ về cách diễn đạt ảnh hưởng lớn tới thiết kế:
- Một chuỗi toàn hệ thống, hay chuỗi riêng cho từng công ty/tenant?
- Reset mỗi năm (2026-000123) hay không reset?
- Series khác nhau cho hóa đơn vs phiếu giảm giá vs ticket?
- Bạn cần định dạng thân thiện với con người (tiền tố, dấu phân cách), hay chỉ cần số nội bộ?
Ví dụ cụ thể: một sản phẩm SaaS nhiều khách hàng có thể yêu cầu số hóa đơn duy nhất trên từng công ty và reset mỗi năm lịch, trong khi ticket duy nhất trên toàn hệ thống và không reset. Đó là hai counter khác nhau với quy tắc khác nhau, dù UI có thể giống.
Nếu bạn thực sự cần không bỏ sót, hãy ghi rõ sự kiện nào được phép xảy ra sau khi số được gán. Ví dụ: có thể xóa hóa đơn không, hay chỉ hủy? Người dùng có được lưu nháp có số hay không? Những lựa chọn này thường quan trọng hơn kỹ thuật cơ sở dữ liệu.
Ghi quy tắc ngắn gọn trước khi xây:
- Loại bản ghi nào dùng chuỗi?
- Khi nào một số được coi là “đã dùng” (nháp, gửi, thanh toán)?
- Phạm vi (toàn cục, theo công ty, theo năm, theo series)?
- Xử lý voids và chỉnh sửa thế nào?
Trong AppMaster, loại quy tắc này thuộc về mô hình dữ liệu và flow nghiệp vụ, để đội triển khai cùng hành vi ở API, web UI và mobile mà không bị bất ngờ.
Các cách tiếp cận phổ biến và đảm bảo của từng cách
Khi nói về “đánh số hóa đơn”, người ta thường trộn hai mục tiêu: (1) không sinh trùng số, và (2) không có khoảng trống. Hầu hết hệ thống dễ đảm bảo mục tiêu đầu. Mục tiêu thứ hai khó hơn vì khoảng trống xuất hiện bất cứ khi nào giao dịch thất bại, nháp bị bỏ, hoặc bản ghi bị hủy.
Cách 1: Sequence của cơ sở dữ liệu (duy nhất nhanh)
Sequence của PostgreSQL là cách đơn giản nhất để có số duy nhất, tăng dần khi có nhiều người cùng tạo. Nó scale tốt vì DB được thiết kế để phát giá trị sequence nhanh, ngay cả khi nhiều user cùng lúc.
Bạn được: tính duy nhất và tăng dần (phần lớn). Bạn không được: không bỏ sót. Nếu insert thất bại sau khi cấp số, số đó bị “đốt” và bạn sẽ thấy khoảng trống.
Cách 2: Ràng buộc UNIQUE cộng retry (để DB quyết định)
Ở đây bạn tạo một số dự kiến (theo logic app), lưu nó, và dựa vào ràng buộc UNIQUE để từ chối trùng. Nếu gặp conflict, bạn retry với số mới.
Cách này hoạt động nhưng có thể gây ồn dưới concurrency cao. Bạn sẽ thấy nhiều retry, nhiều transaction thất bại và spike khó gỡ. Nó cũng không đảm bảo không bỏ sót trừ khi kết hợp với quy tắc dự trữ chặt chẽ, điều này làm tăng độ phức tạp.
Cách 3: Hàng đếm (counter) dạng row với khóa (nhắm đến không bỏ sót)
Nếu thực sự cần không bỏ sót, mẫu thông thường là bảng counter chuyên dụng (một row cho mỗi phạm vi, như theo năm hoặc theo công ty). Bạn khóa row đó trong transaction, tăng nó, và sử dụng giá trị mới.
Đây là gần nhất để đạt gapless trong thiết kế DB thông thường, nhưng có chi phí: nó tạo điểm nóng mà mọi writer phải chờ. Nó cũng tăng rủi ro khi vận hành (transaction dài, timeout, deadlock).
Cách 4: Dịch vụ reservation riêng (chỉ cho trường hợp đặc biệt)
Một “dịch vụ đánh số” độc lập có thể tập trung quy tắc cho nhiều app hoặc DB. Thường chỉ nên dùng khi có nhiều hệ thống cấp số và không thể gom writes.
Đổi lại là rủi ro vận hành: thêm service phải chính xác, luôn sẵn sàng và nhất quán.
Tóm lại đảm bảo cho các lựa chọn:
- Sequence: duy nhất, nhanh, chấp nhận khoảng trống
- Unique + retry: duy nhất, đơn giản ở tải thấp, có thể gây thrash ở tải cao
- Row khóa counter: có thể không bỏ sót, chậm hơn khi concurrency cao
- Dịch vụ riêng: linh hoạt xuyên hệ thống, phức tạp và nhiều chế độ thất bại nhất
Nếu bạn xây trong công cụ no-code như AppMaster, các lựa chọn vẫn áp dụng: DB là nơi đảm bảo tính đúng đắn. App logic hỗ trợ retry và thông báo lỗi rõ, nhưng đảm bảo cuối cùng nên đến từ ràng buộc và transaction.
Từng bước: ngăn trùng bằng sequence và unique constraint
Nếu mục tiêu chính là ngăn trùng (không đảm bảo không bỏ sót), mẫu đơn giản đáng tin là: để DB sinh ID nội bộ, và bắt buộc tính duy nhất trên số hiển thị cho khách hàng.
Bắt đầu bằng tách hai khái niệm. Dùng giá trị sinh bởi DB (identity/sequence) làm primary key cho join, sửa, export. Giữ invoice_no hoặc ticket_no là cột riêng hiển thị cho người dùng.
Thiết lập thực tế trong PostgreSQL
Cách tiếp cận phổ biến giữ logic “số tiếp theo” trong DB, nơi concurrency được xử lý đúng.
-- Internal, never-shown primary key
create table invoices (
id bigint generated always as identity primary key,
invoice_no text not null,
created_at timestamptz not null default now()
);
-- Business-facing uniqueness guarantee
create unique index invoices_invoice_no_uniq on invoices (invoice_no);
-- Sequence for the visible number
create sequence invoice_no_seq;
Bây giờ sinh số hiển thị tại thời điểm insert (không làm select max(invoice_no) + 1). Một mẫu đơn giản là format giá trị sequence trong INSERT:
insert into invoices (invoice_no)
values (
'INV-' || lpad(nextval('invoice_no_seq')::text, 8, '0')
)
returning id, invoice_no;
Ngay cả khi 50 người cùng bấm “Tạo hóa đơn” cùng lúc, mỗi insert sẽ lấy giá trị sequence khác nhau, và unique index sẽ ngăn trùng.
Xử lý khi xảy ra va chạm
Với sequence đơn thuần, va chạm hiếm. Thường xuất hiện khi bạn thêm quy tắc như “reset theo năm”, “theo tenant” hoặc cho phép người dùng chỉnh số. Vì vậy ràng buộc unique vẫn quan trọng.
Ở tầng ứng dụng, xử lý lỗi vi phạm unique bằng một vòng retry nhỏ:
- Thử insert
- Nếu gặp lỗi unique trên invoice_no, thử lại
- Dừng sau một số lần cố gắng nhất định và trả lỗi rõ
Cách này ổn vì retry chỉ xảy ra khi có tình huống bất thường, như hai luồng tạo cùng một formatted number.
Giữ cửa sổ race nhỏ
Đừng tính số trên UI, và đừng “dự trữ” số bằng cách đọc trước rồi insert sau. Sinh số càng gần thao tác ghi vào DB càng tốt.
Nếu dùng AppMaster với PostgreSQL, bạn có thể model id là identity primary key trong Data Designer, thêm ràng buộc unique cho invoice_no, và sinh invoice_no trong flow tạo để nó xảy ra cùng lúc với insert. Khi đó DB là nguồn sự thật và vấn đề đồng thời được xử lý nơi PostgreSQL mạnh nhất.
Từng bước: xây counter không bỏ sót với khóa row
Nếu bạn thực sự cần không bỏ sót số, có thể dùng bảng counter giao dịch và khóa row. Ý tưởng đơn giản: chỉ cho một transaction tại một thời điểm lấy số tiếp theo cho phạm vi, nên số được phát theo thứ tự.
Trước hết, quyết định phạm vi. Nhiều đội cần sequence riêng theo công ty, năm, hoặc series (INV vs CRN). Bảng counter lưu số cuối cùng đã dùng cho mỗi phạm vi.
Mẫu thực tế dùng khóa row trong PostgreSQL:
- Tạo bảng
number_countersvới các cột nhưcompany_id,year,series,last_number, và ràng buộc unique trên(company_id, year, series). - Bắt đầu một transaction.
- Khóa row counter cho phạm vi bằng
SELECT last_number FROM number_counters WHERE ... FOR UPDATE. - Tính
next_number = last_number + 1, cập nhật row counter thànhlast_number = next_number. - Insert hóa đơn hoặc ticket dùng
next_number, rồi commit.
Chìa khóa là FOR UPDATE. Dưới tải cao, bạn không bị trùng. Bạn cũng không bị bỏ sót do “hai người cùng lấy số”, vì transaction thứ hai phải chờ cho tới khi transaction đầu commit hoặc rollback. Sự chờ này là chi phí để đạt gapless.
Khởi tạo một phạm vi mới
Cần có kế hoạch cho lần đầu xuất hiện phạm vi mới (công ty mới, năm mới, series mới). Hai lựa chọn phổ biến:
- Tạo trước các row counter (ví dụ tạo row cho năm kế tiếp vào tháng Mười Hai).
- Tạo theo nhu cầu: thử insert row counter với
last_number = 0, nếu đã tồn tại thì fallback về flow khóa và tăng số.
Nếu bạn xây trong công cụ no-code như AppMaster, giữ toàn bộ chuỗi “khóa, tăng, insert” trong một transaction trong business logic, để nó xảy ra hết hoặc không xảy ra gì cả.
Trường hợp biên: nháp, lưu thất bại, hủy và chỉnh sửa
Phần lớn lỗi đánh số xuất hiện ở những chỗ lộn xộn: nháp không bao giờ đăng, lưu thất bại, hóa đơn bị void, và bản ghi bị chỉnh sửa sau khi ai đó đã thấy số. Nếu muốn đánh số an toàn đồng thời, bạn cần quy tắc rõ ràng khi nào số trở thành “thực”.
Quyết định lớn nhất là thời điểm. Nếu gán số ngay khi người dùng bấm “Mới”, bạn sẽ có khoảng trống do nháp bị bỏ. Nếu chỉ gán khi hóa đơn được finalize (posted, issued, sent, hay nghĩa là “chốt”), bạn có thể giữ số chặt hơn và dễ giải thích hơn.
Lưu thất bại và rollback là nơi kỳ vọng thường xung đột với hành vi DB. Với sequence thông thường, một khi số được lấy thì nó đã bị lấy, ngay cả khi transaction sau đó thất bại. Đó là hành vi bình thường và an toàn, nhưng có thể tạo khoảng trống. Nếu chính sách yêu cầu không bỏ sót, số phải được gán chỉ ở bước cuối cùng và chỉ khi transaction commit. Điều đó thường có nghĩa là khóa một row counter, ghi số cuối cùng và commit trong cùng một đơn vị. Nếu bất kỳ bước nào lỗi, không có số nào được gán.
Hủy và void hầu như không nên “tái sử dụng” số. Giữ nguyên số và đổi trạng thái. Kiểm toán viên và khách hàng mong lịch sử giữ nguyên, ngay cả khi một tài liệu được chỉnh sửa.
Chỉnh sửa đơn giản hơn: một khi số đã hiển thị ra ngoài hệ thống, coi nó là cố định. Đừng đổi số hóa đơn hay ticket sau khi đã chia sẻ, export hay in. Nếu cần sửa, tạo tài liệu mới tham chiếu bản cũ (ví dụ credit note hoặc ticket thay thế), đừng viết lại lịch sử.
Một tập quy tắc thực tế nhiều đội dùng:
- Nháp không có số final (dùng internal ID hoặc “DRAFT”).
- Gán số chỉ khi “Post/Issue”, trong cùng transaction với đổi trạng thái.
- Voids và cancellations giữ số nhưng có trạng thái và lý do rõ ràng.
- Số đã in/gửi không đổi.
- Import giữ số gốc và cập nhật counter để bắt đầu sau giá trị lớn nhất đã import.
Migrations và import cần chú ý. Nếu chuyển từ hệ thống khác, đem theo số hóa đơn hiện có nguyên vẹn, rồi đặt counter bắt đầu sau giá trị tối đa đã import. Quyết định xử lý các format mâu thuẫn (ví dụ các tiền tố khác nhau theo năm). Thường tốt hơn là lưu “số hiển thị” đúng như cũ, và giữ primary key nội bộ riêng.
Ví dụ: helpdesk tạo ticket rất nhanh nhưng nhiều là nháp. Gán số ticket chỉ khi agent bấm “Gửi cho khách hàng”. Điều này tránh lãng phí số cho nháp bỏ và giữ chuỗi hiển thị khớp với giao tiếp thực tế. Trong AppMaster, cùng ý tưởng: giữ nháp là bản ghi không có số công khai, rồi sinh số chính thức trong bước Business Process “submit” mà commit thành công.
Sai lầm phổ biến gây trùng hoặc khoảng trống bất ngờ
Hầu hết vấn đề đánh số bắt nguồn từ một ý tưởng đơn giản: coi số là giá trị hiển thị hơn là trạng thái chia sẻ. Khi nhiều người lưu cùng lúc, hệ thống cần một nơi rõ ràng quyết định số tiếp theo, và một quy tắc rõ ràng khi xảy ra lỗi.
Một sai lầm kinh điển là dùng SELECT MAX(number) + 1 trong mã ứng dụng. Trông ổn khi kiểm thử một người, nhưng hai request có thể đọc cùng MAX trước khi bất kỳ cái nào commit. Cả hai tạo cùng next value và bạn có số trùng. Ngay cả khi thêm “kiểm tra rồi retry”, bạn vẫn có thể tạo thêm load và spike lạ khi traffic cao.
Nguồn trùng khác là tạo số ở phía client (browser hoặc mobile) trước khi lưu. Client không biết người khác đang làm gì, và không thể đảm bảo dự trữ số nếu lưu thất bại. Số do client sinh hợp lý cho nhãn tạm như “Draft 12”, nhưng không nên cho ID chính thức.
Khoảng trống làm các đội ngạc nhiên khi họ cho rằng sequence là không bỏ sót. Trong PostgreSQL, sequence được thiết kế cho tính duy nhất, không phải liên tục hoàn hảo. Số có thể bị bỏ khi transaction rollback, khi bạn prefetch ID, hoặc khi DB khởi động lại. Đó là hành vi bình thường. Nếu yêu cầu thực sự là “không trùng”, sequence cộng unique constraint thường là đáp án đúng. Nếu yêu cầu thật sự là “không bỏ sót”, bạn cần pattern khác (thường là row locking) và phải chấp nhận đánh đổi throughput.
Khóa cũng có thể phản tác dụng nếu khóa quá rộng. Một khóa toàn cục cho mọi đánh số ép mọi thao tác tạo xếp hàng, dù bạn có thể phân vùng counter theo công ty, vị trí, hoặc loại tài liệu. Điều này làm chậm hệ thống và khiến người dùng cảm giác lưu bị “kẹt” ngẫu nhiên.
Các lỗi cần kiểm tra khi triển khai:
- Dùng
MAX + 1(hoặc “tìm số cuối”) mà không có ràng buộc unique ở DB. - Tạo số final trên client rồi cố “fix conflict” sau.
- Mong PostgreSQL sequence là không bỏ sót rồi coi khoảng trống là lỗi.
- Khóa một counter dùng chung cho mọi thứ thay vì phân vùng hợp lý.
- Chỉ test với một người dùng, nên race condition không hiện ra trước khi ra mắt.
mẹo kiểm thử: chạy test concurrency tạo 100–1.000 bản ghi song song rồi kiểm tra trùng và khoảng trống. Với AppMaster, cùng quy tắc: gọi chắc chắn số được gán trong một transaction server-side, không trong luồng UI.
Kiểm tra nhanh trước khi phát hành
Trước khi ra mắt đánh số hóa đơn hoặc ticket, làm một lượt kiểm tra các phần thường hỏng khi có tải thật. Mục tiêu đơn giản: mỗi bản ghi có đúng một số nghiệp vụ, và quy tắc giữ đúng ngay cả khi 50 người bấm “Tạo” cùng lúc.
Danh sách kiểm tra trước khi phát hành:
- Xác nhận cột số nghiệp vụ có ràng buộc unique ở DB (không chỉ kiểm tra ở UI). Đây là hàng rào cuối cùng nếu hai request va chạm.
- Đảm bảo số được gán trong cùng transaction lưu bản ghi. Nếu gán số và lưu tách ra qua nhiều request, cuối cùng bạn sẽ thấy trùng.
- Nếu cần không bỏ sót, chỉ gán số khi bản ghi được finalize (ví dụ khi hóa đơn được phát hành, không khi tạo nháp). Nháp, form bỏ giữa chừng và thanh toán thất bại là nguồn khoảng trống phổ biến.
- Thêm chiến lược retry cho các conflict hiếm. Ngay cả với row locking hoặc sequence, bạn có thể gặp lỗi serialization, deadlock, hoặc unique violation trong các timing biên. Retry đơn giản với backoff ngắn thường đủ.
- Chạy stress test với 20–100 tạo đồng thời qua tất cả entry point: UI, public API, import. Test các kịch bản thực tế như bursts, mạng chậm và double submit.
Cách kiểm chứng nhanh: mô phỏng tình huống helpdesk bận rộn: hai agent mở form “New ticket”, một người submit từ web app trong khi job import chèn ticket từ hộp mail cùng lúc. Sau chạy, kiểm tra tất cả số là duy nhất, đúng format, và các lỗi không để lại bản ghi nửa vời.
Nếu bạn xây workflow trong AppMaster, nguyên tắc vẫn thế: giữ việc gán số trong transaction DB, dựa vào ràng buộc PostgreSQL, và test cả hành động UI lẫn endpoint API tạo cùng một thực thể. Nhiều đội yên tâm bằng test thủ công nhưng bị bất ngờ vào ngày người dùng thực tế đến.
Ví dụ: helpdesk bận rộn và bước tiếp theo
Hãy hình dung một support desk nơi agent tạo ticket suốt ngày trong web app, trong khi một tích hợp cũng tạo ticket từ chat tool và email. Mọi người mong số ticket dạng T-2026-000123, và mỗi số trỏ tới đúng một ticket.
Cách làm ngây thơ là: đọc “số ticket cuối”, cộng 1, lưu ticket mới. Dưới tải, hai request có thể đọc cùng “số cuối” trước khi có insert. Cả hai tính cùng next number và bạn có trùng. Nếu cố “sửa” bằng retry sau lỗi, thường bạn lại tạo khoảng trống mà không có ý nghĩa.
DB có thể ngăn trùng ngay cả khi mã app của bạn ngây thơ. Thêm unique constraint trên cột ticket_number. Khi hai request cố cùng số, một insert sẽ lỗi và bạn có thể retry gọn gàng. Đó là cốt lõi của đánh số an toàn đồng thời: để DB cưỡng chế tính duy nhất, không phải UI.
Đánh số không bỏ sót thay đổi workflow. Nếu cần không bỏ sót, thường bạn không thể gán số final khi ticket mới được tạo (nháp). Thay vào đó, tạo ticket với status như Draft và ticket_number nullable. Gán số chỉ khi ticket được finalize, vậy nháp bỏ và lưu thất bại không “đốt” số.
Thiết kế bảng đơn giản:
- tickets: id, created_at, status (Draft, Open, Closed), ticket_number (nullable), finalized_at
- ticket_counters: key (ví dụ "tickets_2026"), next_number
Trong AppMaster, bạn có thể model trong Data Designer với kiểu PostgreSQL, rồi build logic trong Business Process Editor:
- Create Ticket: insert ticket với status=Draft và không có ticket_number
- Finalize Ticket: bắt transaction, khóa row counter, đặt ticket_number, tăng next_number, commit
- Test: chạy hai hành động “Finalize” cùng lúc và xác nhận không có trùng
Bước tiếp theo: bắt đầu với quy tắc của bạn (chỉ unique vs thực sự không bỏ sót). Nếu chấp nhận khoảng trống, sequence cộng unique constraint thường đủ và giữ flow đơn giản. Nếu cần gapless, chuyển việc đánh số sang bước finalize và coi nháp là một trạng thái chính. Sau đó load-test với nhiều agent click cùng lúc và với tích hợp API nổ burst, để thấy hành vi trước khi người dùng thực sự gặp.


