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

相关文章

来自专栏james大数据架构

微软官方提供的用于监控MS SQL Server运行状况的工具及SQL语句

Microsoft SQL Server 2005 提供了一些工具来监控数据库。方法之一是动态管理视图。动态管理视图 (DMV) 和动态管理函数 (DMF) 返...

2967
来自专栏杂烩

mycat安装使用 原

    github地址:https://github.com/MyCATApache/Mycat-Server/wiki

1382
来自专栏Spark学习技巧

phoenix二级索引

二级索引 二级索引是从主键访问数据的正交方式。Hbase中有一个按照字典排序的主键Rowkey作为单一的索引。不按照Rowkey去读取记录都要遍历整张表,然后按...

7689
来自专栏编程心路

语言小知识-MySQL数据库引擎

MySQL 作为全世界广受欢迎的数据库,被用于很多中小型的项目中,但是你对 MySQL 数据库的存储引擎了解多少呢?

1394
来自专栏蓝天

高性能高可用的分布式唯一ID服务——mooon-uniq-id

源码位置:https://github.com/eyjian/mooon/tree/master/application/uniq_id。

792
来自专栏数据和云

深入内核:Oracle数据库里SELECT操作Hang解析

崔华,网名 dbsnake Oracle ACE Director,ACOUG 核心专家 编辑手记:感谢崔华授权我们独家转载其精品文章,也欢迎大家向“Oracl...

37510
来自专栏Python

MySQL常见的库操作,表操作,数据操作集锦及一些注意事项

一 库操作(文件夹) 1 数据库命名规则 可以由字母、数字、下划线、@、#、$ 区分大小写 唯一性 不能使用关键字如 create select 不能单独使用数...

2589
来自专栏Netkiller

数据库进程间通信解决方案

数据库进程间通信解决方案 数据库与其他第三方应用程序进程间通信解决方案 摘要 你是否想过当数据库中的数据发生变化的时候出发某种操作?但因数据无法与其他进程通信(...

3976
来自专栏禅林阆苑

Sphinx&coreseek实现中文分词索引

众所周知,mysql等数据库的LIKE模糊搜索不支持索引,因此查询效率极低,需要结合第三方索引引擎程序(索引程序)来提高查询性能。

3093
来自专栏idba

漫谈死锁

一 前言 死锁是每个MySQL DBA 都会遇到的技术问题,本文是自己针对死锁学习的一个总结,了解死锁是什么,MySQL如何检测死锁,处理死锁,死锁的案例,...

1584

扫码关注云+社区

领取腾讯云代金券