transaction.py 1.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. """DMS事务、行锁和并发扩展入口。"""
  2. from __future__ import annotations
  3. from collections.abc import Callable, Generator
  4. from contextlib import contextmanager
  5. from time import sleep
  6. from typing import Any, TypeVar
  7. from sqlalchemy import Select
  8. from sqlalchemy.exc import OperationalError
  9. from sqlalchemy.orm import Session
  10. from dms.extensions import db
  11. T = TypeVar("T")
  12. MYSQL_RETRYABLE_ERROR_CODES = {1205, 1213}
  13. @contextmanager
  14. def transaction() -> Generator[Session, None, None]:
  15. """提供统一提交和异常回滚边界。"""
  16. session = db.session
  17. try:
  18. yield session
  19. session.commit()
  20. except Exception:
  21. session.rollback()
  22. raise
  23. def for_update(statement: Select[Any]) -> Select[Any]:
  24. """为后续业务服务提供 ``SELECT ... FOR UPDATE`` 入口。"""
  25. return statement.with_for_update()
  26. def execute_with_deadlock_retry(
  27. operation: Callable[[], T],
  28. *,
  29. max_attempts: int = 3,
  30. base_delay_seconds: float = 0.05,
  31. ) -> T:
  32. """仅重试MySQL锁等待超时和死锁,供后续业务事务显式调用。"""
  33. if max_attempts < 1:
  34. raise ValueError("max_attempts必须大于等于1")
  35. for attempt in range(1, max_attempts + 1):
  36. try:
  37. return operation()
  38. except OperationalError as exc:
  39. db.session.rollback()
  40. error_code = exc.orig.args[0] if getattr(exc.orig, "args", ()) else None
  41. if error_code not in MYSQL_RETRYABLE_ERROR_CODES or attempt == max_attempts:
  42. raise
  43. sleep(base_delay_seconds * attempt)
  44. raise RuntimeError("不可达的事务重试状态")