Daniel Nguyen
← Tất cả dự án

Tính giá vốn kho, ghi tồn kho và sổ cái an toàn

Tôi viết lại engine tính giá vốn bình quân gia quyền để chạy set-based ở nền, và đặt quy tắc lock giữ dữ liệu tồn kho và kế toán nhất quán.

TTMI
Tách khỏi requestKhớp số liệu đối chiếu của kế toán
Mảng
ERP bán lẻ
Thời gian
2025–2026
Vai trò
Backend developer, engine giá vốn
  • Python
  • Django
  • Django REST Framework
  • PostgreSQL
  • pytest

Các phần ghép với nhau thế nào

Tính giá vốn kho, ghi tồn kho và sổ cái an toànDocument saves write stock and ledger rows in one transaction with a fixed lock order, while cost recalculation runs later on a worker and writes back set-based.Client appngười dùngAPI servicedịch vụFixed lock orderkiểm tra, cảnh báoJob queuehàng đợiCosting workerworkerTemp staging tablekho dữ liệuStock + ledger rowskho dữ liệusave stock documentone atomic transactionwrite locked rowsenqueue after commitdeliver jobread lane by warehousebulk-load new pricesset-based update
Document saves write stock and ledger rows in one transaction with a fixed lock order, while cost recalculation runs later on a worker and writes back set-based.
  • Người dùng
  • Dịch vụ
  • Kiểm tra, cảnh báo
  • Hàng đợi
  • Worker
  • Kho dữ liệu

Vấn đề

ERP của TTMI phụ trách kế toán và kho cho 50 cửa hàng, 500 nhân viên và 4 thương hiệu. Mỗi chứng từ kho (nhập, chuyển, bán) đều làm thay đổi giá trị hàng. Hệ thống tính giá tồn theo bình quân gia quyền cho từng kho, nên sửa một chứng từ có thể làm lệch giá bình quân của mọi lần xuất nhập sau đó trong kho ấy, và các con số này chảy tiếp sang bút toán kế toán.

Có hai vấn đề.

Thứ nhất, việc tính lại chạy ngay trong HTTP request. Nó duyệt qua một lịch sử chứng từ kho dài và ghi kết quả từng dòng một. Người dùng bấm lưu một chứng từ có thể giữ request thread rất lâu, và code khó suy luận khi dữ liệu lớn dần.

Thứ hai, nhiều người cùng lúc sửa các chứng từ đụng tới cùng những dòng tài chính. Không có quy tắc rõ ràng về transaction và lock thì sẽ gặp lost update (lần lưu này âm thầm ghi đè lần lưu kia) và deadlock (hai transaction chờ nhau nhả dòng mà bên kia đang giữ).

Tôi đã làm gì

Tôi làm việc với đội kế toán, người nắm quy tắc nghiệp vụ và đưa cho tôi số liệu đối chiếu. Tôi viết engine tính giá vốn và bộ quy ước lock dùng chung cho các luồng ghi của kho và kế toán.

Ghi kết quả set-based qua bảng tạm

Engine tính giá mới, bulk-load vào một bảng tạm có index, rồi cập nhật chứng từ, dòng xuất nhập và bút toán bằng một lệnh update set-based. Cách này thay cho vòng lặp cập nhật từng dòng.

Cái giá là nhiều SQL viết tay hơn và kém portable hơn so với ORM. Đổi lại, database làm một phép join lớn thay vì một chuỗi dài round trip nhỏ.

Chia lane song song theo kho

Giá bình quân của kho này không phụ thuộc kho kia, nên tôi chia việc thành các lane theo kho và chạy song song. Quy tắc này chỉ đúng khi các lane không bao giờ ghi cùng một dòng. Nếu điều đó bị phá, chạy song song sẽ mang lock contention quay lại ngay, nên đây là thứ tôi luôn kiểm tra trước khi sửa phần chia lane.

Đưa tính giá vốn ra background job

Việc tính lại giờ chạy trên nền tảng async job tôi xây (xem case study async jobs). Lưu chứng từ thì commit thay đổi, rồi mới enqueue việc tính lại sau commit.

Cái giá là có một khoảng ngắn số liệu chỉ eventually consistent. Người dùng cần biết việc tính lại đang chờ hay đang chạy, nên trạng thái job được hiển thị cho họ.

Lock theo thứ tự cố định, và chỉ lock thứ thay đổi

Mọi luồng ghi đụng tới dòng tài chính đều theo một mẫu: một transaction atomic; lock chứng từ cha trước; sau đó chỉ lock những dòng con thực sự thay đổi, luôn sắp theo cùng một thứ tự. Thứ tự cố định loại bỏ kiểu deadlock khi hai người giành cùng hai dòng theo thứ tự ngược nhau.

Một chi tiết của database định hình cách làm này. PostgreSQL không cho đặt row lock lên phía nullable của outer join, nên các query kiểu “đọc gì lock nấy” bị lỗi. Khi quan hệ join có thể null, code chỉ lock bảng chính. Pessimistic locking làm giảm khả năng chạy đồng thời trên các dòng nóng, và tôi chấp nhận điều đó vì nó giúp chứng minh tính đúng dễ hơn khi review.

Một độ chính xác cho tiền, và test khóa hành vi

Tôi thống nhất độ chính xác decimal cho các model tài chính trong một lần đổi schema, thay vì nới từng field mỗi khi có bug. Thay đổi đó rộng ngay từ đầu, nhưng chấm dứt chuyện lệch làm tròn giữa các module.

Trước mỗi lần refactor, tôi thêm characterization test ghim output của engine vào số liệu đối chiếu của kế toán. Thay đổi nào làm xê dịch một con số sẽ làm bộ test fail.

Kết quả

Tính giá vốn không còn chặn request thread của API; nó chạy như một background job có trạng thái hiển thị. Hành vi của nó được khóa bằng test dựng trên các trường hợp đối chiếu của kế toán. Cùng bộ quy ước lock đó giờ bảo vệ các luồng ghi của kho, kế toán và kiểm soát chất lượng.

Nếu làm lại

Phần lớn công sức về tính đúng nằm ở chỗ đặt ranh giới transaction và lock, và tôi học được điều đó bằng cách sửa từng luồng ghi một. Nếu làm lại, tôi sẽ viết quy ước lock thành một tài liệu ngắn cho team trước, rồi review các luồng ghi mới theo nó. Tôi cũng sẽ đo thời gian tính lại trước và sau, vì hiện tại tôi chỉ mô tả được mức cải thiện bằng lời.