场景还原
为了能够清楚的描述这个问题,我们写一个简单的服务,语言使用python,框架使用tornado,数据库使用mysql,数据库连接使用MySQLdb。
在数据库里有这样一张表:
1 | drop table if exists singers; |
而我们的服务器代码也是很简单的,根本没什么服务可言,就是收到访问,就读取id为1的歌手姓名,并打印:
1 | import MySQLdb |
并且我们的数据库中会有这样一条记录:
+----+------------+
| id | name |
+----+------------+
| 1 | jay |
+----+------------+
在shell请求此服务和打印的结果:
curl http://localhost:8888
(jay,)
现在我们保持服务器开启,在mysql中输入如下命令:
update singers set name = 'leehom' where id = 1;
再在shell请求此服务和打印的结果:
curl http://localhost:8888
(jay,)
好的,bug出现了!那么有两种方式解决它,第一种是重启服务器,这种方法可以忽略了。第二种就是将代码修改成这种方式:
1 | cur = conn.cursor() |
1 | cur = conn.cursor() |
注意那条commit语句,本文其余部分解读此问题原因。
什么是事务
事务(Transaction)是并发控制的基本单位。是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。
比如一个表在更新插入语句中,会经历a,b,c,d,e五个状态,那么如果我将b->c->d的操作放到一个事务中,那么外界看到的就是a状态或者是e状态,再不会看见其它状态。
a -> b -> c -> d
-> e
事务支持
首先要了解到,Innodb支持事务, MyISAM不支持事务。
而且mysql的事务是默认是打开autocommit的,但是我们使用的python库,mysqldb却是默认关闭autocommit,所以你需要自己去commit,自己去rollback。
回到问题
其实最主要的原因是我们相当然的认为这是一个读(查询)操作,我们没有必要提交。
但是首先,我们的代码在不停的复用同一条连接,这是问题的起因之一。
MySQL事务的默认隔离级别是repeatable read(重复读),而这却会引起phantom read(幻想读)。
幻像读(phantom read):在同一事务中,同一查询多次进行时候,由于其他插入操作(insert)的事务提交,导致每次返回不同的结果集。
而innodb为了防止幻读,会在一个事务中,使所有select的结果保持与第一次select时的snapshot一致,这样其他事务的插入是不会被感知到的。
所以对于一个只有读的事务,我们也应该及时提交后再读,使snapshot刷新。