Pulpcode

捕获,搅碎,拼接,吞咽

0%

记一次使用MySQLdb事务时,select出现的bug

场景还原

为了能够清楚的描述这个问题,我们写一个简单的服务,语言使用python,框架使用tornado,数据库使用mysql,数据库连接使用MySQLdb。

在数据库里有这样一张表:

1
2
3
4
5
drop table if exists singers;
CREATE TABLE singers(
id int PRIMARY KEY AUTO_INCREMENT,
name char(20) NOT NULL
);

而我们的服务器代码也是很简单的,根本没什么服务可言,就是收到访问,就读取id为1的歌手姓名,并打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import MySQLdb
import tornado.ioloop
import tornado.web

host = '127.0.0.1'
user = 'root'
passwd = '627'
port = 3306
db = 'test'

conn = MySQLdb.connect(host=host, user=user, passwd=passwd, port=port, db=db)

class MainHandler(tornado.web.RequestHandler):
def get(self):
cur = conn.cursor()
cur.execute("select name from singers Where `id` = 1")
res = cur.fetchone()
print res
self.write("ok")

application = tornado.web.Application([
(r"/", MainHandler),
])

if __name__ == "__main__":
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()

并且我们的数据库中会有这样一条记录:

+----+------------+
| 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
2
3
4
cur = conn.cursor()
cur.execute("select name from singers Where `id` = 1")
res = cur.fetchone()
print res
1
2
3
4
5
cur = conn.cursor()
cur.execute("select name from singers Where `id` = 1")
res = cur.fetchone()
conn.commit()
print res

注意那条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刷新。