flask-sqlalchemy一看就会

flush和commit区别

  1. flush: 写数据库,但不提交,也就是事务未结束
  2. commit: 是先调用flush写数据库,然后提交,结束事务,并开始新的事务

scoped_session

通常我们用 SQLAlchemy 写数据的时候要创建 Session 对象来维护数据库会话,用完了再关掉。但是这种在web大量的请求的情况下,会出现线程不安全的情况
设置session时,需要指定为scoped_session,目的是session可以共享(ThreadLocal)

  1. # 对于alchemy
  2. from sqlalchemy.orm import sessionmaker, scoped_session
  3. scoped_session(sessionmaker(bind=engine))

开启alchemy子事务

session.begin(subtransactions=True)

数据库acid特性总结

什么是事务

事务是应用程序中一系列逻辑相关的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性:一个事务中的一系列的操作要么全部成功,要么一个都不做。
事务的结束有两种,当事务中的所以步骤全部成功执行时,事务提交。如果其中一个步骤失败,将发生回滚操作,撤消之前的所有操作。
在mysql的执行引擎当中只有innoDB支持事务,可以通过 “show engines;”查看。

事务的 ACID

事务具有四个特征:原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持久性( Durability )。这四个特性简称为 ACID 特性。
1 、原子性
事务是数据库的逻辑工作单位,不可分割,事务中包含的各操作要么都做,要么都不做
2 、一致性
事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。
3 、隔离性
一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。 事务的隔离级别有4级
4 、持续性
也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的,不能回滚。接下来的其它操作或故障不应该对其执行结果有任何影响。

MYSQL的事务处理主要有两种方法。

用BEGIN,ROLLBACK,COMMIT来实现

  1. 开始:START TRANSACTIONBEGIN语句可以开始一项新的事务
  2. 提交:COMMIT可以提交当前事务,是变更成为永久变更
  3. 回滚:ROLLBACK可以回滚当前事务,取消其变更

直接用set来改变mysql的自动提交模式

MYSQL默认是自动提交的,也就是你提交一个QUERY,它就直接执行!我们可以通过set autocommit=0 禁止自动提交set autocommit=1 开启自动提交来实现事务的处理。但注意当你用 set autocommit=0 的时候,你以后所有的SQL都将做为事务处理,直到你用commit确认rollback结束,并且只用于当前连接。

事务隔离级别

※脏读:一个事务读取了另一个未提交的并行事务写的数据。
exp:
小明的分数为89,事务A中把他的分数改为98,但事务A尚未提交。
与此同时,
事务B正在读取小明的分数,读取到小明的分数为98。
随后,
事务A发生异常,而回滚了事务。小明的分数又回滚为89。
最后,
事务B读取到的小明的分数为98的数据即为脏数据,事务B做了一次脏读。
(大部分数据库缺省的事物隔离级别都不会出现这种状况)
※不可重复读:一个事务重新读取前面读取过的数据,发现该数据已经被另一个已提交的事务修改过。
在事务A中,读取到小明的分数为89,操作没有完成,事务还没提交。
与此同时,
事务B把小明的分数改为98,并提交了事务。
随后,
在事务A中,再次读取小明的分数,此时工资变为98。在一个事务中前后两次读取的结果并不致,导致了不可重复读。
※幻读:一个事务重新执行一个查询,返回一套符合查询条件的行,发现这些行因为其他最近提交的事务而发生了改变。
exp:
目前分数为90分以上的的学生有15人,事务A读取所有分数为90分以上的的学生人数有15人。
此时,事务B插入一条分数为99的学生记录。
这是,事务A再次读取90分以上的的学生,记录为16人。此时产生了幻读。
(大部分数据库缺省的事物隔离级别都会出现这种状况,此种事物隔离级别将带来表级锁)
READ UNCOMMITTED:幻读,不可重复读和脏读均允许;
READ COMMITTED:允许幻读和不可重复读,但不允许脏读;
REPEATABLE READ:允许幻读,但不允许不可重复读和脏读;
SERIALIZABLE:幻读,不可重复读和脏读都不允许;
ORACLE默认的是 READ COMMITTED。
MYSQL默认的是 REPEATABLE READ。

  1. 如果数据库的隔离级别为REAE_UNCOMMITTED 则其他线程可以看到未提交的数据, 因此就出现脏读;
  2. 如果数据库隔离级别设为READ_COMMITTED,即没提交的数据别人是看不见的,就避免了脏读;但是,正在读取的数据只获得了读取锁,读完之后就解锁,不管当前事务有没有结束,这样就容许其他事务修改本事务正在读取的数据。导致不可重复读。
  3. REPEATABLE READ因为对正在操作的数据加锁,并且只有等到事务结束才放开锁, 则可以避免不可重复读;
  4. REPEATABLE READ只能保证正在被本事务操作的数据不被其他事务修改,却无法保证有其他事务提交新的数据。 则有可能线程1在操作表T1的时候(特别是统计性的事务),其他线程仍然可以提交新数据到表T1,这样会导致线程1两次统计的结果不一致,就像发生幻觉一样。
  5. SERIALIZABLE因为获得范围锁,且事务是一个接着一个串行执行,则保证了不会发生幻读。
  6. 由此可见,隔离级别越高,受其他事物干扰越少,并发性能越差。

修改事务的隔离级别:

  1. MySQL中默认事务隔离级别是可重复读(Repeatable read).可通过SQL语句查询:
  2. 查看InnoDB系统级别的事务隔离级别:
  3. mysql> SELECT @@global.tx_isolation;
  4. 结果:
  5. +-----------------------+
  6. | @@global.tx_isolation |
  7. +-----------------------+
  8. | REPEATABLE-READ |
  9. +-----------------------+
  10. 查看InnoDB会话级别的事务隔离级别:
  11. mysql> SELECT @@tx_isolation;
  12. 结果:
  13. +-----------------+
  14. | @@tx_isolation |
  15. +-----------------+
  16. | REPEATABLE-READ |
  17. +-----------------+
  18. 修改事务隔离级别:
  19. mysql> set global transaction isolation level read committed;
  20. mysql> set session transaction isolation level read committed;

SQLAlchemy 的 scoped_session 是啥玩意

通常我们用 SQLAlchemy 写数据的时候要创建 Session 对象来维护数据库会话,用完了再关掉。但是听说还有个叫scoped_session的玩意,这是做啥用的?
这东西其实与 web 应用有一些关系。我们在使用 Django 的 ORM 的时候怎么没见到需要创建个 session 呢?因为它已经悄悄帮你实现好了维护 session 的逻辑,自动进行创建和销毁(多么伟大,多么 friendly 啊),而当我们用 Flask 之类裸奔极客模式的 web 框架时就没有这样的好事了,只能自己搞定。

Session 的生命周期

首先我们需要知道一个 sqlalchemy session 的生命周期是怎样的。我们的 web 应用会同时服务多个用户,因此不同的请求要有不同的 session,不然就会翻车。
session 会在一次请求进来的时候创建出来

  1. session = Session()

在整个请求处理过程中被使用

  1. session.add(some_obj)
  2. session.commit()

在请求处理完毕后被关闭销毁。

  1. session.close()

当然上面的代码只是简略的情形,通常还需要包括 try-except 来处理 session 需要 rollback 之类的情况。

scoped_session 与 registry 模式

scoped_session就是用在 web 应用这种处理请求的场景下,协助进行 session 维护的。

  1. from sqlalchemy.orm import scoped_session
  2. from sqlalchemy.orm import sessionmaker
  3. session_factory = sessionmaker(bind=some_engine)
  4. ScopedSession = scoped_session(session_factory)

通常我们在初始化 web 应用时通过scoped_session函数对原始的Session工厂进行处理,返回出一个ScopedSession工厂,在每个请求来的时候就可以通过这个工厂获得一个 scoped_session 对象。
这实际上实现了一种 registry 模式,它有个中心化的 registry 来保存已经创建的 session,并在你调用ScopedSession工厂的时候,在 registry 里面找找是不是之前已经为你创建过 session 了,如果有,就直接把这个 session 返回给你,如果没有,就创建一个新的 session,并注册到 registry 中以便你下次来要的时候给你。

  1. some_session = ScopedSession()
  2. some_other_session = ScopedSession()
  1. >>> some_session is some_other_session
  2. True
  3. >>> some_other_session.remove()
  4. >>> new_session = ScopedSession()
  5. >>> new_session is some_session
  6. False

调用scoped_sessionremove()方法会调用ScopedSession.close()关闭 session,释放连接资源、把数据库 transaction 状态恢复到初始状态等,最后销毁 session 本身,下回再调用ScopedSession的时候,又会重新创建一个 session 出来。
这样,我们在整个请求的任意环节,都可以开开心心地随时通过工厂来获取一个 “新的”session 对象,而只要在请求处理完的时候移除这个 session 就可以了。如果不用它,那么你在每个需要读写数据库的地方,都要小心翼翼地创建个 session 出来,并记得把它们关掉,不然就造成了资源泄漏。

Thread-Local Storage

上面说到 scoped_session 类似单例模式,我们看似创建了新的 session,实际上拿到的可能是之前创建出来的 session。但我们 web 应用通常要同时处理多个请求,我的请求有没有可能不小心拿到别人创建的 session 对象呢?
这是一个好问题,然而正直的 SQLAlchemy 不会让不法分子得逞的
尽管 Python 有着神奇的 GIL,没法真正的并行地跑线程,但至少还是有线程的概念的,对于不同的请求进来的时候我们通常会在不同的线程中进行处理。Python 里有个概念叫 thread local storage(TLS),即线程本地存储,它可以作为全局变量一样使用,但这个数据只在这个线程中有效,与其他的线程是隔离的。

  1. import threading
  2. mydata = threading.local()
  3. mydata.x = 1

等等,全局变量?这不正好和刚才 registry 模式的思想差不多嘛。和我们所希望的一样,scoped_session 也确实使用了 tls 作为 session 的存储方式,一个线程只能拿到自己创建出来的 session,保证了不同线程不会乱入别人的 session。
使用 tls 还有另外一个好处,由于 session 是跟着线程走的,就算你没有调用remove()亲手干掉 session,也会由于线程结束,session 也跟着被一起回收掉,不至于泄漏。(但仍建议在必要的时候对资源进行显式的回收)
还有一个隐蔽的问题,如果我们用了 gevent 来处理并发而不是用多线程,会翻车吗?答案是不会。gevent 在monkey.patch_all()的时候,已经悄悄把这个 threading 相关的东西悄悄替换成自己的一套了,thread-local 的东西已经变成了 greenlet-local,不同协程间仍是隔离的,一般不会有问题。

要不要用 scoped_session

对于 Flask 框架,强烈不建议自己维护 session,就算我们已经有了 scoped_session,但这玩意仍旧不是那么好用,有很多细节需要处理。Flask 有个名为 Flask-SQLAlchemy 的扩展,它已经把 scoped_session 的这一套在内部帮你配置好了,你只需要正常使用它的db.session,无需关心 session 是怎么来的,又是怎么没的。
但如果用了个别的框架,而它又没有好用的自带 ORM(即除了 Django 之外其他框架),或者是在非 web 应用里使用,这时就应该使用 SQLALchemy 的 scoped_session,来减少一些 bug 或者内存泄漏的可能。

参考

参考:https://usyiyi.github.io/sqlalchemy-docs-zh/orm/session_transaction.html