可重复读事务隔离级别之 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 条评论
登录 后参与评论

相关文章

来自专栏沃趣科技

配置详解 | performance_schema全方位介绍

在上一篇 《初相识 | performance_schema全方位介绍》 中粗略介绍了如何配置与使用performance_schema,相信大家对perfor...

7677
来自专栏L宝宝聊IT

SQL server 数据库的存储过程和触发器

1263
来自专栏乐沙弥的世界

Heap size 80869K exceeds notification threshold (51200K)

      前阵子的alert日志获得了所需堆尺寸的大小超出指定阙值的提示,即Heap size 80869K exceeds notification thr...

813
来自专栏散尽浮华

Python3出现“No module named 'MySQLdb'“问题-以及使用PyMySQL连接数据库

Python3 与 Django 连接数据库,出现了报错:Error loading MySQLdb module: No module named 'MySQ...

1062
来自专栏杨建荣的学习笔记

关于db_files和maxdatafiles的问题(r4笔记第31天)

昨天在做生产监控的时候发现有个库的表空间不够了,就发邮件给客户的dba去处理,但是得到的反馈是尝试添加的时候发现已经超过了数据文件的最大数限制。这个错误毫无疑问...

3136
来自专栏你不就像风一样

Hibernate各种主键生成策略与配置详解

主键由外部程序负责生成,在 save() 之前必须指定一个。Hibernate不负责维护主键生成。与Hibernate和底层数据库都无关,可以跨数据库。在存储对...

812
来自专栏转载gongluck的CSDN博客

IOCP反射服务器

这两天学习了一下IOCP网络模型。 主要参考了这两片文章:http://blog.csdn.net/neicole/article/details/754949...

3448
来自专栏乐沙弥的世界

参数CONTROL_FILE_RECORD_KEEP_TIME和MAXLOGHISOTRY

--**************************************************

803
来自专栏跟着阿笨一起玩NET

Server 2005中的分区表(一)

本文转载:http://blog.csdn.net/smallfools/article/details/4930810

422
来自专栏杨建荣的学习笔记

海量数据迁移之外部表切分(r2笔记52天)

在前几篇中讨论过海量数据的并行加载,基本思路就是针对每一个物理表都会有一个对应的外部表,在做数据迁移的时候,如果表有上百G的时候,一个物理表对应一个外部表性能上...

2807

扫码关注云+社区