您的当前位置:首页正文

DRF踩坑小记

2024-11-30 来源:个人技术集锦

1 REST风格简介   

        今年年初有个项目需要用到Django,第一次接触REST(Representational State Transfer。表述性状态传递)风格的编程语言,也就是面向资源编程。面向过程、面向对象和面向资源都是抽象角度的不同,按周志明老师的说法,即:

  • 面向过程编程时,为什么要以算法和处理过程为中心,输入数据,输出结果?当然是为了符合计算机世界中主流的交互方式。
  • 面向对象编程时,为什么要将数据和行为统一起来、封装成对象?当然是为了符合现实世界的主流的交互方式。
  • 面向资源编程时,为什么要将数据(资源)作为抽象的主体,把行为看作是统一的接口?当然是为了符合网络世界的主流的交互方式。

        这也是为啥面向过程的语言性能一般最高,而目前却大量使用面向对象,因为面向对象更容易理解,在大型项目中,大家协作起来更方便。再抽象一点面向对象其实是一个主客体的问题,你编写的系统其实是subject,而系统产生或者操作的对象都是object。

        而其实在REST风格之前,还涌现了好多别的架构,CORBA、Web Service但都因为这样那样的原因而未流行起来(具体可以参考周志明老师的新书《凤凰架构》)。

        而REST这种架构风格对HTTP原有的语义进行了阐述,在这种风格中,原有如在传统Java中,你要获得用户信息,可能需要建个接口getUserInfo;删除数据要写个接口命名为deleteUserById等等。但是,在REST这种风格中,你使用HTTP的GET方法,然后URL中使用User,它的语义即为查询user;当你需要删除用户id为1的用户,只需要使用HTTP的DELETE,然后URL使用/user/1/,它的语义便是删除id为1的用户。

2 REST存在问题

1.对事务的支持。这几乎是最大的问题,下面Django也会谈到这个问题。

2.基本依赖于HTTP。UDP等无法支持。

3.批量处理问题。当我需要增加100条数据时,总不能使用调用100次PUT吧。

4.幂等性的问题。这个大多数REST风格框架实现中,基本都有一个实现,比如POST是更新的语义,不是幂等的,但是PUT这种增加,我误操作多按了一次,不能增加两条吧。

3. DRF

        Django 是一个由 Python 编写的一个开放源代码的 Web 应用框架。而DRF(Django REST Framwork)是通过Django对REST风格的一种实现,很多一些网站如知乎、豆瓣和微信支付的一些业务都是Django。

        Django已经很重了,DRF更重,经常调起错来很多层错误嵌套,调试起来特别麻烦。但是开发和上手起来是真快。比如当我使用DRF生成一个app后,我只需要下面两行代码就可以使用HTTP的语义设备表进行增删改正了。

class Device_numVice(viewsets.ModelViewSet):
    # queryset要传queryset对象,查询了所有的图书
    # serializer_class使用哪个序列化类来序列化这堆数据
    queryset = Device_num.objects.all()
    serializer_class = Device_numSerializer

上面代码中需要有个Device_num的类,相当于Java里的POJO

class Device_num(models.Model):
    did = models.IntegerField('设备id',)
    num = models.IntegerField('设备值', default=0)
    update_time = models.DateTimeField('更新时间', auto_now_add=True)

    class Meta:
        verbose_name = "设备值"
        db_table = "device_num"

    def __str__(self):
        return self.num

        然后有个序列化,默认的就行:

class Device_numSerializer(serializers.ModelSerializer):
    class Meta:
        model = Device_num
        fields = "__all__"

        其余都是系统内置类,这种方式上手简直太快了,当然毕竟是Python实现,而且框架重,性能较一般。

4. DRF看隔离性问题

        这个问题之前学习的时候每太重视,教研室项目这事务方面的要求很少,但之前有一次实验室的数据库勒索病毒问题,现在对MySQL增加了binlog的备份,然后定期将这个备份使用git发布到Gitee的私有库上。当这一套设置完以后,发现服务器上所有用DRF开发的服务都不可用了。

'Cannot execute statement: impossible to write to binary log since 
BINLOG_FORMAT = STATEMENT and at least one table uses a storage engine 
limited to row-based logging. InnoDB is limited to row-logging when 
transaction isolation level is READ COMMITTED or READ UNCOMMITTED.')

        翻译过来大概是

        '无法执行语句:无法写入二进制日志,因为BINLOG_FORMAT=STATEMENT,并且至少有一个表使用的存储引擎仅限于基于ROW的日志记录。当事务隔离级别为“读已提交”或“读未提交”时,InnoDB仅限于ROW日志记录。

        然后一般MySQL的隔离级别是可重复读,这里说因为数据库设置是读已提交或读未提交,无法使用binlog的STATEMENT模式。但我查询数据库的隔离级别确实还是可重复读。

 select @@transaction_isolation;

        再往下查,发现Django默认的事务隔离级别是读已提交。此时解决方案为两种:1是将Djang连接时也默认设置为可重复读。2.将binlog设置为row或格式。

mysql> SET SESSION binlog_format = 'ROW';
mysql> SET GLOBAL binlog_format = 'ROW';
mysql> flush privileges;

注:因为binlog已经是STATEMENT了,设置为MIX可能会失败。MIX的意思是由MySQL决定何时使用ROW,何时使用STATEMENT。

为什么呢?为什么在读未提交和读已提交下他的binlog不能设置为STATEMENT?

        其实很简单,binlog是数据库服务层级别的日志,除了备份外还要用来做主从数据库的备份,而他的提交机制只是简单的时间前后行为,当事务提交时它也生成,而对事务的隔离性支持用到的undolog是在InnoDB存储引擎下才是生效,对数据隔离性的支持也是靠这个日志,而这个日志并不会进行主从同步。从库的undolog生成是按binlog回放时的时间进行生成的,而主库binlog的的生成时按事务提交前后进行持久化的,此时出现主从undolog日志原意义不一致的情况。

注:对undolog的同步是很难实现的,总不能undolog没更新一下就同步,这样对性能影响太大了,而且undolog是InnoDB特有的,总不能再开发一套undolog的同步机制。

        即使在读已提交的级别下,你对某表的操作,在主库中因为有redolog和undolog保证,会有对应隔离性支持,但它同步到从库这个隔离性就无法支持了,会导致主从数据不一致。如:
 

user(id,scoe)

1,10

2,22

3,33
//其中id自增

在事务1(先于事务2开始):

INSERT INTO user(score) VALUES(44);

事务提交;

在事务2中(后于事务1开始):

INSERT INTO user(score) VALUES(55);

//事务2提交。事务2的binlog也会提交
//此时事务1提交,binlog也会提交

此时主库应该是:

user(id,scoe)

1,33

2,10

3,10

4,44

5,55

而从库执行顺序却是事务1执行完后,再执行事务2,数据成了

user(id,scoe)

1,33

2,10

3,10

4,55

5,44

        而ROW格式的日志记录的不是语句,而是这一行数据变化前和后的真实数据。当然从存储空间上讲,ROW比STATEMENT大了最少一倍。(这也是为啥有MIX格式的原因)。

        在不可重复读的模式下,因为GAP锁的存在,事务2是不允许插入,需要等待事务1提交才行。

        但是,哪怕是在可重复读情况下还是不建议使用Statement模式,仍然容易出问题,如中有 limit,所以这个命令可能是 unsafe 的,因为主从走的索引不一定一样。

        而按林晓斌老师的说法,现在大多数互联网公司会使用ROW格式的MySQL,因为即便是MIX,他里面也会有STATEMENT,恢复日志的时候很难自己定制。且STATEMENT会强行依赖上下文,可能带来数据的不一致问题。

参考文献:

1.林晓斌,《MySQL实战45讲》,极客时间。

2.周志明,,GitHub的开源文档。

显示全文