做后端开发,我们必须考虑多线程的情况,那这时就必须重视线程安全的问题。
有这样一种场景:首先环境是 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'))
...
再次模拟大并发观察多线程下的情况,数据不在出现重复插入的问题。
当然,这仅是我对此的一种处理方案。