transaction.py 2.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
  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. # 达梦等非MySQL驱动错误码不统一,死锁/锁超时以异常消息文本兜底识别。
  14. DEADLOCK_MESSAGE_MARKERS = ("死锁", "deadlock", "DeadLock", "lock wait timeout")
  15. @contextmanager
  16. def transaction() -> Generator[Session, None, None]:
  17. """提供统一提交和异常回滚边界。"""
  18. session = db.session
  19. try:
  20. yield session
  21. session.commit()
  22. except Exception:
  23. session.rollback()
  24. raise
  25. def for_update(statement: Select[Any]) -> Select[Any]:
  26. """为后续业务服务提供 ``SELECT ... FOR UPDATE`` 入口。"""
  27. return statement.with_for_update()
  28. def execute_with_deadlock_retry(
  29. operation: Callable[[], T],
  30. *,
  31. max_attempts: int = 3,
  32. base_delay_seconds: float = 0.05,
  33. ) -> T:
  34. """重试数据库死锁与锁等待超时(MySQL错误码或消息特征),供业务事务显式调用。"""
  35. if max_attempts < 1:
  36. raise ValueError("max_attempts必须大于等于1")
  37. for attempt in range(1, max_attempts + 1):
  38. try:
  39. return operation()
  40. except OperationalError as exc:
  41. db.session.rollback()
  42. error_code = exc.orig.args[0] if getattr(exc.orig, "args", ()) else None
  43. retryable = error_code in MYSQL_RETRYABLE_ERROR_CODES or any(
  44. marker in str(exc.orig) for marker in DEADLOCK_MESSAGE_MARKERS
  45. )
  46. if not retryable or attempt == max_attempts:
  47. raise
  48. sleep(base_delay_seconds * attempt)
  49. raise RuntimeError("不可达的事务重试状态")