[MongoDB] MongoDB Transaction

기훈·2024년 3월 23일

MongoDB

목록 보기
20/28
  • 4.0부터 지원하는 기능이다.
  • Transaction을 사용하겠다는 것은 컬렉션을 정규화 하겠다는 의미이다. 그렇기에 굳이 사용을 권장하지 않는다. 몽고디비의 특성과 맞지 않기 때문이다. (정규화를 할 것이면 처음부터 RDBMS를 고려하자)

Transaction을 사용하는이유는 은행 송금 서비스나, 예약 시스템처럼 여러 작업에 대하여 하나의 논리적인 단위로 묶어 원자성을 보장하기 위함이다.
RDBMS 같은 경우 lock을 사용하여 Transaction을 구현하지만,
MongoDB는 같은 오브젝트를 참고할 경우 에러를 발생한다. (4.2부터 에러가 발생했을 때, 에러를 발생시키고 retry를 시키는 부분을 내부에서 처리한다)
따라서 Transaction을 사용하면 성능이 저하되는 문제가 발생한다.

  • 에러가 발생하고 복구하는 로직을 직접 구현한 코드

    from pymongo import MongoClient
    from pymongo.read_concern import ReadConcern
    from pymongo.write_concern import WriteConcern
    from pymongo.errors import ConnectionFailure, OperationFailure
    import certifi
    
    # 몽고디비는 트랜잭션을 추천하지 않는다.
    # 몽고디비는 같은 오브젝트를 참조하면 에러를 발생시킨다. -> 복구에 대한 비용이 매우 크다
    # 4.0 버전에서 직접 retry를 구현해본다.
    conn = "mongodb+srv://mongodb_user:__PWD__@cluster0.v6fiw3s.mongodb.net/"
    client = MongoClient(conn, tlsCAFile=certifi.where())
    
    # client.test.orders.drop()
    # client.test.inventory.drop()
    # client.test.inventory.insert_one({"name": "pencil", "qty": 1000})
    
    def update_orders_and_inventory(session):
        orders = session.client.test.orders
        inventory = session.client.test.inventory
    
        with session.start_transaction(read_concern=ReadConcern('majority'), write_concern=WriteConcern(w='majority')):
            order = 100
            orders.insert_one(
                {"name": "pencil", "qty": order}, 
                session=session
            )
            inventory.update_one(
                {
                    "name": "pencil",
                    "qty": {"$gte": order}
                },
                {
                    "$inc": {"qty": order * -1}
                }
            )
            commit_with_retry(session)
    
    def commit_with_retry(session):
        while True:
            try:
                session.commit_transaction()
                print("Transaction Commited.")
                print(session.client.test.orders.find_one({"name": "pencil"}))
                print(session.client.test.inventory.find_one({"name": "pencil"}))
                break
            except (ConnectionFailure, OperationFailure) as err:
                if err.has_error_label("UnknownTransactionCommitResult"):
                    print("UnknownTransactionCommitResult, retrying commit operation...")
                    continue
                else:
                    print("Error during commit...")
                    raise
    
    def run_transaction_with_retry(transaction_func, session):
        while True:
            try:
                transaction_func(session)
                break
            except (ConnectionFailure, OperationFailure) as err:
                # retry가 필요한 오류가 발생한 경우 while문을 계속 진행한다.
                if err.has_error_label("TransientTransactionError"):
                    print("TransientTransactionError, retryinh transaction...")
                    continue
                else:
                    raise
    
    # 세션을 열고 트랜잭션 실행한다.
    with client.start_session() as session:
        try:
            run_transaction_with_retry(update_orders_and_inventory, session)
        except:
            raise
    
  • 에러처리를 데이터베이스 내부에서 직접 처리하는 코드

    from pymongo import MongoClient
    from pymongo.read_concern import ReadConcern
    from pymongo.write_concern import WriteConcern
    import certifi
    
    conn = "mongodb+srv://mongodb_user:__PWD__@cluster0.v6fiw3s.mongodb.net/"
    client = MongoClient(conn, tlsCAFile=certifi.where())
    
    # client.test.orders.drop()
    # client.test.inventory.drop()
    # client.test.inventory.insert_one({"name": "pencil", "qty": 1000})
    
    def callback(session):
        orders = session.client.test.orders
        inventory = session.client.test.inventory
        order = 200
    
        orders.insert_one(
            {"name": "pencil", "qty": order}, 
            session=session
        )
        inventory.update_one(
            {
                "name": "pencil",
                "qty": {"$gte": order}
            },
            {"$inc": {"qty": order * -1}},
            session=session
        )
    
    with client.start_session() as session:
        session.with_transaction(callback, read_concern=ReadConcern('majority'), write_concern=WriteConcern('majority'))
        print(session.client.test.orders.find_one({"name": "pencil"}))
        print(session.client.test.inventory.find_one({"name": "pencil"}))
    

0개의 댓글