데이터베이스 병발은 가능한 한 간단합니다

22109 단어 tutorialsqlphpdatabase
본고에서 저는 은행 웹 응용 프로그램을 구축하여 요청을 처리하고 발송하는 기본 개념을 보여 드리겠습니다.코드를 작성할 때, 우리는 함정을 특히 주의해야 한다. 왜냐하면 이것은 테스트하기 쉬운 장면이 아니기 때문이다.

은행 계좌 적용 범위

우리의 예시 프로그램은 은행 계좌를 포함하고 한 계좌에서 다른 계좌로 이체를 허용할 것이다.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: 소스 계정 id
  • to: 목적지 계좌 id
  • amount: 양도 대기 금액
  • 100달러의 금액을 계좌 1에서 계좌 2로 이체하려면 다음과 같은 요청을 사용할 수 있습니다.
    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) 을 사용하여 응용 프로그램에 몇 가지 요청을 실행합니다.
    첫 번째 테스트에는 다음과 같은 장면이 있습니다.
  • Alice는 1000 amount
  • 로 시작
  • Bob 0으로 시작
  • Alice에서 Bob에 100번 전송
  • 예상되는 최종 결과:
    앨리스: 0
  • 밥: 1000
  • 다음 명령을 사용할 수 있습니다. 여기서 매개변수n는 총 요청 수입니다. c는 동시 요청 수입니다.
    ab -n 10 -c 1 'http://localhost:8000/move?from=1&to=2&amount=100'
    
    너는 지금 나를 믿어야 하지만, 나는 너에게 위의 명령을 실행한 후에 앨리스는 0, Bob은 1000이 있다는 것을 보증할 수 있다.
    두 번째는 비슷하지만 10개의 동시 요청을 수행합니다.
  • Alice는 1000 amount
  • 로 시작
  • Bob 0으로 시작
  • Alice에서 Bob
  • 로 100회 동시 전송
  • 예상되는 최종 결과:
    앨리스: 0
  • 밥: 1000
  • 매개변수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
    당신의 독서에 감사 드립니다. 나는 당신이 좋아하길 바랍니다.

    좋은 웹페이지 즐겨찾기