MySQL Python并发访问同一表列

5 投票
1 回答
2559 浏览
提问于 2025-04-17 18:45

我尝试了几种解决方案,但都没成功。我有两个 Python 进程(是用 subprocess 的 Popen 创建的),还有一个 MySQL 表(叫做 persone),里面有一行数据,包含两个列:Age:0, id:1。一个进程会选择这一行,取出年龄并把它加一,这个过程会重复 1000 次。另一个进程做的事情是把年龄减一,它也会重复 1000 次。我是同时运行这两个进程的。

理论上,我希望最后年龄能保持在零。但是,实际上我总是得到一些随机值,范围在 100 到 -100 之间,这让我觉得可能是因为同时有多个进程在访问数据库,导致数据出错。我在想,我是不是漏掉了什么?

这是我用来测试的代码:

ajoutAge.py

import MySQLdb as lite

num=1000
connexion = lite.connect(host="localhost", user="root", passwd="123", db="test")
with connexion:
    for i in range(num):
       cur = connexion.cursor()
       cur.execute('start transaction')
       cur.execute('select Age from persone where id=1')
       age = cur.fetchone()[0]
       cur.execute('update persone set Age='+str(age+1)+' where id=1')
       # cur.execute('rollback')
       cur.execute('commit')
print "ajout Done"

retraitAge.py

import MySQLdb as lite

num=1000
connexion = lite.connect(host="localhost", user="root", passwd="123", db="test")
with connexion:
    for i in range(num):
        cur = connexion.cursor()
        cur.execute('start transaction')
        cur.execute('select Age from persone where id=1')
        age = cur.fetchone()[0]
        cur.execute('update persone set Age='+str(age-1)+' where id=1')
        cur.execute('commit')
print "retrait Done"

mymain.py

from subprocess import Popen

a=Popen("python ajoutAge.py", shell=True)
aa=Popen("python ajoutAge.py", shell=True)
b=Popen("python retraitAge.py", shell=True)
bb=Popen("python retraitAge.py", shell=True)

a.communicate()
aa.communicate()
b.communicate()
bb.communicate()
print "Main done"

我的表使用的是 InnoDB 存储引擎。

1 个回答

4

你正在制造一个竞争条件

每次你查询当前年龄的时候,和你更新数据之间会有一个很短的时间间隔。在这个间隔里,其他的进程可能会(而且有时确实会)更新这个值。

所以在第一个进程中,当你把年龄更新为年龄+1的时候,其实是基于一个稍微过时的值在增加。

要解决这个问题,你有几个选择:

  • SELECT ... FOR UPDATE,这个命令会锁定这一行数据,防止其他进程在你完成操作之前修改它。你需要确保不要提交你的查询事务,因为那样会释放锁,这样就可能再次出现竞争条件。

  • UPDATE persone SET age = age+1 WHERE id=1(当然也可以是age=age-1)。换句话说,就是在同一个表达式中读取值并修改它,这样就形成了一个原子操作,其他进程就无法在这个过程中“插队”去修改它。

撰写回答