Nhiều instance cùng xử lý một tài nguyênMany instances working on one resource
Scheduler kích hoạt job trên mọi instance. Instance nào SET NX thành công thì giữ khoá và nhận fencing token tăng dần; storage chỉ chấp nhận ghi có token lớn hơn token đã lưu. Bật “GC pause” để xem vì sao chỉ có TTL là chưa đủ.The scheduler fires the job on every instance. Whoever wins SET NX holds the lock and gets an increasing fencing token; the storage only accepts writes whose token is higher than the stored one. Turn on “GC pause” to see why a TTL alone is not enough.
1Cron kích hoạt job đối soát trên cả hai instance cùng lúc.Cron fires the settlement job on both instances at the same moment.
DEL thẳng sau khi TTL đã hết sẽ xoá khoá của instance khác. Lưu token ngẫu nhiên làm value và nhả bằng Lua: GET so token rồi mới DEL, trong một lệnh nguyên tử.A plain DEL after the TTL expired deletes another instance's lock. Store a random token as the value and release with Lua: compare it, then DEL, atomically.
Quá ngắn: khoá hết hạn khi việc còn chạy. Quá dài: instance chết thì mọi người chờ đến hết TTL. Đặt TTL vừa phải và gia hạn định kỳ (watchdog của Redisson gia hạn mỗi lockWatchdogTimeout/3).Too short and it expires mid-work; too long and everyone waits after a crash. Keep it moderate and renew it (Redisson's watchdog renews every lockWatchdogTimeout/3).
Một tiến trình có thể dừng lâu hơn TTL hoặc đồng hồ máy chạy lệch, nên không bao giờ chắc chắn mình còn giữ khoá. Storage phải kiểm tra fencing token tăng dần và từ chối token cũ.A process can stall past the TTL or run on a drifting clock, so it can never be sure it still holds the lock. The storage must check an increasing fencing token and refuse stale ones.
Một đơn chỉ chi trả một lần thì unique constraint hoặc optimistic locking (cột version) là đủ. Chia hàng việc cho nhiều worker thì dùng SELECT … FOR UPDATE SKIP LOCKED.One payout per order is a unique constraint or optimistic locking (a version column). Splitting a work queue across workers is SELECT … FOR UPDATE SKIP LOCKED.
Replication bất đồng bộ: master chết trước khi key sang replica thì replica mới không có khoá. Redlock dùng đa số 5 node nhưng vẫn dựa vào thời gian; việc cần đúng tuyệt đối nên dùng etcd/ZooKeeper hoặc advisory lock của PostgreSQL.Replication is asynchronous: if the master dies before the key reaches a replica, the new master has no lock. Redlock uses a majority of 5 nodes but still relies on timing; for strict correctness use etcd/ZooKeeper or PostgreSQL advisory locks.
Khoá loại trừ cho cả việc đọc làm nghẽn không cần thiết. RReadWriteLock của Redisson cho nhiều người đọc cùng lúc và chỉ một người ghi.An exclusive lock for reads serialises traffic for nothing. Redisson's RReadWriteLock allows many readers at once and a single writer.
| BảngTable | Cột chínhKey columns | Ghi chúNotes |
|---|---|---|
lock:{resource} | value = owner token (UUID), PX ttl | SET NX PX; nhả bằng Lua so token rồi DELSET NX PX; released by a Lua compare-and-delete |
fence:{resource} | value = counter (INCR) | Fencing token tăng dần, cấp mỗi lần lấy khoáMonotonic fencing token, issued on every acquisition |
payout | order_id, amount, status, fence, paid_at | unique(order_id); chỉ cập nhật khi fence < token mớiunique(order_id); updated only when fence < the new token |
job_run | job_name, owner, fence, started_at, finished_at, status | Lịch sử ai chạy job, để tra cứu và cảnh báoWho ran the job, for auditing and alerts |
Java dùng Redisson tryLock không chờ, có leaseTime; Go tự viết SET NX + Lua compare-and-delete. Cả hai cấp fencing token và để storage từ chối token cũ.Java uses Redisson tryLock with no wait and a lease time; Go writes SET NX plus a Lua compare-and-delete. Both issue a fencing token and let the storage refuse stale ones.
@Slf4j
@Component
@RequiredArgsConstructor
public class PayoutJob {
private final RedissonClient redisson;
private final PayoutRepository payouts;
@Scheduled(cron = "0 */5 * * * *")
public void run() throws InterruptedException {
RLock lock = redisson.getLock("lock:payout");
if (!lock.tryLock(0, 30, TimeUnit.SECONDS)) { // wait 0, lease 30s
log.info("payout job held by another instance, skipping");
return;
}
try {
long fence = redisson.getAtomicLong("fence:payout").incrementAndGet();
for (Payout p : payouts.findPending()) {
if (payouts.markPaid(p.getOrderId(), fence) == 0) {
throw new StaleFenceException(fence); // a newer owner exists
}
}
} finally {
if (lock.isHeldByCurrentThread()) lock.unlock();
}
}
}
public interface PayoutRepository extends JpaRepository<Payout, Long> {
@Modifying
@Transactional
@Query("""
UPDATE Payout p SET p.status = 'PAID', p.fence = :fence
WHERE p.orderId = :orderId AND p.fence < :fence""")
int markPaid(String orderId, long fence);
}var release = redis.NewScript(
"if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0")
var ErrStaleFence = errors.New("stale fencing token")
type Lock struct {
rdb *redis.Client
key string
token string
Fence int64
}
// Acquire returns nil, nil when another instance holds the lock.
func Acquire(ctx context.Context, rdb *redis.Client, key string, ttl time.Duration) (*Lock, error) {
token := uuid.NewString()
ok, err := rdb.SetNX(ctx, key, token, ttl).Result() // SET key token NX PX ttl
if err != nil || !ok {
return nil, err
}
fence, err := rdb.Incr(ctx, "fence:"+key).Result()
if err != nil {
_ = release.Run(ctx, rdb, []string{key}, token).Err()
return nil, err
}
return &Lock{rdb: rdb, key: key, token: token, Fence: fence}, nil
}
func (l *Lock) Release(ctx context.Context) error {
return release.Run(ctx, l.rdb, []string{l.key}, l.token).Err()
}
func MarkPaid(ctx context.Context, db *pgxpool.Pool, orderID string, fence int64) error {
tag, err := db.Exec(ctx,
"UPDATE payout SET status = 'PAID', fence = $2 WHERE order_id = $1 AND fence < $2",
orderID, fence)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return ErrStaleFence
}
return nil
}| Redis (SET NX PX / Redisson)Redis (SET NX PX / Redisson) | PostgreSQL advisory lockPostgreSQL advisory lock | |
|---|---|---|
| Hết hạnExpiry | Theo TTL, cần gia hạn khi việc dàiBy TTL, needs renewal for long work | Tự nhả khi session/kết nối đóngReleased when the session or connection ends |
| Độ đúngCorrectness | Có thể mất khoá khi failover; cần fencing tokenCan lose the lock on failover; needs fencing tokens | Cùng DB với dữ liệu, có thể dùng trong transaction (pg_try_advisory_xact_lock)Same DB as the data, can live inside a transaction (pg_try_advisory_xact_lock) |
| Chi phíCost | Rất nhanh, tải lớn không ảnh hưởng DBVery fast, keeps load off the DB | Giữ một kết nối suốt thời gian khoáHolds a connection for the whole lock |
| Chọn khiPick it when | Đã có Redis, khoá ngắn, nhiều tài nguyênRedis is already there, short locks, many resources | Dữ liệu nằm trong PostgreSQL, vài job định kỳThe data lives in PostgreSQL, a few scheduled jobs |
Mỗi pattern mở bài học tương ứng trong menu Pattern.Each one opens its lesson in the Patterns menu.
Instance chết giữa hai lệnh thì key không có TTL và khoá bị giữ mãi. SET key value NX PX ttl làm cả hai trong một lệnh nguyên tử.If the instance dies between them the key has no TTL and the lock is held forever. SET key value NX PX ttl does both atomically.
Kleppmann chỉ ra Redlock dựa vào giả định thời gian: GC pause, network delay, lệch đồng hồ đều có thể khiến hai bên cùng nghĩ mình giữ khoá, và Redlock không cấp fencing token. Dùng được cho tối ưu hiệu năng; việc cần đúng tuyệt đối phải có fencing token hoặc dùng hệ đồng thuận như etcd/ZooKeeper.Kleppmann showed Redlock relies on timing assumptions: GC pauses, network delays and clock drift can leave two clients believing they hold the lock, and Redlock issues no fencing token. It is fine for efficiency; for correctness you need fencing tokens or a consensus system such as etcd/ZooKeeper.
Khi DB tự đảm bảo được: unique constraint, optimistic locking với cột version, hoặc SELECT … FOR UPDATE SKIP LOCKED để chia việc. Các cách này đúng theo transaction, không phụ thuộc TTL.When the DB can enforce it: a unique constraint, optimistic locking with a version column, or SELECT … FOR UPDATE SKIP LOCKED to share work. They are transactional and do not depend on a TTL.
Chúng ghi qua đồng thuận (Raft/ZAB) nên không mất khoá khi failover; khoá gắn với lease/ephemeral node và có revision/zxid tăng dần dùng làm fencing token sẵn.They write through consensus (Raft/ZAB), so a failover does not lose the lock; locks are tied to a lease or ephemeral node, and the revision/zxid already serves as a fencing token.
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.