SQLAlchemy读写分离下commit后访问实例报已删除错误
问题根源
你的问题是两个机制共同作用导致的:
- SQLAlchemy默认在
commit()后会过期所有实例(expire_on_commit=True),当你访问实例属性(比如User.userId)时,会自动触发SELECT查询重新从数据库加载数据。 - 你的
RoutingSession把所有SELECT都路由到读实例,但读写分离架构下主从同步存在延迟,刚提交到写实例的数据还没同步到读实例,导致查询不到数据,抛出实例不存在的错误。
方案1:关闭commit后的实例过期(简单直接)
创建Session时添加expire_on_commit=False配置,让commit后实例的属性保留在内存中,访问时不再触发数据库查询:
Session = sessionmaker( class_=RoutingSession, autocommit=False, autoflush=True, expire_on_commit=False # 新增该配置 ) db_session = scoped_session(Session)
- 优点:代码改动极小,适合提交后短时间内需要访问实例属性的场景。
- 缺点:实例不会自动感知数据库中后续的变更,如果其他操作修改了这条记录,内存中的实例数据会和数据库不一致。
方案2:修改路由逻辑,让实例刷新查询走写实例(更严谨)
如果需要保留expire_on_commit=True的默认行为(保证数据一致性),可以修改RoutingSession的get_bind方法,识别SQLAlchemy内部用于刷新实例的主键查询,强制这类查询路由到写实例:
from sqlalchemy.sql import Select class RoutingSession(Session): def get_bind(self, mapper=None, clause=None): # 优先处理实例刷新的主键查询:这类查询是commit后加载实例用的,必须走写库 if clause is not None and isinstance(clause, Select) and mapper is not None: primary_key_cols = set(mapper.primary_key) query_cols = set(clause.columns) # 判断查询是否仅包含主键列,且条件为主键等值匹配(SQLAlchemy刷新实例的特征) if primary_key_cols.issuperset(query_cols): where_clause = clause.whereclause if (hasattr(where_clause, 'left') and where_clause.left in primary_key_cols and hasattr(where_clause, 'right') and not hasattr(where_clause.right, 'name')): return engines["write"] # 原有路由逻辑 if isinstance(clause, (Select)): return engines["read"] else: return engines["write"]
- 原理:SQLAlchemy刷新过期实例时,会执行一条仅查询主键、条件为当前实例主键值的
SELECT语句,我们通过识别这类查询的特征,强制让它走写实例,确保能读到刚提交的数据。
额外建议
如果业务允许,可以调整MySQL主从同步策略(比如半同步复制)来降低延迟,但代码层面的修复是更直接的解决方案。
内容的提问来源于stack exchange,提问作者Rajendra Khabiya
相关产品推荐
相关产品推荐

