Django分表的设计与实现

需求背景

由于数据量快速增长,导致接口性能问题,而分表是一个比较有效的解决方式。这里主要探讨以下几个问题

  1. 哪些model需要分表。
  2. 分表的方式。
  3. 如何动态创建表。
  4. 如何动态地对相应表进行增删查改。
  5. 分表后ForeignKey及ManyToManyField如何处理。
  6. 分表后migration问题。

哪些model需要分表

首先是从接口性能出发,用jmeter测试一遍接口,找出存在性能问题的接口,然后添加打点日志,找到影响性能的相关model。基本上是一些数据量大的、关联关系多、存在多个textfield字段查询的一些model。这里要说的是Django一些自带的接口,是存在比较严重的性能问题的,比如bulk_update批量更新的接口,如果更新的object中有多对多的关系,通过打debug日志可以看到Django做了很多连表操作,就会特别慢,甚至要好几秒。

我们系统最后是确定对五个大表进分表,按照一个标注任务(数据量几万到几十万不等)为单位进行分表。

分表的方式

纵向分表,就是竖着把一个很多字段的表分为N个,这N个表是1对1的关系,这个方案不适用我们系统。

横向分表,顾名思义就是横着把一个表分为N个,表的结构是一样的,是根据不同规则进行分表,比如id为单数一个表,双数一个表。我们采用的就这个方案,使用任务id来进行分表,一个任务一个表。

如何动态创建表

Django 提供了根据model创建表的方法 create_model,具体可以参考官方文档,下面有具体实现的例子。

如何动态地对相应表进行增删查改

有了根据model创建表的方法之后,那么我们要考虑的就是如何动态生成model。我调研了一下网上大佬们的做法,梳理了一下,大致有两种方法:

  1. 在model中封装方法,然后通过type函数来动态的生成model,从而达到动态的对表进行增删查改。具体可以查看这篇文章(文章里的方案二可以直接忽略,直接拼接sql居然也算一种方案),但是这种方法有个致命的缺点,就是需要对model的get、filter、update等方法都进行二次封装,属实麻烦。
  2. 对model进行二次封装,通过传参来动态修改Meta类中的db_table字段。这种方法可以完全复用Django orm的所有方法,而且实现起来很简单。下面我们就来实现一下。
from django.db import models, connection

# 对model进行二次封装
def get_task_data_model(task_id=None, init=False):
    table_name = 'task_data' # 这里是没有做分表的老数据,需要兼容一下
    split_table_name = 'task_data_%r' % task_id
    if task_id and (split_table_name in connection.introspection.table_names() or init):
        table_name = split_table_name
    class TaskData(models.Model):
        status = models.IntegerField(default=0)
        desc = models.TextField(null=True)
        ctime = models.DateTimeField(auto_now_add=True)
        mtime = models.DateTimeField(auto_now=True)

        class Meta:
            db_table = table_name
    return TaskData


# 根据动态model来创建相应的表
with connection.schema_editor() as schema_editor:
    logger.info("start create table [%s]" % table_name)
    task_data_model = get_task_data_model(task_id, init=True)
    schema_editor.create_model(task_data_model)
    logger.info("create table [%s] successfully" % table_name)
        
# 根据动态model进行增删查改,Django orm的方法都可以使用
task_data_model = get_task_data_model(task_id)
task_data_model.objects.create()            # 增
task_data_model.objects.filter()            # 查
task_data_model.objects.filter().delete()   # 删
task_data_model.objects.filter().update()   # 改

分表后ForeignKey及ManyToManyField如何处理

为了说明这个问题,我举个例子,假设分表前我有两个model,User和Project,Project里有一个创建人creator的字段,外键关联了User表:

creator = models.ForeignKey(
    User,
    db_constraint=False,
    null=True,
    on_delete=models.DO_NOTHING
)

我们知道,外键实际存的是一个id,假如User进行了分表,每个表都是自增id,那么creator_id就会出现重复的情况,我们就不知道他是来自哪个分表的user了。ManyToManyField也是如此。解决这个问题有几种方式

  1. 分表后不能采用单表自增id,维护一个全局自增的id,然后通过id来判断来自哪个分表,这种方法实现比较麻烦,稍有不慎就会出现bug。
  2. Project表增加一个字段,用于记录是关联了哪个分表。查询关联信息时filter多加一个字段。
  3. 把ForeignKey字段改成IntergerField类型,查询时不能再用’.’查询,最后我们采用的是此方法。

你可能感兴趣的:(python,django,python)