Tortoise ORM 事务与并发

适合需要更精细控制的场景: 自动保存点(推荐):@atomic() 和 in_transaction() 天然支持嵌套,内层块会自动创建数据库保存点(SAVEPOINT)。内层块异常只回滚到进入该块前的状态,不影响外层事务。 手动保存点(底层 API): 防止并发读写同一行时产生竞态条件: select_for_update() 参数(部分数据库支持): 不使用行锁时,用 F 表达式进行原子更新: 用版本号或时间戳实现,不使用数据库锁: 1. 事务尽量短:持有锁的时间越长,并发阻塞越严重。不要在事务内进行网络请求、大量计算。 2. @atomic() 不

分享

官方文档:https://tortoise.github.io/transactions.html
适用版本:tortoise-orm >= 0.20(2026-05-07 核实)


一、事务基础

@atomic() 装饰器

from tortoise.transactions import atomic

@atomic()
async def transfer(from_id: int, to_id: int, amount: float):
    sender = await Account.select_for_update().get(id=from_id)
    receiver = await Account.select_for_update().get(id=to_id)

    if sender.balance < amount:
        raise ValueError('余额不足')

    await Account.filter(id=from_id).update(balance=F('balance') - amount)
    await Account.filter(id=to_id).update(balance=F('balance') + amount)
    # 函数正常返回 → 自动 COMMIT
    # 抛出任何异常 → 自动 ROLLBACK

in_transaction() 上下文管理器

适合需要更精细控制的场景:

from tortoise.transactions import in_transaction

async def create_order_with_items(order_data, items_data):
    async with in_transaction() as conn:
        order = await Order.create(using_db=conn, **order_data)
        for item in items_data:
            await OrderItem.create(using_db=conn, order=order, **item)
        # with 块正常结束 → COMMIT
        # 异常 → ROLLBACK

in_transaction(connection_name) — 指定连接

async with in_transaction('secondary') as conn:
    await SomeModel.create(using_db=conn, ...)

二、保存点(Savepoint)与嵌套事务

自动保存点(推荐)@atomic()in_transaction() 天然支持嵌套,内层块会自动创建数据库保存点(SAVEPOINT)。内层块异常只回滚到进入该块前的状态,不影响外层事务。

from tortoise.transactions import in_transaction, atomic

async with in_transaction() as conn:
    await User.create(using_db=conn, name='Alice')

    # 内层块自动创建 SAVEPOINT
    try:
        async with in_transaction() as conn2:
            await User.create(using_db=conn2, name='Bob', email='invalid')
            # 内层异常 → 回滚到 SAVEPOINT,Alice 不受影响
    except Exception:
        pass

    # Alice 的记录仍然会提交

手动保存点(底层 API)

async with in_transaction() as conn:
    await User.create(using_db=conn, name='Alice')

    try:
        await conn.execute_script('SAVEPOINT sp1')
        await User.create(using_db=conn, name='Bob', email='invalid')
    except Exception:
        await conn.execute_script('ROLLBACK TO SAVEPOINT sp1')

优先使用嵌套 async with in_transaction() 自动保存点,更简洁且不易出错。


三、SELECT FOR UPDATE(行锁)

防止并发读写同一行时产生竞态条件:

from tortoise.expressions import F

async with in_transaction() as conn:
    # 锁定该行,其他事务需要等待锁释放
    product = await Product.select_for_update().get(id=product_id)

    if product.stock < quantity:
        raise ValueError('库存不足')

    await Product.filter(id=product_id).update(
        stock=F('stock') - quantity,
        using_db=conn,
    )

select_for_update() 必须在事务内使用,否则锁无意义。

select_for_update() 参数(部分数据库支持):

参数 类型 默认 说明
nowait bool False 无法立即获取锁时抛出异常而非等待
skip_locked bool False 跳过已锁定的行(用于任务队列场景)
of tuple () 指定锁哪张表(JOIN 场景),如 of=('user',)
no_key bool False 使用较弱的行锁(FOR NO KEY UPDATE,PostgreSQL),允许插入引用该行的子记录
# 任务队列场景:跳过已被其他 worker 锁定的任务
tasks = await Task.filter(status='pending').select_for_update(skip_locked=True).limit(10)

四、原子操作(避免竞态)

不使用行锁时,用 F 表达式进行原子更新:

from tortoise.expressions import F

# 原子自增(安全,不需要先 SELECT)
await Product.filter(id=1).update(view_count=F('view_count') + 1)

# 原子库存扣减(加 filter 保证不会扣为负数)
updated = await Product.filter(
    id=product_id,
    stock__gte=quantity   # 乐观锁:只有库存足够时才更新
).update(stock=F('stock') - quantity)

if updated == 0:
    raise ValueError('库存不足或商品不存在')

五、乐观锁

用版本号或时间戳实现,不使用数据库锁:

class Product(Model):
    stock = fields.IntField()
    version = fields.IntField(default=0)   # 版本号

async def deduct_stock(product_id: int, quantity: int, max_retry: int = 3):
    for attempt in range(max_retry):
        product = await Product.get(id=product_id)

        if product.stock < quantity:
            raise ValueError('库存不足')

        # 只有版本号匹配时才更新(CAS 操作)
        updated = await Product.filter(
            id=product_id,
            version=product.version   # 乐观锁条件
        ).update(
            stock=F('stock') - quantity,
            version=F('version') + 1
        )

        if updated == 1:
            return True   # 成功

        # 版本号不匹配说明被其他请求修改,重试
        await asyncio.sleep(0.01 * (attempt + 1))

    raise RuntimeError('更新失败,请重试')

六、多数据库

配置多个连接

await Tortoise.init(
    config={
        'connections': {
            'default': 'postgres://user:pass@localhost/main_db',
            'replica': 'postgres://user:pass@replica/main_db',
        },
        'apps': {
            'models': {
                'models': ['myapp.models'],
                'default_connection': 'default',
            }
        }
    }
)

路由到不同连接

# 写操作用主库
await User.create(using_db='default', username='alice')

# 读操作用从库
users = await User.all().using_db('replica')

# 事务必须在同一连接内
async with in_transaction('default') as conn:
    user = await User.create(using_db=conn, ...)

七、连接池配置

await Tortoise.init(
    db_url='postgres://user:pass@localhost/db',
    modules={'models': ['myapp.models']},
    # 连接池配置通过 db_url 参数传入(不同驱动不同)
)

# asyncpg(PostgreSQL)通过 URL 参数
db_url = 'postgres://user:pass@localhost/db?minsize=5&maxsize=20'

# aiosqlite(SQLite)无连接池
db_url = 'sqlite://./db.sqlite3'

八、FastAPI 集成最佳实践

from contextlib import asynccontextmanager
from fastapi import FastAPI
from tortoise import Tortoise

TORTOISE_CONFIG = {
    'connections': {'default': 'postgres://user:pass@localhost/db'},
    'apps': {
        'models': {
            'models': ['myapp.models', 'aerich.models'],
            'default_connection': 'default',
        }
    },
    'timezone': 'Asia/Shanghai',
}

@asynccontextmanager
async def lifespan(app: FastAPI):
    await Tortoise.init(config=TORTOISE_CONFIG)
    await Tortoise.generate_schemas()   # 开发环境自动建表
    yield
    await Tortoise.close_connections()

app = FastAPI(lifespan=lifespan)

九、常见并发问题和解法

问题 场景 解法
读-改-写竞态 扣库存、余额转账 select_for_update() + 事务
重复插入 重复注册、重复下单 unique 约束 + 捕获 IntegrityError
统计不准 并发 count() 后插入 原子 F() 表达式
幻读 事务内统计结果变化 可重复读隔离级别(PG 默认)
死锁 多事务交叉锁定 固定加锁顺序、缩短事务时长

十、事务注意事项

  1. 事务尽量短:持有锁的时间越长,并发阻塞越严重。不要在事务内进行网络请求、大量计算。
  2. @atomic() 不可跨协程共享:事务连接绑定到当前执行上下文,不要把 conn 传给其他 task。
  3. 异常后自动回滚@atomic()in_transaction() 中任何未捕获的异常都会触发 ROLLBACK。
  4. auto_now 字段与事务:在事务中 update() 时,auto_now 不会触发;如需记录时间需显式传入。
  5. SQLite 并发限制:SQLite 写操作会锁全表,不适合高并发写入场景,生产环境用 PostgreSQL/MySQL。
  6. 连接泄漏:始终用上下文管理器(async with in_transaction())而非手动 begin/commit,确保连接正确释放。

最佳实践

事务内只做数据库操作,不做网络请求:持有事务锁期间执行 HTTP 请求、消息队列推送等慢操作会阻塞其他并发,显著降低吞吐量。将非数据库副作用移到事务提交后执行。

# 错误:事务内发 HTTP 请求
@atomic()
async def create_order(data):
    order = await Order.create(**data)
    await send_email(order.user.email)  # 慢操作持有锁

# 正确:事务后执行副作用
@atomic()
async def create_order(data):
    order = await Order.create(**data)
    return order

order = await create_order(data)
await send_email(order.user.email)  # 事务已提交

高并发库存/余额场景用 select_for_update() + 事务:使用数据库行锁避免超卖,比乐观锁重试更适合争用激烈的场景。

async with in_transaction() as conn:
    product = await Product.select_for_update().get(id=pid)
    if product.stock < qty:
        raise ValueError('库存不足')
    await Product.filter(id=pid).update(stock=F('stock') - qty, using_db=conn)

无锁原子更新用 F() 表达式:计数器、访问量等只做增减操作的字段,不需要先 SELECT,直接用 F() 在数据库层原子计算。

await Article.filter(id=aid).update(view_count=F('view_count') + 1)

嵌套事务用嵌套 async with in_transaction(),不要手动 SAVEPOINT:内层块自动映射为数据库 SAVEPOINT,内层异常只回滚内层操作,比手动管理更安全。

多连接操作时明确指定 using_db:在事务上下文中创建/查询模型时,始终传入 using_db=conn,否则可能使用默认连接而脱离事务。

async with in_transaction() as conn:
    user = await User.create(using_db=conn, name='Alice')
    await Profile.create(using_db=conn, user=user)

常见陷阱

陷阱:@atomic() 跨协程共享事务连接

现象:@atomic() 函数内用 asyncio.create_task()await asyncio.gather() 启动子任务,子任务中的数据库操作不在同一事务中,或出现 "connection already closed" 错误。

原因: 事务连接绑定到当前执行上下文(通过上下文变量传递),create_task 创建的新 Task 继承的是上下文副本,对连接的修改不会回传,行为不确定。

解决: 不要在事务内并行调度子 Task。需要并行时,在事务外 gather,然后在事务内做最终写入。

陷阱:批量操作不触发信号和 auto_now

现象: 使用 Model.filter(...).update(...) 批量更新后,updated_atauto_now=True)没有变化,注册的信号 handler 也没有执行。

原因: 批量 update()delete() 直接执行 SQL,不经过 Python 模型层,auto_now 赋值和信号分发都在 Python 层,因此均不触发。

解决: 需要触发信号或 auto_now 时,必须逐条 instance.save()instance.delete();或在批量 update 时显式传入时间字段。

# 显式传入时间
from datetime import datetime, timezone
await Post.filter(status='draft').update(
    status='published',
    updated_at=datetime.now(timezone.utc),
)

陷阱:select_for_update() 在事务外使用无效

现象: 调用 select_for_update() 后,并发请求仍然读到相同值,锁没有生效。

原因: SELECT FOR UPDATE 必须在事务中才有意义,事务提交后锁自动释放;在事务外调用,PostgreSQL/MySQL 会忽略锁或立即释放。

解决: 始终将 select_for_update() 放在 async with in_transaction()@atomic() 内。

# 错误:无事务,锁立即释放
product = await Product.select_for_update().get(id=pid)

# 正确
async with in_transaction() as conn:
    product = await Product.select_for_update().get(id=pid)

参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog