前言
很早就有想开始写博客的想法,一方面是对自己近期所学知识的一些总结、沉淀,方便以后对过去的知识进行梳理、追溯,一方面也希望能通过博客来认识更多相同技术圈的朋友。所幸近期通过了博客园的申请,那么今天就开始第一篇随笔,来开始记录自己的学习点滴。
问题背景
本人最近在用python的Web开发框架Django开发一个平台,平台的数据库用的是远程的Mysql (RDS),出于对数据库信息的安全考虑,对访问数据库的ip作了限制。因此,我们是通过在服务器上用轻量级的python Web框架Flask搭建了一些数据存取接口,所有外部机子对数据库的访问是通过调用接口来实现的。而我们起这个web接口的方式非常简单,没有通过任何的容器,仅仅是使用 “python qvServer.py”的方式(qvServer.py是我们的python接口脚本)。这种起接口的方式会存在一个严重的问题:
问题1 单线程:当Flask在处理某一个请求时,如果该请求还没有处理完成,而其他的应用程序又发送了新的请求,此时新请求需要等待,直到前一个请求处理完成时服务器才可以开始处理下一个请求。
当然,这样起也有好处:所有请求排队进行等候一一处理,不会有多个接口同时被调用,而同时对数据库表进行并行操作的风险。然后,在实际执行过程中,我们发现有一些接口的执行时间非常长,比如接口A在执行时,需要执行几千条的SQL插入命令,可能需要耗费将近5-10分钟的时间。此时将会导致其他请求严重阻塞!
为了解决这个问题,参考了朋友的意见和书《Python Web测试驱动方法》中的内容,决定采用Python的容器gunicorn(Green Unicorn,俗称“绿色独角兽”)通过多个进程来起Flask接口服务。使用gunicorn起web服务的命令很简单:nohup gunicorn -w 4 -b 0.0.0.0:9001 qvServer:app & 之后我们查看linux进程的状态,如下图1所示:
图1 查看gunicorn进程
可以看到gunicorn是通过5个进程,一个父进程(PID:17135)和四个子进程来起qvServer,这样处理后,通过实际的执行验证,确实解决了请求不能并发处理的问题,服务器可以同时支持多个请求并行处理,处理效率大大提升,但是却出现了另一个新的问题:
问题2 并发处理导致数据存储失败:还是那个接口A,几千条SQL插入命令在处理时,总是执行失败,且最终数据库总是会残留一部分的脏数据(部分插入成功,发生失败后后续插入操作被中断)
为了解决脏数据的问题,我们自然而然(其实是在老大的提醒下)地会想到引入数据库事务来管理请求,当发生异常时,将这一次的所有数据进行回滚,杜绝脏数据残留在数据库中。由于本人之前对这一块毫无所知,所以趁着清明假期特地学习了一下,主要是借鉴了《深入浅出MySQL——数据库开发、优化与管理维护》一书,下面就是对数据库事务的一些总结,以及在Django中的使用。
事务基础
个人的理解:“事务”就是对一系列与数据库相关操作的封装。主要是当你在对数据库进行“写”操作时,你可以将一系列的操作进行“捆绑”,当且仅当所有这一系列操作都执行成功时,你再进行“一次提交”通知Mysql进行一次性地处理。如果中间的某一项操作出现了异常,你可以对整个事务包含的所有操作(包括已经成功的操作)进行“回滚”,回到执行前的状态。此外,你可以在你的这一系列操作中间设置“保存点”(savepoint),当出现异常时,你可以选择回滚到指定保存点,使得该保存点之前的所有操作执行有效,该保存点后的所有操作都被回退。
默认情况下,Mysql的事务是自动提交的,即你每执行一条sql语句,都会立即提交到数据库。
下面是事务的一些基本操作:
start transaction —— 开始事务
commit —— 提交事务
commit and chain —— 提交事务,并启动一个新的事务
rollback —— 回滚事务(默认回滚整个事务)
savepoint test —— 定义savepoint,名为test (注意:mysql支持指定回滚事务的一个部分,但是不能指定提交事务的一个部分;复杂的应用可以通过定义多个不同的savepoint,满足不同的条件回滚到不同的savepoint;但是,如果定义了相同名字的savepoint,则后面定义的会覆盖前面定义的!)
rollback to savepoint test —— 回滚到savepoint test处
release savepoint test —— 删除名为test的savepoint
下面是一些实际的操作:
1. 事务开始和提交。
首先通过cmd命令行打开两个数据库连接(session1、session2),初始时表lists_userinfo为空。
步骤1:session1中执行操作:开始事务--插入1行数据--查询
图2 事务开始和提交(1)
步骤2:session2中执行操作:查询
图3 事务开始和提交(2)
可以看到,此时session1中开始了事务,且并未提交,session2中并未查询到任何结果,而session1可以查询到自己已经提交的数据。
步骤3:session1中执行操作:提交事务--查询
图4 事务开始和提交(3)
步骤4:session2中执行操作:查询
图5 事务开始和提交(4)
可以看到,此时session2中才可以查询到session1插入的数据。即:只有当事务完成提交后,事务中的操作才会真正应用到数据库中。
2. 事务回滚。
步骤1:session1执行操作:开始事务--插入1行数据--查询--回滚--查询
图6 事务回滚(1)
可以看到,在rollback后,事务的操作被回滚,实际并没有应用到Mysql中生效。
3. 回滚到指定savepoint及删除指定savepoint。
步骤1:session1执行操作:开始事务--插入1行数据--设置savepoint--再插入1行数据--查询--回滚到savepoint
图7 回滚到指定savepoint及删除指定savepoint(1)
可以看到,当回滚到指定保存点cy后,在保存点cy之后插入的数据没有提交到数据库。那么此时,在session2中进行查询会是什么结果呢?我们来看看步骤2。
步骤2: session2执行操作:查询
图8 回滚到指定savepoint及删除指定savepoint(2)
可以看到此时session2中并没有查询到session1中插入的数据。
步骤3: session1执行操作:提交
图9 回滚到指定savepoint及删除指定savepoint(3)
步骤4: session2执行操作:查询
图10 回滚到指定savepoint及删除指定savepoint(4)
可以看到,即使rollback后,也需要执行commit操作,数据库的事务才能应用生效。
对于删除savepoint,大家可以自己尝试验证一下。步骤如下:
图11 回滚到指定savepoint及删除指定savepoint(5)
可以看到,当删除了savepoint后,系统就找不到指定的savepoint了。
延伸
另外,作为一个Mysql菜鸟,在实践过程中还发现以下两点跟锁相关的事务特性:
1.rollback不能回滚锁。
2.start transaction开启新事务,会自动执行一个隐含的unlock tables操作。
步骤1:session1执行操作: 对表加write锁--插入1行数据
图12 事务与锁(1)
步骤2:session2执行操作:查询
图13 事务与锁(2)
此时session2的查询处于阻塞状态。
步骤3:session1执行操作:start transaction
图14 事务与锁(3)
步骤4:察看session2中之前的”查询“语句执行结果
图15 事务与锁(4)
可以看到,此时session2中的查询执行成功,说明session1中执行start transaction会隐含地将write锁释放。