从SQLAlchemy的“缓存”问题说起
https://www.jianshu.com/p/c0a8275cce99
问题描述
最近在排查一个问题,为了方便说明,我们假设现在有如下一个API:
@app.route("/sqlalchemy/test", methods=['GET'])
def sqlalchemy_test_api():
data = {}
# 获取商品价格
product = Product.query.get(1)
data['old_price'] = product.present_price
# 休眠10秒,等待外部修改价格
time.sleep(10)
product = Product.query.get(1)
data['new_price'] = product.present_price
return jsonify(status='ok', data=data)
这里我们的后台使用了Flask作为服务端框架,SQLAlchemy作为数据库ORM框架。Product是一张商品表的ORM模型,假设原来id=1的商品价格为10,在程序休眠的10秒内价格被修改为20,那么你觉得返回的结果是多少?
old_price显然是10,那么new_price呢?讲道理的话由于外部修改价格为20了,同时程序在sleep后立刻又query了一次,你可能觉得new_price应该是20。但结果并不是,真实测试的结果是10,给人感觉就像是SQLAlchemy“缓存”了上一次的结果。
另外在测试的过程还发现一个现象,虽然在第一次API调用时两个price都是10,但是在第二次调用API时,读到的price是20。也就是说,在一个新的API开始时,之前“缓存”的结果被清除了。
SQLAlchemy的session状态管理
之前我们提出了一个猜测:第二次查询是否“缓存”了第一次查询。为了验证这个猜想,我们可以把SQLALCHEMY_ECHO这个配置项打开,这是个全局配置项,官方文档定义如下:
| 配置项 | 说明 |
|---|---|
SQLALCHEMY_ECHO |
If set to True SQLAlchemy will log all the statements issued to stderr which can be useful for debugging. |
在这个配置项打开的情况下,我们可以看到查询语句输出到终端下。我们再次调用API,可以发现第一次查询会输出类似SELECT * FROM product WHERE id = 1的语句,而第二次查询则没有这样的输出。如此看来,SQLAlchemy确实缓存了上次的结果,在第二次查询的时候直接使用了上次的结果。
实际上,当执行第一句product = Product.query.get(1)时,product这个对象处于持久状态(persistent)了,我们可以通过一些工具看到ORM对象目前处于的状态。详细的状态列表可在官方文档中找到。
>>> from sqlalchemy import inspect
>>> insp = inspect(product)
>>> insp.persistent
True
>>> product.__dict__
{
'id': 1, 'present_price': 10,
'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x1106a3350>,
}
为了清除该对象的缓存,程度从低到高有下面几种做法。expire会清除对象里缓存的数据,这样下次查询时会直接从数据库进行查询。refresh不仅清除对象里缓存的数据,还会立刻触发一次数据库查询更新数据。expire_all的效果和expire一样,只不过会清除session里所有对象的缓存。flush会把所有本地修改写入到数据库,但没有提交。commit不仅把所有本地修改写入到数据库,同时也提交了该事务。
db.session.expire(product)
db.session.refresh(product)
db.session.expire_all()
db.session.flush()
db.session.commit()
我们对这几种方法依次做实验,结果发现这5个操作都会让下次查询直接从数据库进行查询,但只有commit会读到最新的price。那这个又是什么原因呢,我们已经强制每次查询走数据库,为何还是读到“缓存”的数据。这个就要用数据库的事务隔离机制来解释了。
事务隔离
在数据库系统中,事务隔离级别(isolation level)决定了数据在系统中的可见性。隔离级别从低到高分为四种:未提交读(Read uncommitted),已提交读(Read committed),可重复读(Repeatable read),可串行化(Serializable)。他们的区别如下表所示。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 未提交读(RU) | 可能 | 可能 | 可能 |
| 已提交读(RC) | 不可能 | 可能 | 可能 |
| 可重复读(RR) | 不可能 | 不可能 | 可能 |
| 可串行化 | 不可能 | 不可能 | 不可能 |
脏读(dirty read)是指一个事务可以读到其他事务还未提交的数据。不可重复读(non-repeatable read)是指在一个事务中同一行被读取了多次,可以读到不同的值。幻读(phantom read)是指在一个事务中执行同一个语句多次,读到的数据行发生了改变,即可能行数增加了或减少了。
前面提到的问题其实就涉及到不可重复读这个特性,即在一个事务中我们query了product.id=1的数据多次,但读到了重复的数据。对于MySQL来说,默认的事务隔离级别是RR,通过上表我们可知RR是可重复读的,因此可以解释这个现象。
| 事务A | 事务B |
|---|---|
BEGIN; |
BEGIN; |
SELECT present_price FROM product WHERE id = 1; /* id=1的商品价格为10 */ |
|
UPDATE product SET present_price = 20 WHERE id = 1; /* 修改id=1的商品价格为20 */ |
|
COMMIT; |
|
SELECT present_price FROM product WHERE id = 1; /* 再次查询id=1的商品价格 */ |
|
COMMIT; |
对于前面的问题,我们可以把两个事务的执行时序图画出来如上所示。因此为了使第二次查询得到正确的值,我们可以把隔离级别设为RC,或者在第二次查询前进行COMMIT新起一个事务。
Flask-SQLAlchemy的自动提交
前面还遗留一个问题没有搞清楚:在一个新的API开始时,之前“缓存”的结果似乎被清除了。由于打开了SQLALCHEMY_ECHO配置项,我们可以观察到每次API结束的时候都会自动触发一次COMMIT,而正是这个自动提交清空了所有的“缓存”。通过查找源代码,我们发现是下面这段代码在起作用:
@teardown
def shutdown_session(response_or_exc):
if app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN']:
if response_or_exc is None:
self.session.commit()
self.session.remove()
return response_or_exc
如果配置项SQLALCHEMY_COMMIT_ON_TEARDOWN为True,那么首先触发COMMIT,最后统一执行session.remove()操作,即释放连接并回滚事务操作。
有意思的是,这个配置项在Flask2.0版本的Changelog中被移除了。

关于删除的原因,作者在stackoverflow的一个帖子里进行了说明。这个帖子同时也解释了为什么在我们的生产环境中经常报这个错误:
InvalidRequestError: This session is in 'prepared' state; no further SQL can be emitted within this transaction.,而且只有重启才能解决问题。有兴趣的同学可以深入阅读一下。
总结
在MySQL的同一个事务中,多次查询同一行的数据得到的结果是相同的,这里既有SQLAlchemy本身“缓存”结果的原因,也受到数据库隔离级别的影响。如果要强制读取最新的结果,最简单的办法就是在查询前手动COMMIT一次。根据这个原则,我们可以再仔细阅读下自己项目中的代码,看看会不会有一些隐藏的问题。
从SQLAlchemy的“缓存”问题说起的更多相关文章
- SQLAlchemy 对象缓存和刷新
SQLAlchemy 对象缓存和刷新 SQLAlchemy 带有对象缓存机制,在重复查询相同的对象时,直接先查询本地的缓存,而不需要从数据库加载数据. 在每个 model 对象的内部,SQLAlche ...
- SQLAlchemy的“缓存”问题导致的BUG
问题描述: 最近做项目,遇到一个问题,两个项目操作同一个数据库,其中A项目用的pymysql链接操作数据库,B项目用的sqlalchemy,当我请求B项目中的一个接口,会通知A项目操作数据库,然后返回 ...
- sqlalchemy的缓存和刷新
其实只是第一次查询了数据库,其他的时候都使用的是缓存,所以有时候,因为这个特性会出错,所以需要刷新对象或者使对象过期 参考链接:http://www.cnblogs.com/fengyc/p/5369 ...
- Python 【第六章】:Python操作 RabbitMQ、Redis、Memcache、SQLAlchemy
Memcached Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载.它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高动态.数据库驱动网站的速度 ...
- Python操作Redis、Memcache、RabbitMQ、SQLAlchemy
Python操作 Redis.Memcache.RabbitMQ.SQLAlchemy redis介绍:redis是一个开源的,先进的KEY-VALUE存储,它通常被称为数据结构服务器,因为键可以包含 ...
- Python操作 RabbitMQ、Redis、Memcache、SQLAlchemy
Memcached Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载.它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高动态.数据库驱动网站的速度 ...
- Python之路【第九篇】:Python操作 RabbitMQ、Redis、Memcache、SQLAlchemy
Python之路[第九篇]:Python操作 RabbitMQ.Redis.Memcache.SQLAlchemy Memcached Memcached 是一个高性能的分布式内存对象缓存系统,用 ...
- python 自动化之路 day 10 协程、异步IO、队列、缓存
本节内容 Gevent协程 Select\Poll\Epoll异步IO与事件驱动 RabbitMQ队列 Redis\Memcached缓存 Paramiko SSH Twsited网络框架 引子 到目 ...
- Python开发【第二十篇】:缓存
Python开发[第二十篇]:缓存redis&Memcache 点击这里 Python之路[第九篇]:Python操作 RabbitMQ.Redis.Memcache.SQLAlchemy ...
随机推荐
- Leetcode 5274. 停在原地的方案数
纪念第一次正式参加,听说这次题目很水,感觉确实不是很难,一般前两题都很简单,这次,到第三题的时候,都还可以做,emm...... 实际代码记录: #include <iostream> # ...
- 上传文件到新浪云Storage的方法
上传文件到新浪云Storage的方法,兼容本地服务器 if (!empty($_FILES['sharepic']['name'])){ $tmp_file = $_FILES['sharepic'] ...
- 【VS开发】vector features(num_images)析构报错
代码一气呵成,但运行的时候会出现_CrtIsValidHeapPointer的异常,跟进去调了一上午的Bug,终于搞定 跟踪定位到 _CrtIsValidHeapPointer ,注意到 g 8h&q ...
- IO流——字节流
文件输出流 FileOutputStream:文件输出流是用于将数据写入 File,每次运行,都会覆盖之前文件中的数据 FileOutputStream(File file):创建一个向指定 File ...
- 使用 Jest 和 Supertest 进行接口端点测试
如何创建测试是一件困难的事.网络上有许多关于测试的文章,却从来不告诉你他们是如何开始创建测试的. 所以,今天我将分享我在实际工作中是如何从头开始创建测试的.希望能够对你提供一些灵感. 目录: 使用 E ...
- 概率dp - Uva 10900 So you want to be a 2n-aire?
So you want to be a 2n-aire? Problem's Link Mean: 玩一个答题赢奖金的游戏,一开始有1块钱,玩n次,每次赢的概率为t~1之间的某个实数. 给定n和t,求 ...
- Scala Collection Method
接收一元函数 map 转换元素,主要应用于不可变集合 (1 to 10).map(i => i * i) (1 to 10).flatMap(i => (1 to i).map(j =&g ...
- 关于FMX 单击出来右键菜单,并显示在当前下面
关于FMX 单击出来右键菜单,并显示在当前下面 procedure TForm1.btn6MouseDown(Sender: TObject; Button: TMouseButton; Shift ...
- 【搬运工】RHEL6.5 移植使用CentOS 的YUM 步骤
转载地址:http://www.cnblogs.com/rchen98/p/6056469.html 问题:使用 Red Hat Enterprise Linux Server(RHEL) yum安装 ...
- Python 中拼音库 PyPinyin 的用法【华为云技术分享】
[摘要] 最近碰到了一个问题,项目中很多文件都是接手过来的中文命名的一些素材,结果在部署的时候文件名全都乱码了,导致项目无法正常运行. 后来请教了一位大佬怎么解决文件名乱码的问题,他说这个需要正面解决 ...