Hai request cùng sửa một dữ liệuTwo requests updating the same data
Hai người dùng đi qua load balancer tới hai instance API khác nhau, cùng ghi vào một dòng trong PostgreSQL. Khoá trong bộ nhớ của từng instance không thấy nhau, nên việc điều phối phải nằm ở DB. Chọn từng kịch bản để so sánh optimistic locking, read-modify-write và cộng dồn nguyên tử.Two users go through the load balancer to two different API instances and write the same PostgreSQL row. In-memory locks in each instance cannot see each other, so coordination must live in the database. Pick a scenario to compare optimistic locking, read-modify-write and an atomic increment.
1A và B cùng mở form sửa hồ sơ 42. Cả hai nhận version 7.A and B both open the edit form for profile 42. Both receive version 7.
Đọc giá trị, tính trong code, rồi ghi lại là ba bước tách rời. Hai request xen kẽ nhau sẽ ghi cùng kết quả và một thay đổi biến mất. Với bộ đếm, chuyển phép tính vào SQL: SET like_count = like_count + 1.Reading a value, computing in code and writing it back are three separate steps. Two interleaved requests write the same result and one change disappears. For counters, move the arithmetic into SQL: SET like_count = like_count + 1.
“Kiểm tra email chưa tồn tại rồi INSERT” hay “kiểm tra còn hàng rồi trừ kho” đều có khe hở giữa hai bước. Gộp điều kiện vào câu ghi (UPDATE stock SET qty = qty - 1 WHERE id = ? AND qty > 0, rồi kiểm tra số dòng) hoặc để unique constraint quyết định.“Check the email is free, then INSERT” or “check stock, then decrement” both leave a gap between the steps. Fold the condition into the write (UPDATE stock SET qty = qty - 1 WHERE id = ? AND qty > 0, then check the row count) or let a unique constraint decide.
Người dùng mở form vài phút rồi mới lưu, nên không thể giữ khoá DB suốt thời gian đó. Thêm cột version: client gửi lại version đã đọc, UPDATE … WHERE id = ? AND version = ?; 0 dòng nghĩa là có người sửa trước, trả 409. JPA làm việc này với @Version.A user keeps a form open for minutes before saving, so you cannot hold a DB lock that long. Add a version column: the client sends back the version it read, UPDATE … WHERE id = ? AND version = ?; zero rows means someone saved first, so return 409. JPA does this with @Version.
Khi nhiều giao dịch ngắn cùng sửa một dòng và cần đọc trước khi quyết định (chuyển tiền giữa hai ví), SELECT … FOR UPDATE khoá dòng để các giao dịch xếp hàng. Giữ transaction ngắn, khoá theo thứ tự cố định (id nhỏ trước) để tránh deadlock, đặt lock_timeout.When many short transactions modify one row and must read before deciding (moving money between two wallets), SELECT … FOR UPDATE locks the row so they queue. Keep transactions short, lock in a fixed order (lower id first) to avoid deadlocks, and set lock_timeout.
PostgreSQL mặc định READ COMMITTED, không chặn lost update. REPEATABLE READ và SERIALIZABLE báo lỗi 40001 khi phát hiện xung đột, nên ứng dụng phải bắt lỗi và chạy lại cả transaction. MySQL InnoDB ở REPEATABLE READ không báo lỗi lost update theo cách này.PostgreSQL defaults to READ COMMITTED, which does not prevent lost updates. REPEATABLE READ and SERIALIZABLE raise error 40001 on a conflict, so the application must catch it and rerun the whole transaction. MySQL InnoDB at REPEATABLE READ does not report lost updates this way.
Cache hay bộ đếm dùng chung giữa các thread cũng bị race. Java: synchronized, AtomicLong, ConcurrentHashMap.merge. Go: sync.Mutex, sync/atomic, hoặc để một goroutine sở hữu dữ liệu và nhận lệnh qua channel. Chạy go test -race trong CI. Các khoá này chỉ có tác dụng trong một instance.Caches or counters shared between threads race too. Java: synchronized, AtomicLong, ConcurrentHashMap.merge. Go: sync.Mutex, sync/atomic, or one goroutine owning the data and taking commands over a channel. Run go test -race in CI. These locks only work within a single instance.
| BảngTable | Cột chínhKey columns | Ghi chúNotes |
|---|---|---|
user_profile | id, display_name, bio, avatar_url, version, updated_at | version tăng mỗi lần UPDATE; client gửi lại version đã đọcversion grows on every UPDATE; the client sends back the version it read |
post | id, author_id, body, like_count, created_at | like_count chỉ thay đổi bằng like_count + 1 hoặc - 1 trong SQLlike_count only changes through like_count + 1 or - 1 in SQL |
post_like | post_id, user_id, created_at | primary key (post_id, user_id): lớp chặn cuối cùng cho thích trùngprimary key (post_id, user_id): the last line of defence against double likes |
wallet | id, user_id, balance, version | check (balance >= 0); chuyển tiền dùng SELECT … FOR UPDATE theo thứ tự idcheck (balance >= 0); transfers use SELECT … FOR UPDATE in id order |
Sửa hồ sơ bằng optimistic locking và trả 409 khi version lệch; thích bài bằng unique constraint cộng với cộng dồn nguyên tử trong SQL.Edit a profile with optimistic locking and return 409 on a version mismatch; like a post with a unique constraint plus an atomic increment in SQL.
@Entity
public class UserProfile {
@Id private Long id;
private String displayName;
private String bio;
@Version private long version;
// getters, setters
}
public interface PostRepository extends JpaRepository<Post, Long> {
@Modifying
@Query("update Post p set p.likeCount = p.likeCount + 1 where p.id = :id")
int incrementLikes(@Param("id") long id);
}
@Service
@RequiredArgsConstructor
public class ProfileService {
private final UserProfileRepository profiles;
private final PostRepository posts;
private final PostLikeRepository likes;
@Transactional
public UserProfile update(long id, ProfileEdit edit) {
UserProfile p = profiles.findById(id).orElseThrow();
if (p.getVersion() != edit.version()) {
throw new ObjectOptimisticLockingFailureException(UserProfile.class, id);
}
p.setDisplayName(edit.displayName());
p.setBio(edit.bio());
return p; // UPDATE … WHERE id = ? AND version = ? at commit
}
@Transactional
public void like(long postId, long userId) {
if (likes.insertIgnore(postId, userId) == 0) return; // already liked
posts.incrementLikes(postId); // no read-modify-write
}
}
@RestControllerAdvice
class ConflictAdvice {
@ExceptionHandler(ObjectOptimisticLockingFailureException.class)
ResponseEntity<String> conflict() {
return ResponseEntity.status(HttpStatus.CONFLICT).body("Reload and try again");
}
}var ErrConflict = errors.New("profile was modified by someone else")
func (r *Repo) UpdateProfile(ctx context.Context, e ProfileEdit) error {
tag, err := r.db.Exec(ctx, `
UPDATE user_profile
SET display_name = $1, bio = $2, version = version + 1
WHERE id = $3 AND version = $4`,
e.DisplayName, e.Bio, e.ID, e.Version)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return ErrConflict // handler maps it to 409
}
return nil
}
func (r *Repo) Like(ctx context.Context, postID, userID int64) error {
return pgx.BeginFunc(ctx, r.db, func(tx pgx.Tx) error {
tag, err := tx.Exec(ctx,
"INSERT INTO post_like (post_id, user_id) VALUES ($1, $2) ON CONFLICT DO NOTHING",
postID, userID)
if err != nil || tag.RowsAffected() == 0 {
return err // nil: already liked
}
_, err = tx.Exec(ctx,
"UPDATE post SET like_count = like_count + 1 WHERE id = $1", postID)
return err
})
}
// In-process shared state needs a lock too; run go test -race in CI.
type ViewCounter struct {
mu sync.Mutex
n map[int64]int
}
func (c *ViewCounter) Inc(postID int64) {
c.mu.Lock()
defer c.mu.Unlock()
c.n[postID]++
}| Optimistic (cột version)Optimistic (version column) | Pessimistic (SELECT … FOR UPDATE)Pessimistic (SELECT … FOR UPDATE) | |
|---|---|---|
| Hợp vớiGood for | Ít xung đột, người dùng sửa qua form trong thời gian dàiRare conflicts, users editing through a form over a long time | Dòng tranh chấp cao, transaction ngắn cần đọc rồi quyết địnhHot rows, short transactions that read before deciding |
| Bên thua nhận gìWhat the loser gets | 409 hoặc lỗi, phải tải lại hoặc thử lạiA 409 or an error; reload or retry | Chờ khoá rồi chạy tiếp với dữ liệu mớiWaits for the lock, then proceeds on fresh data |
| Chi phíCost | Không giữ khoá; nhiều xung đột thì tốn công thử lạiNo locks held; many conflicts mean wasted retries | Giữ khoá, giảm song song, có thể deadlock nếu khoá sai thứ tựHolds locks, reduces parallelism, can deadlock if locks are taken out of order |
| Ví dụExample | Sửa hồ sơ, sửa bài viết, cấu hìnhProfile edits, article edits, settings | Chuyển tiền giữa hai ví, giữ chỗ ghế cuối cùngMoving money between wallets, reserving the last seat |
Mỗi pattern mở bài học tương ứng trong menu Pattern.Each one opens its lesson in the Patterns menu.
synchronized chỉ khoá trong một JVM. Request đi tới instance khác không thấy khoá đó. Điều phối phải nằm ở nơi dùng chung: DB (cộng dồn nguyên tử, version, FOR UPDATE) hoặc khoá phân tán.synchronized only locks within one JVM; a request on another instance never sees it. Coordination must live in the shared place: the database (atomic updates, versions, FOR UPDATE) or a distributed lock.
Optimistic khi xung đột hiếm và thời gian giữa đọc và ghi dài (form). Pessimistic khi dòng bị tranh chấp nhiều và transaction ngắn, vì thử lại liên tục sẽ tốn hơn chờ khoá.Optimistic when conflicts are rare and the gap between read and write is long (forms). Pessimistic when a row is heavily contended and transactions are short, because constant retries cost more than waiting for a lock.
Nó ngăn bất thường trong DB, nhưng đổi lại là lỗi 40001 khi xung đột. Ứng dụng phải chạy lại cả transaction, và mọi side effect bên ngoài (gửi email, gọi API) phải idempotent hoặc làm sau commit. Thông lượng cũng giảm khi tranh chấp cao.It prevents anomalies inside the DB, at the price of 40001 errors on conflict. The application must rerun the whole transaction, and external side effects (emails, API calls) must be idempotent or happen after commit. Throughput also drops under contention.
Kiểm tra trong code là check-then-act, luôn có khe hở giữa hai request. Unique constraint là lớp chặn cuối cùng do DB đảm bảo; code chỉ cần bắt lỗi vi phạm hoặc dùng ON CONFLICT.A check in code is check-then-act and always leaves a gap between two requests. The unique constraint is the last line of defence enforced by the DB; the code just handles the violation or uses ON CONFLICT.
Phím 1–4 để chọn đáp án, Enter để kiểm tra.Press 1–4 to choose an answer, Enter to check.
Ôn tập ngắt quãng (spaced repetition): thẻ được lặp lại sau 1 → 3 → 7 → 14 → 30 ngày để củng cố trí nhớ dài hạn.Spaced repetition: cards are repeated after 1 → 3 → 7 → 14 → 30 days to build long-term memory.