Ескертпе
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Жүйеге кіруді немесе каталогтарды өзгертуді байқап көруге болады.
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Каталогтарды өзгертуді байқап көруге болады.
Драйвер mssql-python поддерживает полный контроль над транзакциями, включая подтверждение, откат, настройку автокоммита и уровни изоляции транзакций.
Основы транзакций
Транзакция объединяет последовательность операций с базой данных в одну единицу работы. Сделки следуют свойствам ACID:
- Атомичность: Все операции либо успешны, либо все проваливаются.
- Согласованность: база данных остаётся в действительном состоянии.
- Изоляция: Одновременные транзакции не мешают друг другу.
- Долговечность: Зафиксированные изменения сохраняются после сбоев системы.
Режим автоматической фиксации транзакций
Параметр autocommit определяет, фиксируются ли изменения автоматически.
Автокоммит отключён (по умолчанию)
По умолчанию autocommit=False. Вы должны явно внести изменения.
import mssql_python
conn = mssql_python.connect(connection_string)
print(conn.autocommit) # False
cursor = conn.cursor()
cursor.execute("CREATE TABLE #TxnBasic (Name NVARCHAR(50))")
cursor.execute("INSERT INTO #TxnBasic (Name) VALUES ('Widget')")
cursor.execute("INSERT INTO #TxnBasic (Name) VALUES ('Gadget')")
# Changes are staged but not visible to other connections
conn.commit() # Now changes are permanent
conn.close()
Если вы не фиксируете изменения, драйвер отбрасывает их при закрытии соединения.
Включено автокоммит
Когда вы устанавливаете autocommit=True, драйвер сразу фиксирует каждое утверждение:
conn = mssql_python.connect(connection_string, autocommit=True)
# OR
conn.setautocommit(True)
cursor = conn.cursor()
cursor.execute("CREATE TABLE #AutoDemo (Name NVARCHAR(50))")
cursor.execute("INSERT INTO #AutoDemo (Name) VALUES ('Widget')")
# Immediately committed - no explicit commit needed
Предостережение
Когда включена автофиксация, вы не можете откатить несколько инструкций как единую группу. Используйте автокоммит только тогда, когда это подходит для вашего случая.
Фиксация и откат
Зафиксировать
Призыв commit() сделать ожидающие изменения постоянными:
cursor.execute("CREATE TABLE #CommitDemo (Name NVARCHAR(50), Price DECIMAL(10,2), CategoryID INT)")
cursor.execute("INSERT INTO #CommitDemo VALUES ('A',10,1),('B',20,1)")
cursor.execute("UPDATE #CommitDemo SET Price = Price * 1.1 WHERE CategoryID = 1")
conn.commit() # The update is now permanent
Откат
Чтобы отменить ожидающие изменения, позвоните rollback():
try:
cursor.execute("CREATE TABLE #RollDemo (Name NVARCHAR(50), Price DECIMAL(10,2), CategoryID INT)")
cursor.execute("INSERT INTO #RollDemo VALUES ('A',10,1),('B',20,1)")
cursor.execute("UPDATE #RollDemo SET Price = Price * 1.1 WHERE CategoryID = 1")
# Verify the update
cursor.execute("SELECT AVG(Price) FROM #RollDemo WHERE CategoryID = 1")
avg_price = cursor.fetchval()
if avg_price > 100:
conn.rollback() # Price too high, undo both updates
print("Rolled back: average price would exceed limit")
else:
conn.commit()
except Exception as e:
conn.rollback() # Undo on error
raise
Фиксация на уровне курсора и откат
Для удобства можно вызывать commit() и rollback() для курсоров:
cursor = conn.cursor()
cursor.execute("CREATE TABLE #CursorDemo (Name NVARCHAR(50))")
cursor.execute("INSERT INTO #CursorDemo (Name) VALUES ('Widget')")
cursor.commit() # Delegates to connection
cursor.execute("DELETE FROM #CursorDemo WHERE Name = 'Widget'")
cursor.rollback() # Delegates to connection
Замечание
Операции фиксации и отката, выполняемые через курсор, влияют на все курсоры в одном соединении, а не только на тот курсор, для которого вы вызываете эти операции.
Менеджеры контекста
Менеджер контекста соединения фиксирует транзакцию при нормальном завершении и откатывает её, если возникает исключение. Соединение всегда закрывается при выходе. Когда вы устанавливаете autocommit=True, вызовы commit и rollback не имеют никакого эффекта.
with mssql_python.connect(connection_string) as conn:
cursor = conn.cursor()
cursor.execute("CREATE TABLE #CtxDemo (Name NVARCHAR(50))")
cursor.execute("INSERT INTO #CtxDemo (Name) VALUES ('Widget')")
cursor.execute("INSERT INTO #CtxDemo (Name) VALUES ('Gadget')")
# Transaction is committed and connection is closed on exit
Если возникает исключение, транзакция откатывается:
try:
with mssql_python.connect(connection_string) as conn:
cursor = conn.cursor()
cursor.execute("CREATE TABLE #TxnDemo (Name NVARCHAR(50))")
cursor.execute("INSERT INTO #TxnDemo (Name) VALUES ('Widget')")
raise ValueError("Something went wrong")
except ValueError:
pass
# Transaction is rolled back and connection is closed on exit
Уровни изоляции транзакций
Уровни изоляции контролируют, как транзакции взаимодействуют с параллельными транзакциями. Установить уровень изоляции с помощью set_attr():
import mssql_python
conn = mssql_python.connect(connection_string)
# Set isolation level
conn.set_attr(
mssql_python.SQL_ATTR_TXN_ISOLATION,
mssql_python.SQL_TXN_SERIALIZABLE
)
Доступные уровни изоляции
| Постоянный | Описание |
|---|---|
SQL_TXN_READ_UNCOMMITTED |
Может читать неподтверждённые изменения других транзакций (допускаются грязные чтения) |
SQL_TXN_READ_COMMITTED |
Читает только фиксированные данные (по умолчанию в SQL Server) |
SQL_TXN_REPEATABLE_READ |
Гарантирует единообразные считывания внутри транзакции |
SQL_TXN_SERIALIZABLE |
Максимальная изоляция; Транзакции, по-видимому, выполняются последовательно |
Выберите уровень изоляции
| Сценарий использования | Рекомендуемый уровень |
|---|---|
| Общие нагрузки OLTP |
READ_COMMITTED (по умолчанию) |
| Отчёты, которым нужны последовательные снимки |
REPEATABLE_READ или снимок |
| Финансовые расчёты, требующие точности | SERIALIZABLE |
| Нагрузки с преобладанием операций чтения, допускающие устаревание данных | READ_UNCOMMITTED |
Изоляция моментальных снимков
Для выделения снимков используйте Transact-SQL (T-SQL). Изоляция снимков использует версионирование строк в tempdb, что может увеличить требования к хранилищу при больших нагрузках на запись.
# Enable snapshot isolation on the database (one-time setup, requires autocommit)
conn.commit()
conn.autocommit = True
cursor.execute("ALTER DATABASE AdventureWorks2022 SET ALLOW_SNAPSHOT_ISOLATION ON")
# Set isolation level while still in autocommit, then start the transaction
cursor.execute("SET TRANSACTION ISOLATION LEVEL SNAPSHOT")
conn.autocommit = False
cursor.execute("SELECT TOP 5 Name, ListPrice FROM Production.Product")
rows = cursor.fetchall()
for row in rows:
print(row.Name, row.ListPrice)
conn.commit()
Вложенные транзакции и точки сохранения
SQL Server поддерживает точки сохранения для частичного отката в рамках транзакции.
cursor = conn.cursor()
cursor.execute("BEGIN TRANSACTION")
cursor.execute("CREATE TABLE #SaveDemo (Name NVARCHAR(50))")
cursor.execute("INSERT INTO #SaveDemo (Name) VALUES ('Widget')")
cursor.execute("SAVE TRANSACTION SaveDemoPoint")
cursor.execute("INSERT INTO #SaveDemo (Name) VALUES ('Gadget')")
# Roll back to savepoint, keeping first insert
cursor.execute("ROLLBACK TRANSACTION SaveDemoPoint")
cursor.execute("COMMIT TRANSACTION")
Управление взаимоблокировками
Тупики возникают, когда две сделки ждут блокировки друг друга. SQL Server автоматически обнаруживает тупиковые блокировки и завершает одну из транзакций.
import time
def execute_with_retry(conn, cursor, sql, params=None, max_retries=3):
"""Execute SQL with deadlock retry logic."""
for attempt in range(max_retries):
try:
cursor.execute(sql, params)
return
except mssql_python.OperationalError as e:
if "1205" in str(e): # Deadlock error number
if attempt < max_retries - 1:
conn.rollback() # Clear the failed transaction
time.sleep(0.1 * (2 ** attempt)) # Exponential backoff
continue
raise
raise Exception(f"Failed after {max_retries} attempts")
Лучшие практики
Держите транзакции короткими , чтобы минимизировать длительность блокировки и возможность тупиков.
Используйте autocommit=False для транзакций, включающих несколько инструкций, которые должны быть атомарными.
Всегда обрабатывайте исключения с откатом:
conn = None try: conn = mssql_python.connect(connection_string) cursor = conn.cursor() cursor.execute("CREATE TABLE #RollbackPattern (ID INT, Name NVARCHAR(50))") cursor.execute("INSERT INTO #RollbackPattern (ID, Name) VALUES (1, 'Widget')") cursor.execute("UPDATE #RollbackPattern SET Name = 'Updated Widget' WHERE ID = 1") conn.commit() except Exception: if conn is not None: conn.rollback() raise finally: if conn is not None: conn.close()Используйте контекстные менеджеры для автоматического управления транзакциями. Контекстный менеджер фиксирует изменения при штатном завершении и выполняет откат при возникновении исключения.
with mssql_python.connect(connection_string) as conn: cursor = conn.cursor() cursor.execute("CREATE TABLE #ContextManagerDemo (ID INT, Name NVARCHAR(50))") cursor.execute("INSERT INTO #ContextManagerDemo (ID, Name) VALUES (1, 'Widget')") cursor.execute("UPDATE #ContextManagerDemo SET Name = 'Committed Widget' WHERE ID = 1") # Committed automatically on exitВыбирайте подходящие уровни изоляции , исходя из ваших требований к стабильности и производительности.
Используйте подсказки блокировки для паттернов чтения-модификации и записи , чтобы предотвратить потерю обновлений. Когда вы читаете значение, которое будет обновляться в рамках той же транзакции, используйте подсказки, например
WITH (UPDLOCK, ROWLOCK)на SELECT, чтобы получить замки на раннем этапе и установить единый порядок замков, что снижает риск тупиков.# Good: Acquire lock during read to prevent lost update pattern cursor.execute(""" SELECT Balance FROM Accounts WITH (UPDLOCK, ROWLOCK) WHERE ID = %(id)s """, {"id": account_id}) balance = cursor.fetchval() if balance >= amount: cursor.execute(""" UPDATE Accounts SET Balance = Balance - %(amount)s WHERE ID = %(id)s """, {"amount": amount, "id": account_id})Реализуйте повторную логику при временных сбоях, таких как тупиковые блокировки.
Пример: Перевод средств (атомная операция)
Этот пример демонстрирует атомарную логику передачи с подсказками блокировки, чтобы предотвратить потерю обновлений в одновременных сценариях:
def transfer_funds(conn, from_account, to_account, amount):
"""Transfer funds atomically between accounts."""
cursor = conn.cursor()
try:
# Read balance with lock hint to prevent lost updates
cursor.execute(
"SELECT Balance FROM Accounts WITH (UPDLOCK, ROWLOCK) WHERE AccountID = %(account_id)s",
{"account_id": from_account}
)
balance = cursor.fetchval()
if balance is None:
raise ValueError(f"Source account {from_account} not found")
if balance < amount:
raise ValueError("Insufficient funds")
# Debit source account
cursor.execute(
"UPDATE Accounts SET Balance = Balance - %(amount)s WHERE AccountID = %(account_id)s",
{"amount": amount, "account_id": from_account}
)
# Credit destination account
cursor.execute(
"UPDATE Accounts SET Balance = Balance + %(amount)s WHERE AccountID = %(account_id)s",
{"amount": amount, "account_id": to_account}
)
if cursor.rowcount != 1:
raise ValueError(f"Destination account {to_account} not found")
conn.commit()
print(f"Transferred ${amount} from {from_account} to {to_account}")
except Exception:
conn.rollback()
raise
Блокировочные подсказки WITH (UPDLOCK, ROWLOCK) в операторе SELECT гарантируют, что блокировка устанавливается заранее. Это предотвращает одновременное чтение того же баланса другой транзакции и создание сценария потерянного обновления, при котором обе транзакции считывают старый баланс, выполняют отдельные обновления, и сохраняется только последнее обновление.
Область видимости временной таблицы
Временные таблицы (#tablename) ограничены сессией, но их создание является частью текущей транзакции. Если вы создаёте временную таблицу и транзакция откатывается, временная таблица удаляется:
conn = mssql_python.connect(connection_string) # autocommit=False
cursor = conn.cursor()
cursor.execute("CREATE TABLE #Staging (ID INT, Name NVARCHAR(50))")
cursor.execute("INSERT INTO #Staging VALUES (1, 'test')")
# Rollback removes the temp table entirely
conn.rollback()
# This fails: Invalid object name '#Staging'
try:
cursor.execute("SELECT * FROM #Staging")
except mssql_python.ProgrammingError:
print("Temp table was dropped by rollback")
Чтобы временная таблица была независима от вашей транзакции с данными, сделайте коммит после её создания:
cursor.execute("CREATE TABLE #Staging (ID INT, Name NVARCHAR(50))")
conn.commit() # Temp table persists regardless of later rollbacks
# Now data operations can roll back without losing the table
cursor.execute("INSERT INTO #Staging VALUES (1, 'test')")
conn.rollback() # Data is gone, but #Staging still exists
DDL-операторы, требующие автоподтверждения
Некоторые DDL-операторы, такие как CREATE DATABASE, ALTER DATABASE, и DROP DATABASE, не могут выполняться внутри транзакции. Установите autocommit=True перед их выполнением:
conn.autocommit = True
cursor.execute("CREATE DATABASE TestDB")
conn.autocommit = False # Return to transactional mode
Если вы запускаете CREATE DATABASE с autocommit=False, вы получаете ошибку: CREATE DATABASE statement not allowed within multi-statement transaction.