You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多实例并发访问SQL Server/Azure SQL数据库表时的数据重复问题及事务锁相关疑问

多实例并发访问SQL Server/Azure SQL数据库表时的数据重复问题及事务锁相关疑问

兄弟,你遇到的是典型的并发丢失更新问题——多个实例同时读取同一个计数器,各自加1后写回,结果就撞出重复值了。先给你把核心问题拆明白,再给你靠谱的解决方案:

首先,你原来写的with conn.begin() as transaction:只是开启了一个事务,但默认情况下(SQL Server的READ COMMITTED隔离级别),事务里的读操作不会加排他锁,两个实例还是能同时读到同一个旧值,所以光开事务根本拦不住并发问题。

那该怎么解决?按优先级给你说几个方案:

1. 最优解:用数据库原子操作代替先读后写

别在应用层读计数器再加1,直接把增量逻辑丢给数据库执行——SQL Server的UPDATE语句是原子性的,数据库会自动帮你处理并发,不会出现重复。比如你要给某个事件类型的计数器加1,直接写:

from sqlalchemy import text

with engine.connect() as conn:
    with conn.begin():
        # 先尝试更新现有记录
        update_result = conn.execute(
            text("UPDATE Log SET Counter = Counter + 1 WHERE EventType = :event_type; SELECT @@ROWCOUNT;"),
            {"event_type": "你的事件类型"}
        )
        updated_rows = update_result.scalar()
        # 如果没有找到对应记录,就插入新的初始化计数器
        if updated_rows == 0:
            conn.execute(
                text("INSERT INTO Log (EventType, Counter) VALUES (:event_type, 1);"),
                {"event_type": "你的事件类型"}
            )

这种方式完全不用管锁和重试,数据库自己会保证同一时间只有一个实例能成功更新,其他的要么排队要么触发插入,绝对不会有重复。

2. 必须先读后写?那就加显式锁+重试

如果业务逻辑必须先读取计数器做一些处理再写回去,那你得在读取的时候就给记录加锁,不让其他实例读。SQL Server里用WITH (UPDLOCK, HOLDLOCK)来实现——UPDLOCK加更新锁,HOLDLOCK把锁持有到事务结束。

同时,被锁阻塞的实例可能会抛出锁超时或者死锁错误,这时候你得在应用层加重试逻辑,比如最多重试3次:

from sqlalchemy import text
import sqlalchemy.exc

def safe_increment_counter(engine, event_type, max_retries=3):
    for attempt in range(max_retries):
        try:
            with engine.connect() as conn:
                with conn.begin():
                    # 读取时加锁,其他实例必须等当前事务提交才能读
                    counter_result = conn.execute(
                        text("SELECT Counter FROM Log WITH (UPDLOCK, HOLDLOCK) WHERE EventType = :event_type;"),
                        {"event_type": event_type}
                    )
                    counter_row = counter_result.fetchone()
                    
                    if counter_row:
                        new_counter = counter_row[0] + 1
                        # 这里可以加你需要的其他业务逻辑
                        conn.execute(
                            text("UPDATE Log SET Counter = :new_counter, OtherData = :other_data WHERE EventType = :event_type;"),
                            {"new_counter": new_counter, "other_data": "你的其他数据", "event_type": event_type}
                        )
                    else:
                        # 初始化新记录
                        conn.execute(
                            text("INSERT INTO Log (EventType, Counter, OtherData) VALUES (:event_type, 1, :other_data);"),
                            {"event_type": event_type, "other_data": "你的其他数据"}
                        )
                # 事务提交成功,直接返回
                return
        except sqlalchemy.exc.OperationalError as e:
            # 捕获SQL Server的死锁(错误码1205)或锁超时错误
            error_msg = str(e)
            if "1205" in error_msg or "lock request time out period exceeded" in error_msg:
                if attempt == max_retries - 1:
                    # 最后一次重试失败,抛异常让上层处理
                    raise
                # 否则继续重试
                continue
        except Exception as e:
            # 其他异常直接抛出
            raise

3. 关于事务隔离级别的补充

如果你不想用显式锁,也可以把事务隔离级别改成REPEATABLE READ或者SERIALIZABLE,但SERIALIZABLE隔离级别太严格,容易导致大量锁等待和死锁,除非业务场景特殊,否则不推荐。用SQLAlchemy设置隔离级别可以这么写:

with engine.connect().execution_options(isolation_level="REPEATABLE READ") as conn:
    with conn.begin():
        # 你的业务逻辑
        pass

但还是那句话,优先用原子更新,这是最省心最可靠的方案。

最后提醒下:重试逻辑一定要加,不管用哪种锁机制,并发下都可能出现锁冲突,重试能让失败的请求自动重试,避免业务报错。

备注:内容来源于stack exchange,提问作者BlakeB9

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.21 14:19:35