可重复读事务隔离级别之 django 解读

事务作为并发访问数据库一种有效工具,如果使用不当,也会引起问题。mysql是公司内使用的主流数据库,默认事务隔离级别是可重复读。

本文尝试结合django解释应用开发中并发访问数据库可能会遇到的可重复读引起的问题,希望能帮助大家在开发过程中有效避免类似问题,如果老版本应用中出现这类问题也可以快速定位。

由于django1.3(由于历史原因,目前蓝鲸体系内大多数稳定运营的工具系统用的是django1.3)中该问题最为严重,本文先对django1.3环境中的一个应用案例进行分析,说明问题产生的具体原因,然后说明如何有效避免类似问题,最后介绍较新版本django中事务实现原理(django1.6开始已经很好避免本文案例中的大多数情况),并提供一个django1.8中由于对事务使用不当造成的异常案例。

先看下如下这段代码在django1.3中会有什么问题:

class MyData(models.Model):
    key = models.CharField(primary_key=True, max_length=64)
    value = models.FloatField()

def simple_test(request):
    key = str(uuid.uuid4())
    set_data_in_backend.apply_async(args=(key, ))
    sleep(1)  # do something
    obj, r = MyData.objects.get_or_create(key=key, defaults={"value": 1})
    return HttpResponse(str(r))

@task  # celery task
def set_data_in_backend(key):
    obj, r = MyData.objects.get_or_create(key=key, defaults={"value": 0})
    return r

通过链接http://127.0.0.1:8000/simple_test/请求得到的结果是500错误, 如果开启了debug,则可以看到如下错误信息:

IntegrityError at /simple_test/
(1062, "Duplicate entry '6d8587ff-d983-4fd3-baab-a987faf4ae78' for key 1")
...

这个执行结果有点让人吃惊,本应该返回False才对。

为了快速说明该问题产生的原因,这里将请求simple_test过程中simple_test和后台任务set_data_in_backend所执行的sql语句分别打印出来:

simple_test响应请求过程执行的sql:

set autocommit: False
query: SELECT `django_session`.`session_key`, `django_session`.`session_data`, `django_session`.`expire_date` FROM `django_session` WHERE (`django_session`.`session_key` = '4279838c2ca84586ff76514491ed457b'  AND `django_session`.`expire_date` > '2016-08-07 02:31:04' )
query: SELECT `auth_user`.`id`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`password`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`is_superuser`, `auth_user`.`last_login`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 1 
query: SELECT `home_application_mydata`.`key`, `home_application_mydata`.`value` FROM `home_application_mydata` WHERE `home_application_mydata`.`key` = 'd180d7c3-c23b-4940-a5b5-9381e835b7bd' 
query: INSERT INTO `home_application_mydata` (`key`, `value`) VALUES ('d180d7c3-c23b-4940-a5b5-9381e835b7bd', 1)
query: SELECT `home_application_mydata`.`key`, `home_application_mydata`.`value` FROM `home_application_mydata` WHERE `home_application_mydata`.`key` = 'd180d7c3-c23b-4940-a5b5-9381e835b7bd' 
query: SELECT `home_application_mydata`.`key`, `home_application_mydata`.`value` FROM `home_application_mydata` LIMIT 21

后台任务set_data_in_backend执行过程中执行的sql语句:

set autocommit: False
query: SELECT `home_application_mydata`.`key`, `home_application_mydata`.`value` FROM `home_application_mydata` WHERE `home_application_mydata`.`key` = '6e3247f8-31c5-46d7-a3e9-1c855077ea56'
sql-debugger(connection): INSERT INTO `home_application_mydata` (`key`, `value`) VALUES ('6e3247f8-31c5-46d7-a3e9-1c855077ea56', 0)
commit
query: SELECT `celery_taskmeta`.`id`, `celery_taskmeta`.`task_id`, `celery_taskmeta`.`status`, `celery_taskmeta`.`result`, `celery_taskmeta`.`date_done`, `celery_taskmeta`.`traceback`, `celery_taskmeta`.`hidden`, `celery_taskmeta`.`meta` FROM `celery_taskmeta` WHERE `celery_taskmeta`.`task_id` = 'fd292219-da59-45a4-8b59-89ab1152c20c'
query: INSERT INTO `celery_taskmeta` (`task_id`, `status`, `result`, `date_done`, `traceback`, `hidden`, `meta`) VALUES ('fd292219-da59-45a4-8b59-89ab1152c20c', 'SUCCESS', 'gAKILg==', '2016-08-07 02:35:24', NULL, 0, 'eJxrYKotZIzgYGBgSM7IzEkpSs0rZIotZC7WAwBWuwcA')
commit

结合simple_test响应过程执行的sql语句来看,就比较好理解上面的500错误duplicate entry了。响应开始的时候, 开发框架进行了一次用户登录认证,django设置了autocommit为False,这会直接开启一个事务

这时key=6e3247f8-31c5-46d7-a3e9-1c855077ea56的记录还不存在,由于mysql默认的事务隔离级别是可重复读,因此在simple_test整个事务期间,都找不到key=6e3247f8-31c5-46d7-a3e9-1c855077ea56的记录,所以simple_test执行到get_or_create会尝试插入一条记录key=6e3247f8-31c5-46d7-a3e9-1c855077ea56,但是在此之前后台任务已经向数据库中插入了这个key,simple_test执行get_or_create的时候mysql就给直接报一致性错误。

弄明白了这个异常发生的原理之后,我们可能会吓出一身冷汗,如果写个while循环一直去查询数据库中任务的状态到完成状态,岂不是死循环了。在django1.3中的确是这样,因为这个问题django1.3中的cache框架就被提交了Bug,django1.3遵循的是PEP 249Python数据库API 规范v2.0, 需要将autocommit初试设置为关闭状态。到了Django1.6之后已经覆盖了这个默认规范并且将autocommit设置为 on. 因此新版本的django出现上述问题的概率会大大降低。

我们可能会有些相对稳定运营的django1.3在生产环境,如果真的出现了类似的问题,可以尝试从几个方面修复:

(1)调整中间件,对登录认证完成之后进行一次commit操作。部分因为中间件过早开启事务的情形有用,比如本文的案例。

(2)发生类似错误时,显式进行一次commit操作。这种解决方式比较直观,但是如果错误本身就发生在事务中则会过早提交事务。

(3)如果只是需要把记录拿出来更新,可以考虑直接写sql更新记录。

为了说明django1.8中事务实现机制如何与django1.3不一样,将本文开始时使用案例放在django1.8中执行,调用的sql如下:

set autocommit: False
set autocommit: True
query: SET SQL_AUTO_IS_NULL = 0
set autocommit: False
set autocommit: True
query: SET SQL_AUTO_IS_NULL = 0
query: SELECT `django_session`.`session_key`, `django_session`.`session_data`, `django_session`.`expire_date` FROM `django_session` WHERE (`django_session`.`session_key` = 'd0q3afzgeilvh1zbdgkp19misc37dim5' AND `django_session`.`expire_date` > '2016-08-07 03:32:40')
query: SELECT `auth_user`.`id`, `auth_user`.`password`, `auth_user`.`last_login`, `auth_user`.`is_superuser`, `auth_user`.`username`, `auth_user`.`first_name`, `auth_user`.`last_name`, `auth_user`.`email`, `auth_user`.`is_staff`, `auth_user`.`is_active`, `auth_user`.`date_joined` FROM `auth_user` WHERE `auth_user`.`id` = 1
query: SELECT `test_123_mydata`.`key`, `test_123_mydata`.`value` FROM `test_123_mydata` WHERE `test_123_mydata`.`key` = '27ada689-86f4-4192-a0b9-dc6608d74ed9'

从django1.8中执行的sql可以看出,Django1.8的默认行为是运行在自动提交模式下。任何一个查询都立即被提交到数据库中,除非显示激活一个事务。最后,django1.8只是将这种可重复读引起问题的概率降低了很多,如果我们在事务中处理不当,也会引起类似问题,django本文最开始的例子进行稍微调整,在django1.8中运行一样会报错。

@atomic
def simple_test(request):
    keys = list(MyData.objects.values("key"))
    key = str(uuid.uuid4())
    set_data_in_backend.apply_async(args=(key, ))
    sleep(1)  # do something
    obj, r = MyData.objects.get_or_create(key=key, defaults={"value": 1})
    return HttpResponse(str(r))

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏乐沙弥的世界

Oracle 参数文件

主要用来记录数据库的配置文件,在数据库启动时,Oracle读取参数文件,并根据参数文件中的参数设置来配置数据库。

621
来自专栏清风

MySQL数据库 原

其中“/yourpath/crontab_mysql.sql”为需要执行的sql语句。

733
来自专栏青枫的专栏

day37_Spring学习笔记_05_CRM_01

CRM : custom releation manager 客户关系管理系统,用于维护客户和公司之间关系。 我们要做的是:学校 和 大家 之间关系。

342
来自专栏xcywt

学习SQLite之路(四)

20160621 更新 参考: http://www.runoob.com/sqlite/sqlite-tutorial.html 1. SQLite   a...

1898
来自专栏乐沙弥的世界

SQL*Loader使用方法

SQL*Loader由一个输入控制文件来控制整个装载的相关描述信息,一个或多个数据文件作为原始数据,其详细组成结构包括

582
来自专栏琯琯博客

Yii2 开发小技巧

2174
来自专栏蓝天

g++中宏NULL究竟是什么?

NULL是个指针,还是个整数?0?或(void*)0?答案是和g++版本有关。g++ 4.6支持C++11,引入了nullptr,也许会发生变化。

743
来自专栏idba

order by 主键id导致全表扫描的问题

一 简介 在检查某业务数据库的slowlog 时发现一个慢查询,查询时间 1.57s ,检查表结构 where条件字段存在正确的组合索引,正确的情况下优化器应...

702
来自专栏程序员的SOD蜜

使用OQL“语言”构造ORM实体类的复杂查询条件

OQL”语言“ 是PDF.NET数据开发框架的实体对象查询语言,一直以来,ORM的复杂查询条件都是困扰ORM的问题,所以很多时候不得不舍弃ORM,直接手工拼接S...

1756
来自专栏WindCoder

MySQL数据库对象与应用-MySQL数据库对象单元测验

这是微专业参加单元测试后的试题及答案整理,分享出来,供大家参考,所有标红的为答案。

683

扫码关注云+社区