Django中select_for_update方法的应用

背景

在最近做的一个消耗能量型的需求中,因为 Nginx 的重复转发或者客户端的连续点击,造成了同样的接口在极短时间内被调用两次,数据库中出现了重复记录。
于是,我想利用 MySQL 的锁机制,在某一条记录被操作的时候,给此条记录添加行锁,就可以避免出现上述 bug。

解决流程

定位 bug 过程不再赘述,解决流程如下:

  • 在利用 Django ORM 取数据时,在查询语句前添加 select_for_update() 方法。
     添加前:existUser = User.objects.get(id=userId)
     ====================================================================
     添加后:existUser = User.objects.select_for_update().get(id=userId)
    
  • select_for_update() 这个方法有两个默认参数,nowait=Falseskip_locked=False
    nowait的含义是匹配的记录被锁时不等待,会抛异常。但是 MySQL8.0 以前不支持。
    skip_locked的含义是SELECT时跳过被锁的记录。
    # 源码
    def select_for_update(self, nowait=False, skip_locked=False):
        """
        Returns a new QuerySet instance that will select objects with a
        FOR UPDATE lock.
        """
        if nowait and skip_locked:
            raise ValueError('The nowait option cannot be used with skip_locked.')
        obj = self._clone()
        obj._for_write = True
        obj.query.select_for_update = True
        obj.query.select_for_update_nowait = nowait
        obj.query.select_for_update_skip_locked = skip_locked
        return obj
    
  • 第三点,select_for_update()方法必须应用在事务中,可利用@transaction.atomic()装饰器包裹视图函数
    from django.db import transaction
    
    @transaction.atomic
    def doSomeThing(request):
        pass
        return HttpResponse('OK')
    

注意

  • 在我的业务中,由于我封装了由 requestuser对象 的方法,所以查询更新被分开到不同的文件、不同的方法里,这里就需要在两个方法上都加装饰器,否则行锁不生效。

挖坑

  • 因为用的 MySQL 版本是 5.7 ,nowait=True不可用,所以暂时不知道如何应对可能出现的死锁

你可能感兴趣的:(数据库,Django,ORM,MySQL)