В днешната статия ще разгледаме някои по-напреднали техники за работа с бази данни в Python, фокусирайки се върху асинхронния достъп до множество източници на данни. Ще обсъдим предизвикателствата, които възникват при поддържането на консистентност на данните в разпределена среда, и ще покажем практически примери, използвайки популярни библиотеки като SQLAlchemy и databases.

Защо асинхронен достъп до бази данни?

В модерните уеб приложения е често срещано да се налага достъп до различни бази данни или услуги за съхранение. Например, може да използваме релационна база данни като PostgreSQL за структурираните данни, NoSQL решение като MongoDB за по-гъвкаво съхранение на документи и Redis за кеширане и управление на сесии.

Традиционният синхронен подход за достъп до тези ресурси може да доведе до блокиране на цялото приложение, докато се чака заявка към базата данни да приключи. В натоварени системи с много конкурентни заявки, това може сериозно да навреди на производителността и мащабируемостта.

Асинхронното програмиране предлага решение на този проблем, позволявайки на приложението да обработва други задачи, докато чака заявките към базите данни да завършат. Python предоставя няколко опции за писане на асинхронен код, като asyncio и async/await синтаксиса, въведен в Python 3.5.

Асинхронен достъп до бази данни с SQLAlchemy и databases

Нека разгледаме практически пример за асинхронна работа с релационна и документна база данни, използвайки SQLAlchemy ORM и библиотеката databases.

Първо, да инсталираме необходимите библиотеки:


# Install the dependencies
pip install databases[postgresql,mongodb] SQLAlchemy

Ще създадем прост модел на потребител с полета, съхранявани в PostgreSQL и MongoDB:


from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String)

Сега да дефинираме функция за създаване на нов потребител, която асинхронно записва данните в двете бази:


import asyncio
from databases import Database

async def create_user(name, email):
    # Make async connection to PostgreSQL
    pg_db = Database('postgresql://user:password@localhost/myapp') 
    await pg_db.connect()

    # Make async connection to MongoDB
    mongo_db = Database('mongodb://localhost:27017/myapp')
    await mongo_db.connect()

    # Save to PostgreSQL
    query = User.__table__.insert().values(name=name, email=email)
    user_id = await pg_db.execute(query)

    # Save to MongoDB
    await mongo_db.execute('users', 'insert_one', {
        '_id': user_id,
        'name': name,
        'email': email
    })

    # Close the connection (very important)
    await pg_db.disconnect()
    await mongo_db.disconnect()

    return user_id

Функцията create_user първо установява асинхронни връзки към PostgreSQL и MongoDB, използвайки удобния API на databases. След това извършва INSERT заявка в PostgreSQL таблицата users, връщайки автоматично генерираното ID. Същият запис се извършва и в MongoDB колекцията users, използвайки полученото ID като _id.

Накрая връзките се затварят и потребителското ID се връща като резултат.

Ето как можем да извикаме тази функция в асинхронен контекст:


async def main():
    user_id = await create_user('John Doe', '[email protected]')
    print(f"Created user with ID: {user_id}")

asyncio.run(main())

Осигуряване на консистентност на данните

Един от основните проблеми при записа в множество бази данни е поддържането на консистентност на данните. Какво се случва, ако записът в едната база е успешен, а в другата възникне грешка? Може да се окажем в състояние на несъответствие между двата източника на данни.

Има различни подходи за справяне с този проблем, всеки със своите компромиси:

  1. Двуфазов къмит (Two-phase commit): Разпределена транзакция, която осигурява консистентност чрез къмит на промените във всички бази едновременно. Недостатъкът е, че изисква допълнителна координация между участниците и може да има лошо влияние върху производителността.
  2. Компенсиращи транзакции (Compensating transactions): Вместо да се опитваме да постигнем перфектна консистентност, можем да допуснем временни несъответствия и да ги коригираме с последващи компенсиращи действия. Например, ако записът в MongoDB се провали, можем да изтрием или обновим вече създадения запис в PostgreSQL.
  3. Опашки със съобщения (Message queues): Чрез използването на опашка като Apache Kafka или RabbitMQ, можем да постигнем така наречената „евентуална консистентност“. Вместо директен запис в базите данни, изпращаме събитие в опашката, което впоследствие се консумира от отделни работници (consumers) за всяка база. Така дори някой запис временно да се провали, съобщенията остават в опашката и могат да бъдат повторно обработени.

Изборът на подходяща стратегия зависи от конкретните изисквания за консистентност и наличните ресурси в нашето приложение.

Както виждате, асинхронната работа с множество бази данни крие своите предизвикателства, но и отваря вратата към по-мащабируеми и отзивчиви приложения. С правилните инструменти и добре обмислена стратегия за съгласуваност на данните, можем да се възползваме максимално от този подход.

Надявам се тази статия да ви е дала храна за размисъл и да сте научили нещо ново за асинхронния достъп до бази данни в Python. Не се колебайте да експериментирате и да прилагате тези концепции в своите проекти.

Last Update: май 3, 2024