데이터베이스 병발은 가능한 한 간단합니다
은행 계좌 적용 범위
우리의 예시 프로그램은 은행 계좌를 포함하고 한 계좌에서 다른 계좌로 이체를 허용할 것이다.PHP, Symfony, ORM을 사용하여 구축되었지만, 이러한 기술을 익힐 필요는 없고, Postgres 데이터베이스만 익히면 됩니다.
계정 실체
은행 계좌는 소유자의 성명과 최종 금액을 저장할 것이다.
CREATE TABLE "public"."bank_account" (
"id" int4 NOT NULL,
"name" varchar(255) NOT NULL,
"amount" int4 NOT NULL,
PRIMARY KEY ("id")
);
이체 API
두 계정 간 이체의 끝점은 조회 매개 변수를 통해 3개의 변수를 수신합니다.
from
: 소스 계정 idto
: 목적지 계좌 idamount
: 양도 대기 금액http://localhost:8000/move?from=1&to=2&amount=100
계정 저장소
이러한 끝점을 지원하기 위해 다음 저장소를 사용합니다.
class BankAccountRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, BankAccount::class);
}
public function transferAmount($from, $to, $amount): void
{
// Fetches both account entities to be update
$fromAccount = $this->find($from);
$toAccount = $this->find($to);
// Updates the amount on each of them
$fromAccount->setAmount($fromAccount->getAmount() - $amount);
$toAccount->setAmount($toAccount->getAmount() + $amount);
// Persist both entities
$this->getEntityManager()->persist($fromAccount);
$this->getEntityManager()->persist($toAccount);
$this->getEntityManager()->flush();
}
}
SQL에서는 다음과 같은 작업을 수행합니다(가독성을 위해 규칙으로 생성된 SQL을 편집함).SELECT * FROM bank_account WHERE id = 1; # source
SELECT * FROM bank_account WHERE id = 2; # destination
START TRANSACTION;
UPDATE bank_account SET amount = ? WHERE id = 1; # source
UPDATE bank_account SET amount = ? WHERE id = 2; # destination
COMMIT;
계정 관리자
컨트롤러 쪽에서, 우리는 조회 파라미터를 분석하고 저장소를 호출하기만 하면 된다.
class BankAccountController extends AbstractController
{
#[Route('/move', name: 'bank_account')]
public function transfer(Request $request, BankAccountRepository $repository): Response
{
$from = $request->query->get('from');
$to = $request->query->get('to');
$amount = $request->query->get('amount');
$repository->transferAmount($from, $to, $amount);
return new Response(sprintf('from %s to %s amount %s', $from, $to, $amount));
}
}
테스트해 봅시다!
이제 데이터베이스에 테스트 계정을 생성합니다.
INSERT INTO "public"."bank_account" ("id", "name", "amount") VALUES
(1, 'Alice', 1000),
(2, 'Bob', 0);
그런 다음 100을 Alice에서 Bob으로 이동합니다.curl http://localhost:8000/move?from=1&to=2&amount=100
데이터베이스를 검사하고 모든 것이 정상입니다.| id | name | amount |
|----|-------|--------|
| 1 | Alice | 900 |
| 2 | Bob | 100 |
간단하죠?우리는 이 실현을 강조할 수 있다. 단원 테스트, 집적 테스트를 작성하면 모든 것이 정상적으로 작동할 것이다.그게 왜?
문제를 확인하기 위해서 Apache HTTP server benchmarking tool (ab) 을 사용하여 응용 프로그램에 몇 가지 요청을 실행합니다.
첫 번째 테스트에는 다음과 같은 장면이 있습니다.
앨리스: 0
n
는 총 요청 수입니다. c
는 동시 요청 수입니다.ab -n 10 -c 1 'http://localhost:8000/move?from=1&to=2&amount=100'
너는 지금 나를 믿어야 하지만, 나는 너에게 위의 명령을 실행한 후에 앨리스는 0, Bob은 1000이 있다는 것을 보증할 수 있다.두 번째는 비슷하지만 10개의 동시 요청을 수행합니다.
앨리스: 0
c
가 10으로 변경되었습니다.ab -n 10 -c 10 'http://localhost:8000/move?from=1&to=2&amount=100'
그다지 좋지 않은 결과는:| id | name | amount |
|----|-------|--------|
| 1 | Alice | 300 |
| 2 | Bob | 700 |
그런데 왜요?기본적으로 일부 프로세스는 데이터 양을 업데이트하고, 일부 프로세스는 오래된 데이터 양을 읽고 메모리에 저장합니다.두 개의 동시 프로세스 A와 B가 Alice 계정만 업데이트할 것으로 예상합니다.
1-프로세스 A가 Alice 계정에서 1000 읽기
2-프로세스 B Alice 계정에서 1000 읽기
3. - 프로세스 A가 Alice 계정에 900개를 적었어요.
4. 프로세스 B가 Alice 계정에 900을 썼어요. (800일 거예요. 창피해요!)
그럼 해결책은 뭐예요?
한 가지 해결 방안이 있는 것이 아니지만, 읽기와 쓰기에 대해 비관적인 잠금을 사용하는 해결 방안을 보여 드리겠습니다.이것은 데이터베이스가 모든 자원을 한 번만 읽거나 쓸 수 있음을 의미하며, 이 예에서는account 실체이다.
원칙적으로 우리는 다음과 같은 코드를 사용하여 이 점을 실현할 수 있다.
public function transferAmountConcurrently($from, $to, $amount): void
{
$this->getEntityManager()->beginTransaction();
$fromAccount = $this->find($from, LockMode::PESSIMISTIC_WRITE);
$toAccount = $this->find($to, LockMode::PESSIMISTIC_WRITE);
$fromAccount->setAmount($fromAccount->getAmount() - $amount);
$toAccount->setAmount($toAccount->getAmount() + $amount);
$this->getEntityManager()->persist($fromAccount);
$this->getEntityManager()->persist($toAccount);
$this->getEntityManager()->flush();
$this->getEntityManager()->commit();
}
우리는 지금 자물쇠를 얻기 전에 업무를 현저하게 시작해야 한다. 이것은 의미가 있다. 왜냐하면 업무가 언제 시작되는지 알 수 없기 때문이다.마지막으로, SQL 정보:
START TRANSACTION;
SELECT * FROM bank_account WHERE id = 1 FOR UPDATE; # source
SELECT * FROM bank_account WHERE id = 2 FOR UPDATE; # destination
UPDATE bank_account SET amount = ? WHERE id = 1; # source
UPDATE bank_account SET amount = ? WHERE id = 2; # destination
COMMIT;
테스트의 경우 이전에 생성된 BankAccountController
에 새 엔드포인트를 생성합니다.#[Route('/move-concurrently', name: 'bank_account_concurrent')]
public function transferConcurrently(Request $request, BankAccountRepository $repository): Response
{
$from = $request->query->get('from');
$to = $request->query->get('to');
$amount = $request->query->get('amount');
$repository->transferAmountConcurrently($from, $to, $amount);
return new Response(sprintf('from %s to %s amount %s', $from, $to, $amount));
}
이제 Apache 벤치마크 테스트 도구를 사용하여 테스트를 수행할 수 있습니다.ab -n 10 -c 10 'http://localhost:8000/move-concurrently?from=1&to=2&amount=100'
나를 믿어라, 그것은 지금 작용했다. 앨리스는 0, 밥은 1000이다.결말
자물쇠 정책을 사용하면 자물쇠를 가져오는 과정은 최신 값을 읽고 지난번에 읽은 업데이트와 일치하는 데이터를 기반으로 하는 것이라고 보장합니다.최종 코드는 Github에 있습니다.
fabiothiroki / symfony-bank-transaction
당신의 독서에 감사 드립니다. 나는 당신이 좋아하길 바랍니다.
Reference
이 문제에 관하여(데이터베이스 병발은 가능한 한 간단합니다), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/fabiothiroki/database-concurrency-as-simple-as-possible-18g1텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)