BLOG
Enjoy when you can, and endure when you must.
JUN 08, 2015/Django
MySQL 防止重复数据插入

做后端开发,我们必须考虑多线程的情况,那这时就必须重视线程安全的问题。

有这样一种场景:首先环境是 Python + Django + MySQL,我们的每一张表都有一个字段 is_deleted 用来标识该条数据是否被删除,也就是当用户在删除数据的时候,服务器并不是真正将其删除,而只是简单的在 is_deleted 字段上标记一下。假如有下面的一个 MODEL:

class Test(models.Model):

    name = models.CharField(max_length=32)
    is_deleted = models.BooleanField(default=False)

    class Meta:
        verbose_name = 'test'

一个 name 和之前所说的 is_deleted 两个 field。现在有一个需求是同一 name 在表中有且只能存在一份有效的数据。在 Django 的 ORM 中有一个 get_or_create() 的简单方法貌似能满足需求:

test, created = TestModel.objects.get_or_create(name='t1', is_deleted=False)

乍一看这好像是那么回事,但细看 get_or_create() 会发现这里会存在线程安全的问题,而阅读 Django 的官方文档也确实提到了这一点。这里利用 nginx + uwsgi 来运行工程,在使用 webbench 来模拟多用户的并发请求:

webbench -c 2000 -t 1 http://127.0.0.1/test/

最终我们发现这个入口变成了永无止境的 500:

它确实在这种时候不是线程安全的,通常的解决方案是在 name 上增加 unique 限制。但这里因为有 is_deleted 的存在而打破了常规,导致没法这么操作。

我开始一直考虑能从 Python 的角度去解决这个问题,但我发现这反而使问题变得复杂化。而反过来,在 MySQL 的层面却有一个很直观的解决方法:insert ... where not exists,如下所示:

...

sql = "INSERT INTO `inserttest_test` (name, is_deleted) SELECT '{name}', 0 FROM DUAL WHERE NOT EXISTS (SELECT name FROM `inserttest_test` WHERE name='{name}' AND is_deleted=0)"

from django.db import connection

cursor = connection.cursor()
cursor.execute(sql.format(name='t1'))

...

再次模拟大并发观察多线程下的情况,数据不在出现重复插入的问题。

当然,这仅是我对此的一种处理方案。

COMMENTS
17/07From test

nice

12/08From felix

学习了!!! =_=

17/06From dotdog

为什么浏览器不用Chrome?

08/06From rory

学习了!!!!

LEAVE COMMNT