协程造成的数据库死锁

最近产线上的应用遇到了一个数据库的死锁问题(不能严格的算死锁),在使用eventlet这类协程库的应用可能都会遇到,所以记录下。

应用的表现为对请求无响应,此时登陆数据库上show processlist下后可以看到大量的session在上面处于Lock状态(这里用的MySQL),也就是在请求锁,同时可以发现一个会话处于Sleep状态。通过Time字段可以看到这个Sleep的会话已经连上来很久了,对于提供RESTFul服务的无状态的应用来说,如果使用的数据库连接是短连接,那么不应该会有这么长时间的连接且还处于Sleep状态。可以简单的推断这个Sleep的会话持有了锁,其它的会话都在等这把锁释放,但这个会话一直处于Sleep状态不释放这把锁。

为什么会出现一直Sleep的会话呢?一种原因是我们的数据库连接开始了一个事物,在数据库上执行了一些查询,然后转到代码上执行了一些代码但并未关闭数据库连接,在应用在执行代码的这段时间内其状态就是Sleep的。例如:

def query():
    db.connect()
    db.execute('LOCK TABLES XXX WRITE')
    db.execute('SELECT * FROM A')
    data = db.fetchall()
    _do_sth_on_data(data) //这个操作在进行的时候,数据库上看到的这个连接就处于Sleep状态
    db.execute('UNLOCK TABLES')
    db.disonnect()

现在我们看下在协程的使用下会发生什么情况。例如:

def query():
    db.connect()
    db.execute('LOCK TABLES XXX WRITE')
    db.execute('SELECT * FROM A')
    data = db.fetchall()
    _do_sth_on_data(data) //这个操作会有阻塞IO的请求,因此切换到了另一个协程执行代码
    db.execute('UNLOCK TABLES')
    db.disonnect()

此时_do_sth_on_data阻塞了IO,把CPU让给了其它协程,假设其让给了本线程中的另一个协程,而另一个协程执行了如下的代码:

    db.connect()
    db.execute('LOCK TABLES XXX WRITE')
    //此时这个协程被阻塞了,因此获取不到XXX这张表的写锁

这里假设我们用的是MySQLdb这个C语言的模块来和MySQL操作,由于eventlet只能green纯python的代码,因此MySQLdb产生的阻塞操作并不会造成协程上下文的切换,因此同一个线程下面的两个协程就形成了不严格的死锁。

目前对于这个问题,我这采取的方法是调整锁超时时间。之后会考虑替换掉eventlet,或者是测试下pymysql的性能。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*