[PostgreSQL] 분산 우선순위 락 구현해보기

이전 포스트에서 확장되는 내용이다.
https://blog.naver.com/sssang97/224093632164

단순한 사용사례라면 그냥 모든 프로세스가 동등하게 경쟁하는 Lock으로도 충분할 수 있지만, 때때로는 Lock에 대한 점유 우선순위를 부여할 필요도 있다.
A와 B과 동시에 요청을 하면 A의 손을 먼저 들어줘야 하는 것이다.

일반 Lock에 비하면 구현이 번거롭긴 하지만, 이것도 구현은 가능하다.




테이블 설계

우선순위 Lock을 구현하는 것은 일반 Lock을 구현하는 것에 비하면 다소 복잡한 편이다.
Lock에 우선순위의 개념을 적용하려면, 우선순위 큐가 필요하기 때문이다.

이건 분산 Lock이 아니라 일반 Lock의 경우도 마찬가지다.

일단 lock 테이블이 필요하다.

		CREATE TABLE IF NOT EXISTS priority_locks (
			name TEXT PRIMARY KEY,
			owner TEXT NOT NULL,
			priority INTEGER NOT NULL,
			expires_at TIMESTAMPTZ NOT NULL
		);

그리고 경쟁의 우선순위를 관리할 큐 테이블은 다음과 같이 설계했다.

		CREATE TABLE IF NOT EXISTS lock_queue (
			lock_name TEXT NOT NULL,
			owner TEXT NOT NULL,
			priority INTEGER NOT NULL,
			requested_at TIMESTAMPTZ NOT NULL,
			heartbeat_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
			PRIMARY KEY (lock_name, owner)
		);

		CREATE INDEX IF NOT EXISTS idx_queue_priority
		ON lock_queue(lock_name, priority DESC, requested_at ASC);

priority가 높을수록 우선순위가 높은 것이다.
그리고 queue에 넣기만 한 채로 프로세스가 죽을 경우를 대비해서 heartbeat_at 필드도 추가했다. 저걸 초과하면 유효하지 않은 큐 메세지라고 가정하는 것이다.




TryLock, Lock 구현

좀 복잡한 편이다.
일단 TryLock은 lock과 queue 테이블 모두에 write를 해야 한다.

우선, INSERT를 기반으로 Lock 점유를 시도한다.

		INSERT INTO priority_locks (name, owner, priority, expires_at)
		VALUES ($1, $2, $3, NOW() + make_interval(secs => $4))
		ON CONFLICT (name) DO UPDATE
		SET owner = EXCLUDED.owner,
		    priority = EXCLUDED.priority,
		    expires_at = EXCLUDED.expires_at
		WHERE priority_locks.expires_at <= NOW()
		RETURNING expires_at;

이거 자체는 단순한 편이다.

하지만 Lock 점유에 실패했을 경우에는 queue에 대기 정보를 넣어주는 과정이 추가된다.

		INSERT INTO lock_queue (lock_name, owner, priority, requested_at)
		VALUES ($1, $2, $3, NOW())
		ON CONFLICT (lock_name, owner) DO UPDATE
		SET priority = EXCLUDED.priority, requested_at = EXCLUDED.requested_at;

Lock에서 블로킹 루프를 구현하는 부분도 좀 복잡해진다.

현재 대기열에서 내 Lock의 우선순위가 가장 높다면 TryLock을 바로 또 시도하고, 아니라면 그냥 하릴없이 대기하는 식으로 처리를 해줘야 한다.
그래서 더 높은 우선순위 대기자가 있다면 이건 TryLock을 시도해보지도 못하고 계속 밀린다.

그리고 queue가 살아있다는 것을 알리기 위해서 하트비트를 갱신하는 과정도 추가된다.

그 외 부분은 그다지 복잡하진 않다. 일반 락 구현과 거의 동등하다.

Unlock을 할때는 lock과 queue를 동시에 지우기만 하면 된다.



전체 코드는 github에 있다.
https://github.com/myyrakle/oddments/tree/master/Go_Example/database/postgresql/pg_lock